diff options
-rw-r--r-- | TODO.txt | 8 | ||||
-rw-r--r-- | coverage/collector.py | 2 | ||||
-rw-r--r-- | coverage/control.py | 25 | ||||
-rw-r--r-- | coverage/plugin.py | 52 | ||||
-rw-r--r-- | coverage/pytracer.py | 13 | ||||
-rw-r--r-- | tests/plugin1.py | 5 | ||||
-rw-r--r-- | tests/test_plugins.py | 37 |
7 files changed, 113 insertions, 29 deletions
@@ -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]) |