diff options
-rw-r--r-- | coverage/control.py | 54 | ||||
-rw-r--r-- | coverage/misc.py | 4 | ||||
-rw-r--r-- | coverage/plugin.py | 2 | ||||
-rw-r--r-- | coverage/tracer.c | 10 | ||||
-rw-r--r-- | tests/test_plugins.py | 109 |
5 files changed, 156 insertions, 23 deletions
diff --git a/coverage/control.py b/coverage/control.py index 65830ee7..0bbe301c 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -7,6 +7,7 @@ import platform import random import socket import sys +import traceback from coverage import env from coverage.annotate import AnnotateReporter @@ -244,7 +245,9 @@ class Coverage(object): if self.file_tracers and not self.collector.supports_plugins: raise CoverageException( "Plugin file tracers (%s) aren't supported with %s" % ( - ", ".join(ft.plugin_name for ft in self.file_tracers), + ", ".join( + ft._coverage_plugin_name for ft in self.file_tracers + ), self.collector.tracer_name(), ) ) @@ -328,7 +331,7 @@ class Coverage(object): if self.debug.should('sys'): self.debug.write_formatted_info("sys", self.sys_info()) for plugin in self.plugins: - header = "sys: " + plugin.plugin_name + header = "sys: " + plugin._coverage_plugin_name info = plugin.sys_info() self.debug.write_formatted_info(header, info) wrote_any = True @@ -460,19 +463,33 @@ class Coverage(object): # Try the plugins, see if they have an opinion about the file. plugin = None for plugin in self.file_tracers: - file_tracer = plugin.file_tracer(canonical) - if file_tracer is not None: - file_tracer.plugin_name = plugin.plugin_name - disp.trace = True - disp.file_tracer = file_tracer - 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() - ) - break + if not plugin._coverage_enabled: + continue + + try: + file_tracer = plugin.file_tracer(canonical) + if file_tracer is not None: + file_tracer._coverage_plugin_name = \ + plugin._coverage_plugin_name + disp.trace = True + disp.file_tracer = file_tracer + 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() + ) + break + except Exception as e: + self._warn( + "Disabling plugin %r due to an exception:" % ( + plugin._coverage_plugin_name + ) + ) + traceback.print_exc() + plugin._coverage_enabled = False + continue else: # No plugin wanted it: it's Python. disp.trace = True @@ -827,7 +844,7 @@ class Coverage(object): if file_reporter is None: raise CoverageException( "Plugin %r did not provide a file reporter for %r." % ( - plugin.plugin_name, morf + plugin._coverage_plugin_name, morf ) ) else: @@ -999,7 +1016,7 @@ class Coverage(object): ('pylib_dirs', self.pylib_dirs), ('tracer', self.collector.tracer_name()), ('file_tracers', [ - ft.plugin_name for ft in self.file_tracers + ft._coverage_plugin_name for ft in self.file_tracers ]), ('config_files', self.config.attempted_config_files), ('configs_read', self.config.config_files), @@ -1142,7 +1159,8 @@ class Plugins(object): if plugin_class: options = config.get_plugin_options(module) plugin = plugin_class(options) - plugin.plugin_name = module + plugin._coverage_plugin_name = module + plugin._coverage_enabled = True plugins.order.append(plugin) plugins.names[module] = plugin else: diff --git a/coverage/misc.py b/coverage/misc.py index 17fb3df9..d5197ea3 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -165,9 +165,9 @@ def overrides(obj, method_name, base_class): # TODO: abc? def _needs_to_implement(that, func_name): """Helper to raise NotImplementedError in interface stubs.""" - if hasattr(that, "plugin_name"): + if hasattr(that, "_coverage_plugin_name"): thing = "Plugin" - name = that.plugin_name + name = that._coverage_plugin_name else: thing = "Class" klass = that.__class__ diff --git a/coverage/plugin.py b/coverage/plugin.py index 1389180b..6648d7a6 100644 --- a/coverage/plugin.py +++ b/coverage/plugin.py @@ -5,6 +5,8 @@ import re from coverage.misc import _needs_to_implement +# TODO: document that the plugin objects may be decorated with attributes with +# named "_coverage_*". class CoveragePlugin(object): """Base class for coverage.py plugins. diff --git a/coverage/tracer.c b/coverage/tracer.c index 102c474e..a31340fc 100644 --- a/coverage/tracer.c +++ b/coverage/tracer.c @@ -530,6 +530,9 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse disp_file_tracer, "dynamic_source_filename", "OO", tracename, frame ); + if (next_tracename == NULL) { + goto error; + } Py_DECREF(tracename); tracename = next_tracename; @@ -577,7 +580,7 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse /* If the disposition mentions a plugin, record that. */ if (disp_file_tracer != Py_None) { - disp_plugin_name = PyObject_GetAttrString(disp_file_tracer, "plugin_name"); + disp_plugin_name = PyObject_GetAttrString(disp_file_tracer, "_coverage_plugin_name"); if (disp_plugin_name == NULL) { goto error; } @@ -702,12 +705,13 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse } ret = RET_OK; - goto ok; + goto cleanup; error: STATS( self->stats.errors++; ) -ok: +cleanup: + Py_XDECREF(tracename); Py_XDECREF(disposition); Py_XDECREF(disp_trace); diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 9cbe23b9..95c5e3d3 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -247,6 +247,10 @@ class FileTracerTest(CoverageTest): if not env.C_TRACER: self.skip("Plugins are only supported with the C tracer.") + +class GoodPluginTest(FileTracerTest): + """Tests of plugin happy paths.""" + def test_plugin1(self): self.make_file("simple.py", """\ import try_xyz @@ -336,3 +340,108 @@ class FileTracerTest(CoverageTest): self.assertEqual(statements, [1, 2, 3]) self.assertEqual(missing, [1]) self.assertIn("unicode_3.html", cov.data.summary()) + + +class BadPluginTest(FileTracerTest): + """Test error handling around plugins.""" + + def run_bad_plugin(self, plugin_name, our_error=True): + """Run a file, and see that the plugin failed.""" + self.make_file("simple.py", """\ + import other + a = 2 + b = 3 + """) + self.make_file("other.py", """\ + x = 1 + y = 2 + """) + + cov = coverage.Coverage() + cov.config["run:plugins"] = [plugin_name] + self.start_import_stop(cov, "simple") + + stderr = self.stderr() + print(stderr) # for diagnosing test failures. + + if our_error: + errors = stderr.count("# Oh noes!") + # The exception we're causing should only appear once. + self.assertEqual(errors, 1) + + # There should be a warning explaining what's happening, but only one. + msg = "Disabling plugin '%s' due to an exception:" % plugin_name + tracebacks = stderr.count(msg) + self.assertEqual(tracebacks, 1) + + def test_file_tracer_fails(self): + self.make_file("bad_plugin.py", """\ + import coverage.plugin + class Plugin(coverage.plugin.CoveragePlugin): + def file_tracer(self, filename): + 17/0 # Oh noes! + """) + self.run_bad_plugin("bad_plugin") + + def test_file_tracer_returns_wrong(self): + self.make_file("bad_plugin.py", """\ + import coverage.plugin + class Plugin(coverage.plugin.CoveragePlugin): + def file_tracer(self, filename): + return 3.14159 + """) + self.run_bad_plugin("bad_plugin", our_error=False) + + def test_has_dynamic_source_filename_fails(self): + self.make_file("bad_plugin.py", """\ + import coverage.plugin + class Plugin(coverage.plugin.CoveragePlugin): + def file_tracer(self, filename): + return BadFileTracer() + + class BadFileTracer(coverage.plugin.FileTracer): + def has_dynamic_source_filename(self): + 23/0 # Oh noes! + """) + self.run_bad_plugin("bad_plugin") + + def test_source_filename_fails(self): + self.make_file("bad_plugin.py", """\ + import coverage.plugin + class Plugin(coverage.plugin.CoveragePlugin): + def file_tracer(self, filename): + return BadFileTracer() + + class BadFileTracer(coverage.plugin.FileTracer): + def source_filename(self): + 42/0 # Oh noes! + """) + self.run_bad_plugin("bad_plugin") + + def test_source_filename_returns_wrong(self): + self.make_file("bad_plugin.py", """\ + import coverage.plugin + class Plugin(coverage.plugin.CoveragePlugin): + def file_tracer(self, filename): + return BadFileTracer() + + class BadFileTracer(coverage.plugin.FileTracer): + def source_filename(self): + return 17.3 + """) + self.run_bad_plugin("bad_plugin", our_error=False) + + def xxx_dynamic_source_filename_fails(self): + self.make_file("bad_plugin.py", """\ + import coverage.plugin + class Plugin(coverage.plugin.CoveragePlugin): + def file_tracer(self, filename): + return BadFileTracer() + + class BadFileTracer(coverage.plugin.FileTracer): + def has_dynamic_source_filename(self): + return True + def dynamic_source_filename(self, filename, frame): + 101/0 + """) + self.run_bad_plugin("bad_plugin") |