summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--TODO.txt8
-rw-r--r--coverage/collector.py2
-rw-r--r--coverage/control.py25
-rw-r--r--coverage/plugin.py52
-rw-r--r--coverage/pytracer.py13
-rw-r--r--tests/plugin1.py5
-rw-r--r--tests/test_plugins.py37
7 files changed, 113 insertions, 29 deletions
diff --git a/TODO.txt b/TODO.txt
index 1de6e0b5..d15654e1 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -36,6 +36,14 @@ Key:
+ A pain, b/c of the structure of the tests.
+ BTW: make an easier way to write those tests.
+- Documentation
+ - Plugins!
+ Once per process
+ Once per file
+ - create a file tracer
+ - call its has_dynamic_source_file()
+ Once per call
+ Once per line
* --source stuff:
diff --git a/coverage/collector.py b/coverage/collector.py
index 72a7b7f1..6ffdc7c4 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -186,6 +186,8 @@ class Collector(object):
tracer.plugin_data = self.plugin_data
if hasattr(tracer, 'threading'):
tracer.threading = self.threading
+ if hasattr(tracer, 'check_include'):
+ tracer.check_include = self.check_include
fn = tracer.start()
self.tracers.append(tracer)
diff --git a/coverage/control.py b/coverage/control.py
index 550293c7..64175ee4 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -360,7 +360,10 @@ class Coverage(object):
file_tracer.plugin_name = plugin.plugin_name
disp.trace = True
disp.file_tracer = file_tracer
- disp.source_filename = self.file_locator.canonical_filename(file_tracer.source_filename())
+ if file_tracer.has_dynamic_source_filename():
+ disp.has_dynamic_filename = True
+ else:
+ disp.source_filename = self.file_locator.canonical_filename(file_tracer.source_filename())
else:
disp.trace = True
disp.source_filename = canonical
@@ -368,15 +371,16 @@ class Coverage(object):
if disp.trace:
if file_tracer:
disp.file_tracer = file_tracer
- if disp.source_filename is None:
- raise CoverageException(
- "Plugin %r didn't set source_filename for %r" %
- (plugin, disp.original_filename)
- )
- if disp.check_filters:
- reason = self._check_include_omit_etc(disp.source_filename)
- if reason:
- nope(disp, reason)
+ if not disp.has_dynamic_filename:
+ if disp.source_filename is None:
+ raise CoverageException(
+ "Plugin %r didn't set source_filename for %r" %
+ (plugin, disp.original_filename)
+ )
+ if disp.check_filters:
+ reason = self._check_include_omit_etc(disp.source_filename)
+ if reason:
+ nope(disp, reason)
return disp
@@ -903,6 +907,7 @@ class FileDisposition(object):
self.trace = False
self.reason = ""
self.file_tracer = None
+ self.has_dynamic_filename = False
def debug_message(self):
"""Produce a debugging message explaining the outcome."""
diff --git a/coverage/plugin.py b/coverage/plugin.py
index 1e6e2353..24a2b9a3 100644
--- a/coverage/plugin.py
+++ b/coverage/plugin.py
@@ -41,22 +41,58 @@ class CoveragePlugin(object):
`file_tracer`. It's an error to return None.
"""
- raise Exception("Plugin %r needs to implement file_reporter" % self.plugin_name)
+ raise NotImplementedError("Plugin %r needs to implement file_reporter" % self.plugin_name)
class FileTracer(object):
- """Support needed for files during the tracing phase."""
+ """Support needed for files during the tracing phase.
+
+ You may construct this object from CoveragePlugin.file_tracer any way you
+ like. A natural choice would be to pass the filename given to file_tracer.
+
+ """
def source_filename(self):
- return "xyzzy"
+ """The source filename for this file.
+
+ This may be any filename you like. A key responsibility of a plugin is
+ to own the mapping from Python execution back to whatever source
+ filename was originally the source of the code.
+
+ Returns:
+ The filename to credit with this execution.
+
+ """
+ return None
- def dynamic_source_file_name(self):
- """Returns a callable that can return a source name for a frame.
+ def has_dynamic_source_filename(self):
+ """Does this FileTracer have dynamic source filenames?
- The callable should take a filename and a frame, and return either a
- filename or None:
+ FileTracers can provide dynamically determined filenames by implementing
+ dynamic_source_filename. Invoking that function is expensive. To
+ determine whether it should invoke it, coverage.py uses the result of
+ this function to know if it needs to bother invoking
+ dynamic_source_filename.
- def dynamic_source_filename_func(filename, frame)
+ Returns:
+ A boolean, true if `dynamic_source_filename` should be called to
+ get dynamic source filenames.
+
+ """
+ return False
+
+ def dynamic_source_filename(self, filename, frame):
+ """Returns a dynamically computed source filename.
+
+ Some plugins need to compute the source filename dynamically for each
+ frame.
+
+ This function will not be invoked if `has_dynamic_source_filename`
+ returns False.
+
+ Returns:
+ The source filename for this frame, or None if this frame shouldn't
+ be measured.
Can return None if dynamic filenames aren't needed.
diff --git a/coverage/pytracer.py b/coverage/pytracer.py
index 84071bb1..b4fd59fa 100644
--- a/coverage/pytracer.py
+++ b/coverage/pytracer.py
@@ -28,6 +28,7 @@ class PyTracer(object):
self.arcs = False
self.should_trace = None
self.should_trace_cache = None
+ self.check_include = None
self.warn = None
self.plugin_data = None
# The threading module to use, if any.
@@ -83,13 +84,11 @@ class PyTracer(object):
self.cur_file_dict = None
if disp.trace:
tracename = disp.source_filename
- if disp.file_tracer:
- dyn_func = disp.file_tracer.dynamic_source_file_name()
- if dyn_func:
- tracename = dyn_func(tracename, frame)
- if tracename:
- if not self.check_include(tracename):
- tracename = None
+ if disp.file_tracer and disp.has_dynamic_filename:
+ tracename = disp.file_tracer.dynamic_source_filename(tracename, frame)
+ if tracename:
+ if not self.check_include(tracename):
+ tracename = None
else:
tracename = None
if tracename:
diff --git a/tests/plugin1.py b/tests/plugin1.py
index 9401e327..21e64aeb 100644
--- a/tests/plugin1.py
+++ b/tests/plugin1.py
@@ -1,4 +1,4 @@
-"""Plugins for test_plugins.py to import."""
+"""A plugin for test_plugins.py to import."""
import os.path
@@ -12,8 +12,7 @@ class Plugin(coverage.CoveragePlugin):
def file_tracer(self, filename):
"""Trace only files named xyz.py"""
if "xyz.py" in filename:
- file_tracer = FileTracer(filename)
- return file_tracer
+ return FileTracer(filename)
def file_reporter(self, filename):
return FileReporter(filename)
diff --git a/tests/test_plugins.py b/tests/test_plugins.py
index 7c4986a5..f2658998 100644
--- a/tests/test_plugins.py
+++ b/tests/test_plugins.py
@@ -137,7 +137,11 @@ class PluginTest(CoverageTest):
cov.start()
cov.stop()
- def test_importing_myself(self):
+
+class FileTracerTest(CoverageTest):
+ """Tests of plugins that implement file_tracer."""
+
+ def test_plugin1(self):
if sys.platform == 'win32':
raise SkipTest("Plugin stuff is jank on windows.. fixing soon...")
@@ -162,3 +166,34 @@ class PluginTest(CoverageTest):
self.assertEqual(missing, [])
_, statements, _, _ = cov.analysis("/src/try_ABC.zz")
self.assertEqual(statements, [105, 106, 107, 205, 206, 207])
+
+ def test_plugin2(self):
+ self.make_file("render.py", """\
+ def render(filename, linenum):
+ fiddle_around = 1 # vamp until ready
+ return "[{0} @ {1}]".format(filename, linenum)
+ """)
+ self.make_file("caller.py", """\
+ from render import render
+
+ assert render("foo.html", 17) == "[foo.html @ 17]"
+ assert render("bar.html", 23) == "[bar.html @ 23]"
+ """)
+
+ cov = coverage.Coverage()
+ cov.config["run:plugins"] = ["tests.plugin2"]
+ cov.config["run:debug"] = ["trace"]
+
+ self.start_import_stop(cov, "caller")
+
+ print(self.stderr())
+ cov._harvest_data()
+ print(cov.data.line_data())
+
+ return # TODO: finish this test
+
+ _, statements, missing, _ = cov.analysis("simple.py")
+ self.assertEqual(statements, [1,2,3])
+ self.assertEqual(missing, [])
+ _, statements, _, _ = cov.analysis("/src/try_ABC.zz")
+ self.assertEqual(statements, [105, 106, 107, 205, 206, 207])