From 039ef0959c3f21fe0991204c19fb99fab14055f5 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Sun, 13 Sep 2020 15:47:15 -0400 Subject: [PATCH] If a plugin is disabled, don't try to record its file tracers. #1011 --- CHANGES.rst | 5 +++++ coverage/collector.py | 13 ++++++++++++- coverage/control.py | 4 ++++ tests/test_plugins.py | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index 604763f74..29d7fbe3b 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -31,7 +31,12 @@ Unreleased to name a package to disambiguate this case. Thanks, Thomas Grainger. Fixes `issue 268`_. +- If a plugin was disabled due to an exception, we used to still try to record + its information, causing an exception, as reported in `issue 1011`_. This is + now fixed. + .. _issue 268: https://github.com/nedbat/coveragepy/issues/268 +.. _issue 1011: https://github.com/nedbat/coveragepy/issues/1011 .. _changes_521: diff --git a/coverage/collector.py b/coverage/collector.py index 3545ab1e2..9333d66a8 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -196,6 +196,8 @@ def reset(self): # handle them. self.file_tracers = {} + self.disabled_plugins = set() + # The .should_trace_cache attribute is a cache from file names to # coverage.FileDisposition objects, or None. When a file is first # considered for tracing, a FileDisposition is obtained from @@ -419,6 +421,10 @@ def mapped_file_dict(self, d): return dict((self.cached_mapped_file(k), v) for k, v in items if v) + def plugin_was_disabled(self, plugin): + """Record that `plugin` was disabled during the run.""" + self.disabled_plugins.add(plugin._coverage_plugin_name) + def flush_data(self): """Save the collected data to our associated `CoverageData`. @@ -434,7 +440,12 @@ def flush_data(self): self.covdata.add_arcs(self.mapped_file_dict(self.data)) else: self.covdata.add_lines(self.mapped_file_dict(self.data)) - self.covdata.add_file_tracers(self.mapped_file_dict(self.file_tracers)) + + file_tracers = { + k: v for k, v in self.file_tracers.items() + if v not in self.disabled_plugins + } + self.covdata.add_file_tracers(self.mapped_file_dict(file_tracers)) self._clear_data() return True diff --git a/coverage/control.py b/coverage/control.py index 7c4a48280..2d75417e2 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -709,6 +709,10 @@ def get_data(self): self._init_data(suffix=None) self._post_init() + for plugin in self._plugins: + if not plugin._coverage_enabled: + self._collector.plugin_was_disabled(plugin) + if self._collector and self._collector.flush_data(): self._post_save_work() diff --git a/tests/test_plugins.py b/tests/test_plugins.py index ed58c5f41..6340f9c33 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -611,6 +611,7 @@ def g(x): cov = coverage.Coverage() cov.set_option("run:plugins", [module_name]) self.start_import_stop(cov, "simple") + cov.save() # pytest-cov does a save after stop, so we'll do it too. return cov def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None, excmsgs=None): @@ -715,6 +716,37 @@ def coverage_init(reg, options): """) self.run_bad_plugin("bad_plugin", "Plugin") + def test_file_tracer_fails_eventually(self): + # Django coverage plugin can report on a few files and then fail. + # https://github.com/nedbat/coveragepy/issues/1011 + self.make_file("bad_plugin.py", """\ + import os.path + import coverage.plugin + class Plugin(coverage.plugin.CoveragePlugin): + def __init__(self): + self.calls = 0 + + def file_tracer(self, filename): + print(filename) + self.calls += 1 + if self.calls <= 2: + return FileTracer(filename) + else: + 17/0 # Oh noes! + + class FileTracer(coverage.FileTracer): + def __init__(self, filename): + self.filename = filename + def source_filename(self): + return os.path.basename(self.filename).replace(".py", ".foo") + def line_number_range(self, frame): + return -1, -1 + + def coverage_init(reg, options): + reg.add_file_tracer(Plugin()) + """) + self.run_bad_plugin("bad_plugin", "Plugin") + def test_file_tracer_returns_wrong(self): self.make_file("bad_plugin.py", """\ import coverage.plugin