summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--coverage/control.py54
-rw-r--r--coverage/misc.py4
-rw-r--r--coverage/plugin.py2
-rw-r--r--coverage/tracer.c10
-rw-r--r--tests/test_plugins.py109
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")