# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt """Tests for plugins.""" import inspect import io import os.path from xml.etree import ElementTree import pytest import coverage from coverage import env from coverage.control import Plugins from coverage.data import line_counts from coverage.exceptions import CoverageException, CoverageWarning from coverage.misc import import_local_file import coverage.plugin from tests.coveragetest import CoverageTest from tests.helpers import CheckUniqueFilenames class FakeConfig: """A fake config for use in tests.""" def __init__(self, plugin, options): self.plugin = plugin self.options = options self.asked_for = [] def get_plugin_options(self, module): """Just return the options for `module` if this is the right module.""" self.asked_for.append(module) if module == self.plugin: return self.options else: return {} class LoadPluginsTest(CoverageTest): """Test Plugins.load_plugins directly.""" def test_implicit_boolean(self): self.make_file("plugin1.py", """\ from coverage import CoveragePlugin class Plugin(CoveragePlugin): pass def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) config = FakeConfig("plugin1", {}) plugins = Plugins.load_plugins([], config) assert not plugins plugins = Plugins.load_plugins(["plugin1"], config) assert plugins def test_importing_and_configuring(self): self.make_file("plugin1.py", """\ from coverage import CoveragePlugin class Plugin(CoveragePlugin): def __init__(self, options): self.options = options self.this_is = "me" def coverage_init(reg, options): reg.add_file_tracer(Plugin(options)) """) config = FakeConfig("plugin1", {'a': 'hello'}) plugins = list(Plugins.load_plugins(["plugin1"], config)) assert len(plugins) == 1 assert plugins[0].this_is == "me" assert plugins[0].options == {'a': 'hello'} assert config.asked_for == ['plugin1'] def test_importing_and_configuring_more_than_one(self): self.make_file("plugin1.py", """\ from coverage import CoveragePlugin class Plugin(CoveragePlugin): def __init__(self, options): self.options = options self.this_is = "me" def coverage_init(reg, options): reg.add_file_tracer(Plugin(options)) """) self.make_file("plugin2.py", """\ from coverage import CoveragePlugin class Plugin(CoveragePlugin): def __init__(self, options): self.options = options def coverage_init(reg, options): reg.add_file_tracer(Plugin(options)) """) config = FakeConfig("plugin1", {'a': 'hello'}) plugins = list(Plugins.load_plugins(["plugin1", "plugin2"], config)) assert len(plugins) == 2 assert plugins[0].this_is == "me" assert plugins[0].options == {'a': 'hello'} assert plugins[1].options == {} assert config.asked_for == ['plugin1', 'plugin2'] # The order matters... config = FakeConfig("plugin1", {'a': 'second'}) plugins = list(Plugins.load_plugins(["plugin2", "plugin1"], config)) assert len(plugins) == 2 assert plugins[0].options == {} assert plugins[1].this_is == "me" assert plugins[1].options == {'a': 'second'} def test_cant_import(self): with pytest.raises(ImportError, match="No module named '?plugin_not_there'?"): _ = Plugins.load_plugins(["plugin_not_there"], None) def test_plugin_must_define_coverage_init(self): self.make_file("no_plugin.py", """\ from coverage import CoveragePlugin Nothing = 0 """) msg_pat = "Plugin module 'no_plugin' didn't define a coverage_init function" with pytest.raises(CoverageException, match=msg_pat): list(Plugins.load_plugins(["no_plugin"], None)) class PluginTest(CoverageTest): """Test plugins through the Coverage class.""" def test_plugin_imported(self): # Prove that a plugin will be imported. self.make_file("my_plugin.py", """\ from coverage import CoveragePlugin class Plugin(CoveragePlugin): pass def coverage_init(reg, options): reg.add_noop(Plugin()) with open("evidence.out", "w") as f: f.write("we are here!") """) self.assert_doesnt_exist("evidence.out") cov = coverage.Coverage() cov.set_option("run:plugins", ["my_plugin"]) cov.start() cov.stop() # pragma: nested with open("evidence.out") as f: assert f.read() == "we are here!" def test_missing_plugin_raises_import_error(self): # Prove that a missing plugin will raise an ImportError. with pytest.raises(ImportError, match="No module named '?does_not_exist_woijwoicweo'?"): cov = coverage.Coverage() cov.set_option("run:plugins", ["does_not_exist_woijwoicweo"]) cov.start() cov.stop() def test_bad_plugin_isnt_hidden(self): # Prove that a plugin with an error in it will raise the error. self.make_file("plugin_over_zero.py", "1/0") with pytest.raises(ZeroDivisionError): cov = coverage.Coverage() cov.set_option("run:plugins", ["plugin_over_zero"]) cov.start() cov.stop() def test_plugin_sys_info(self): self.make_file("plugin_sys_info.py", """\ import coverage class Plugin(coverage.CoveragePlugin): def sys_info(self): return [("hello", "world")] def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) debug_out = io.StringIO() cov = coverage.Coverage(debug=["sys"]) cov._debug_file = debug_out cov.set_option("run:plugins", ["plugin_sys_info"]) with pytest.warns(None): # Catch warnings so we don't see "plugins aren't supported on PyTracer" cov.start() cov.stop() # pragma: nested out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] if env.C_TRACER: assert 'plugins.file_tracers: plugin_sys_info.Plugin' in out_lines else: assert 'plugins.file_tracers: plugin_sys_info.Plugin (disabled)' in out_lines assert 'plugins.configurers: -none-' in out_lines expected_end = [ "-- sys: plugin_sys_info.Plugin -------------------------------", "hello: world", "-- end -------------------------------------------------------", ] assert expected_end == out_lines[-len(expected_end):] def test_plugin_with_no_sys_info(self): self.make_file("plugin_no_sys_info.py", """\ import coverage class Plugin(coverage.CoveragePlugin): pass def coverage_init(reg, options): reg.add_configurer(Plugin()) """) debug_out = io.StringIO() cov = coverage.Coverage(debug=["sys"]) cov._debug_file = debug_out cov.set_option("run:plugins", ["plugin_no_sys_info"]) cov.start() cov.stop() # pragma: nested out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] assert 'plugins.file_tracers: -none-' in out_lines assert 'plugins.configurers: plugin_no_sys_info.Plugin' in out_lines expected_end = [ "-- sys: plugin_no_sys_info.Plugin ----------------------------", "-- end -------------------------------------------------------", ] assert expected_end == out_lines[-len(expected_end):] def test_local_files_are_importable(self): self.make_file("importing_plugin.py", """\ from coverage import CoveragePlugin import local_module class MyPlugin(CoveragePlugin): pass def coverage_init(reg, options): reg.add_noop(MyPlugin()) """) self.make_file("local_module.py", "CONST = 1") self.make_file(".coveragerc", """\ [run] plugins = importing_plugin """) self.make_file("main_file.py", "print('MAIN')") out = self.run_command("coverage run main_file.py") assert out == "MAIN\n" out = self.run_command("coverage html -q") # sneak in a test of -q assert out == "" @pytest.mark.skipif(env.C_TRACER, reason="This test is only about PyTracer.") class PluginWarningOnPyTracerTest(CoverageTest): """Test that we get a controlled exception with plugins on PyTracer.""" def test_exception_if_plugins_on_pytracer(self): self.make_file("simple.py", "a = 1") cov = coverage.Coverage() cov.set_option("run:plugins", ["tests.plugin1"]) expected_warnings = [ r"Plugin file tracers \(tests.plugin1.Plugin\) aren't supported with PyTracer", ] with self.assert_warnings(cov, expected_warnings): self.start_import_stop(cov, "simple") @pytest.mark.skipif(not env.C_TRACER, reason="Plugins are only supported with the C tracer.") class FileTracerTest(CoverageTest): """Tests of plugins that implement file_tracer.""" class GoodFileTracerTest(FileTracerTest): """Tests of file tracer plugin happy paths.""" def test_plugin1(self): self.make_file("simple.py", """\ import try_xyz a = 1 b = 2 """) self.make_file("try_xyz.py", """\ c = 3 d = 4 """) cov = coverage.Coverage() CheckUniqueFilenames.hook(cov, '_should_trace') CheckUniqueFilenames.hook(cov, '_check_include_omit_etc') cov.set_option("run:plugins", ["tests.plugin1"]) # Import the Python file, executing it. self.start_import_stop(cov, "simple") _, statements, missing, _ = cov.analysis("simple.py") assert statements == [1, 2, 3] assert missing == [] zzfile = os.path.abspath(os.path.join("/src", "try_ABC.zz")) _, statements, _, _ = cov.analysis(zzfile) assert statements == [105, 106, 107, 205, 206, 207] def make_render_and_caller(self): """Make the render.py and caller.py files we need.""" # plugin2 emulates a dynamic tracing plugin: the caller's locals # are examined to determine the source file and line number. # The plugin is in tests/plugin2.py. self.make_file("render.py", """\ def render(filename, linenum): # This function emulates a template renderer. The plugin # will examine the `filename` and `linenum` locals to # determine the source file and line number. fiddle_around = 1 # not used, just chaff. return "[{} @ {}]".format(filename, linenum) def helper(x): # This function is here just to show that not all code in # this file will be part of the dynamic tracing. return x+1 """) self.make_file("caller.py", """\ import sys from render import helper, render assert render("foo_7.html", 4) == "[foo_7.html @ 4]" # Render foo_7.html again to try the CheckUniqueFilenames asserts. render("foo_7.html", 4) assert helper(42) == 43 assert render("bar_4.html", 2) == "[bar_4.html @ 2]" assert helper(76) == 77 # quux_5.html will be omitted from the results. assert render("quux_5.html", 3) == "[quux_5.html @ 3]" # For Python 2, make sure unicode is working. assert render(u"uni_3.html", 2) == "[uni_3.html @ 2]" """) # will try to read the actual source files, so make some # source files. def lines(n): """Make a string with n lines of text.""" return "".join("line %d\n" % i for i in range(n)) self.make_file("bar_4.html", lines(4)) self.make_file("foo_7.html", lines(7)) def test_plugin2(self): self.make_render_and_caller() cov = coverage.Coverage(omit=["*quux*"]) CheckUniqueFilenames.hook(cov, '_should_trace') CheckUniqueFilenames.hook(cov, '_check_include_omit_etc') cov.set_option("run:plugins", ["tests.plugin2"]) self.start_import_stop(cov, "caller") # The way plugin2 works, a file named foo_7.html will be claimed to # have 7 lines in it. If render() was called with line number 4, # then the plugin will claim that lines 4 and 5 were executed. _, statements, missing, _ = cov.analysis("foo_7.html") assert statements == [1, 2, 3, 4, 5, 6, 7] assert missing == [1, 2, 3, 6, 7] assert "foo_7.html" in line_counts(cov.get_data()) _, statements, missing, _ = cov.analysis("bar_4.html") assert statements == [1, 2, 3, 4] assert missing == [1, 4] assert "bar_4.html" in line_counts(cov.get_data()) assert "quux_5.html" not in line_counts(cov.get_data()) _, statements, missing, _ = cov.analysis("uni_3.html") assert statements == [1, 2, 3] assert missing == [1] assert "uni_3.html" in line_counts(cov.get_data()) def test_plugin2_with_branch(self): self.make_render_and_caller() cov = coverage.Coverage(branch=True, omit=["*quux*"]) CheckUniqueFilenames.hook(cov, '_should_trace') CheckUniqueFilenames.hook(cov, '_check_include_omit_etc') cov.set_option("run:plugins", ["tests.plugin2"]) self.start_import_stop(cov, "caller") # The way plugin2 works, a file named foo_7.html will be claimed to # have 7 lines in it. If render() was called with line number 4, # then the plugin will claim that lines 4 and 5 were executed. analysis = cov._analyze("foo_7.html") assert analysis.statements == {1, 2, 3, 4, 5, 6, 7} # Plugins don't do branch coverage yet. assert analysis.has_arcs() is True assert analysis.arc_possibilities() == [] assert analysis.missing == {1, 2, 3, 6, 7} def test_plugin2_with_text_report(self): self.make_render_and_caller() cov = coverage.Coverage(branch=True, omit=["*quux*"]) cov.set_option("run:plugins", ["tests.plugin2"]) self.start_import_stop(cov, "caller") repout = io.StringIO() total = cov.report(file=repout, include=["*.html"], omit=["uni*.html"], show_missing=True) report = repout.getvalue().splitlines() expected = [ 'Name Stmts Miss Branch BrPart Cover Missing', '--------------------------------------------------------', 'bar_4.html 4 2 0 0 50% 1, 4', 'foo_7.html 7 5 0 0 29% 1-3, 6-7', '--------------------------------------------------------', 'TOTAL 11 7 0 0 36%', ] assert expected == report assert round(abs(total-36.36), 2) == 0 def test_plugin2_with_html_report(self): self.make_render_and_caller() cov = coverage.Coverage(branch=True, omit=["*quux*"]) cov.set_option("run:plugins", ["tests.plugin2"]) self.start_import_stop(cov, "caller") total = cov.html_report(include=["*.html"], omit=["uni*.html"]) assert round(abs(total-36.36), 2) == 0 self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/bar_4_html.html") self.assert_exists("htmlcov/foo_7_html.html") def test_plugin2_with_xml_report(self): self.make_render_and_caller() cov = coverage.Coverage(branch=True, omit=["*quux*"]) cov.set_option("run:plugins", ["tests.plugin2"]) self.start_import_stop(cov, "caller") total = cov.xml_report(include=["*.html"], omit=["uni*.html"]) assert round(abs(total-36.36), 2) == 0 dom = ElementTree.parse("coverage.xml") classes = {} for elt in dom.findall(".//class"): classes[elt.get('name')] = elt assert classes['bar_4.html'].attrib == { 'branch-rate': '1', 'complexity': '0', 'filename': 'bar_4.html', 'line-rate': '0.5', 'name': 'bar_4.html', } assert classes['foo_7.html'].attrib == { 'branch-rate': '1', 'complexity': '0', 'filename': 'foo_7.html', 'line-rate': '0.2857', 'name': 'foo_7.html', } def test_defer_to_python(self): # A plugin that measures, but then wants built-in python reporting. self.make_file("fairly_odd_plugin.py", """\ # A plugin that claims all the odd lines are executed, and none of # the even lines, and then punts reporting off to the built-in # Python reporting. import coverage.plugin class Plugin(coverage.CoveragePlugin): def file_tracer(self, filename): return OddTracer(filename) def file_reporter(self, filename): return "python" class OddTracer(coverage.plugin.FileTracer): def __init__(self, filename): self.filename = filename def source_filename(self): return self.filename def line_number_range(self, frame): lineno = frame.f_lineno if lineno % 2: return (lineno, lineno) else: return (-1, -1) def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.make_file("unsuspecting.py", """\ a = 1 b = 2 c = 3 d = 4 e = 5 f = 6 """) cov = coverage.Coverage(include=["unsuspecting.py"]) cov.set_option("run:plugins", ["fairly_odd_plugin"]) self.start_import_stop(cov, "unsuspecting") repout = io.StringIO() total = cov.report(file=repout, show_missing=True) report = repout.getvalue().splitlines() expected = [ 'Name Stmts Miss Cover Missing', '-----------------------------------------------', 'unsuspecting.py 6 3 50% 2, 4, 6', '-----------------------------------------------', 'TOTAL 6 3 50%', ] assert expected == report assert total == 50 def test_find_unexecuted(self): self.make_file("unexecuted_plugin.py", """\ import os import coverage.plugin class Plugin(coverage.CoveragePlugin): def file_tracer(self, filename): if filename.endswith("foo.py"): return MyTracer(filename) def file_reporter(self, filename): return MyReporter(filename) def find_executable_files(self, src_dir): # Check that src_dir is the right value files = os.listdir(src_dir) assert "foo.py" in files assert "unexecuted_plugin.py" in files return ["chimera.py"] class MyTracer(coverage.plugin.FileTracer): def __init__(self, filename): self.filename = filename def source_filename(self): return self.filename def line_number_range(self, frame): return (999, 999) class MyReporter(coverage.FileReporter): def lines(self): return {99, 999, 9999} def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.make_file("foo.py", "a = 1") cov = coverage.Coverage(source=['.']) cov.set_option("run:plugins", ["unexecuted_plugin"]) self.start_import_stop(cov, "foo") # The file we executed claims to have run line 999. _, statements, missing, _ = cov.analysis("foo.py") assert statements == [99, 999, 9999] assert missing == [99, 9999] # The completely missing file is in the results. _, statements, missing, _ = cov.analysis("chimera.py") assert statements == [99, 999, 9999] assert missing == [99, 999, 9999] # But completely new filenames are not in the results. assert len(cov.get_data().measured_files()) == 3 with pytest.raises(CoverageException): cov.analysis("fictional.py") class BadFileTracerTest(FileTracerTest): """Test error handling around file tracer plugins.""" def run_plugin(self, module_name): """Run a plugin with the given module_name. Uses a few fixed Python files. Returns the Coverage object. """ self.make_file("simple.py", """\ import other, another a = other.f(2) b = other.f(3) c = another.g(4) d = another.g(5) """) # The names of these files are important: some plugins apply themselves # to "*other.py". self.make_file("other.py", """\ def f(x): return x+1 """) self.make_file("another.py", """\ def g(x): return x-1 """) 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): """Run a file, and see that the plugin failed. `module_name` and `plugin_name` is the module and name of the plugin to use. `our_error` is True if the error reported to the user will be an explicit error in our test code, marked with an '# Oh noes!' comment. `excmsg`, if provided, is text that must appear in the stderr. `excmsgs`, if provided, is a list of messages, one of which must appear in the stderr. The plugin will be disabled, and we check that a warning is output explaining why. """ with pytest.warns(Warning) as warns: self.run_plugin(module_name) stderr = self.stderr() stderr += "".join(w.message.args[0] for w in warns) if our_error: # The exception we're causing should only appear once. assert stderr.count("# Oh noes!") == 1 # There should be a warning explaining what's happening, but only one. # The message can be in two forms: # Disabling plug-in '...' due to previous exception # or: # Disabling plug-in '...' due to an exception: print([str(w) for w in warns.list]) warns = [w for w in warns.list if issubclass(w.category, CoverageWarning)] assert len(warns) == 1 warnmsg = warns[0].message.args[0] assert f"Disabling plug-in '{module_name}.{plugin_name}' due to " in warnmsg if excmsg: assert excmsg in stderr if excmsgs: found_exc = any(em in stderr for em in excmsgs) # pragma: part covered assert found_exc, f"expected one of {excmsgs} in stderr" def test_file_tracer_has_no_file_tracer_method(self): self.make_file("bad_plugin.py", """\ class Plugin(object): pass def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) def test_file_tracer_has_inherited_sourcefilename_method(self): self.make_file("bad_plugin.py", """\ import coverage class Plugin(coverage.CoveragePlugin): def file_tracer(self, filename): # Just grab everything. return FileTracer() class FileTracer(coverage.FileTracer): pass def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin( "bad_plugin", "Plugin", our_error=False, excmsg="Class 'bad_plugin.FileTracer' needs to implement source_filename()", ) def test_plugin_has_inherited_filereporter_method(self): self.make_file("bad_plugin.py", """\ import coverage class Plugin(coverage.CoveragePlugin): def file_tracer(self, filename): # Just grab everything. return FileTracer() class FileTracer(coverage.FileTracer): def source_filename(self): return "foo.xxx" def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) cov = self.run_plugin("bad_plugin") expected_msg = "Plugin 'bad_plugin.Plugin' needs to implement file_reporter()" with pytest.raises(NotImplementedError, match=expected_msg): cov.report() 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! def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) 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 class Plugin(coverage.plugin.CoveragePlugin): def file_tracer(self, filename): return 3.14159 def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin( "bad_plugin", "Plugin", our_error=False, excmsg="'float' object has no attribute", ) 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! def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin("bad_plugin", "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! def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin("bad_plugin", "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 def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin( "bad_plugin", "Plugin", our_error=False, excmsgs=[ "expected str, bytes or os.PathLike object, not float", "'float' object has no attribute", "object of type 'float' has no len()", "'float' object is unsubscriptable", ], ) def test_dynamic_source_filename_fails(self): self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): def file_tracer(self, filename): if filename.endswith("other.py"): return BadFileTracer() class BadFileTracer(coverage.plugin.FileTracer): def has_dynamic_source_filename(self): return True def dynamic_source_filename(self, filename, frame): 101/0 # Oh noes! def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin("bad_plugin", "Plugin") def test_line_number_range_raises_error(self): self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): def file_tracer(self, filename): if filename.endswith("other.py"): return BadFileTracer() class BadFileTracer(coverage.plugin.FileTracer): def source_filename(self): return "something.foo" def line_number_range(self, frame): raise Exception("borked!") def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin( "bad_plugin", "Plugin", our_error=False, excmsg="borked!", ) def test_line_number_range_returns_non_tuple(self): self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): def file_tracer(self, filename): if filename.endswith("other.py"): return BadFileTracer() class BadFileTracer(coverage.plugin.FileTracer): def source_filename(self): return "something.foo" def line_number_range(self, frame): return 42.23 def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin( "bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple", ) def test_line_number_range_returns_triple(self): self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): def file_tracer(self, filename): if filename.endswith("other.py"): return BadFileTracer() class BadFileTracer(coverage.plugin.FileTracer): def source_filename(self): return "something.foo" def line_number_range(self, frame): return (1, 2, 3) def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin( "bad_plugin", "Plugin", our_error=False, excmsg="line_number_range must return 2-tuple", ) def test_line_number_range_returns_pair_of_strings(self): self.make_file("bad_plugin.py", """\ import coverage.plugin class Plugin(coverage.plugin.CoveragePlugin): def file_tracer(self, filename): if filename.endswith("other.py"): return BadFileTracer() class BadFileTracer(coverage.plugin.FileTracer): def source_filename(self): return "something.foo" def line_number_range(self, frame): return ("5", "7") def coverage_init(reg, options): reg.add_file_tracer(Plugin()) """) self.run_bad_plugin( "bad_plugin", "Plugin", our_error=False, excmsgs=[ "an integer is required", "cannot be interpreted as an integer", ], ) class ConfigurerPluginTest(CoverageTest): """Test configuring plugins.""" run_in_temp_dir = False def test_configurer_plugin(self): cov = coverage.Coverage() cov.set_option("run:plugins", ["tests.plugin_config"]) cov.start() cov.stop() # pragma: nested excluded = cov.get_option("report:exclude_lines") assert "pragma: custom" in excluded assert "pragma: or whatever" in excluded class DynamicContextPluginTest(CoverageTest): """Tests of plugins that implement `dynamic_context`.""" def make_plugin_capitalized_testnames(self, filename): """Create a dynamic context plugin that capitalizes the part after 'test_'.""" self.make_file(filename, """\ from coverage import CoveragePlugin class Plugin(CoveragePlugin): def dynamic_context(self, frame): name = frame.f_code.co_name if name.startswith(("test_", "doctest_")): parts = name.split("_", 1) return "%s:%s" % (parts[0], parts[1].upper()) return None def coverage_init(reg, options): reg.add_dynamic_context(Plugin()) """) def make_plugin_track_render(self, filename): """Make a dynamic context plugin that tracks 'render_' functions.""" self.make_file(filename, """\ from coverage import CoveragePlugin class Plugin(CoveragePlugin): def dynamic_context(self, frame): name = frame.f_code.co_name if name.startswith("render_"): return 'renderer:' + name[7:] return None def coverage_init(reg, options): reg.add_dynamic_context(Plugin()) """) def make_test_files(self): """Make some files to use while testing dynamic context plugins.""" self.make_file("rendering.py", """\ def html_tag(tag, content): return '<%s>%s' % (tag, content, tag) def render_paragraph(text): return html_tag('p', text) def render_span(text): return html_tag('span', text) def render_bold(text): return html_tag('b', text) """) self.make_file("testsuite.py", """\ import rendering def test_html_tag(): assert rendering.html_tag('b', 'hello') == 'hello' def doctest_html_tag(): assert eval(''' rendering.html_tag('i', 'text') == 'text' '''.strip()) def test_renderers(): assert rendering.render_paragraph('hello') == '

hello

' assert rendering.render_bold('wide') == 'wide' assert rendering.render_span('world') == 'world' def build_full_html(): html = '%s' % ( rendering.render_paragraph( rendering.render_span('hello'))) return html """) def run_all_functions(self, cov, suite_name): # pragma: nested """Run all functions in `suite_name` under coverage.""" cov.start() suite = import_local_file(suite_name) try: # Call all functions in this module for name in dir(suite): variable = getattr(suite, name) if inspect.isfunction(variable): variable() finally: cov.stop() def test_plugin_standalone(self): self.make_plugin_capitalized_testnames('plugin_tests.py') self.make_test_files() # Enable dynamic context plugin cov = coverage.Coverage() cov.set_option("run:plugins", ['plugin_tests']) # Run the tests self.run_all_functions(cov, 'testsuite') # Labeled coverage is collected data = cov.get_data() filenames = self.get_measured_filenames(data) expected = ['', 'doctest:HTML_TAG', 'test:HTML_TAG', 'test:RENDERERS'] assert expected == sorted(data.measured_contexts()) data.set_query_context("doctest:HTML_TAG") assert [2] == data.lines(filenames['rendering.py']) data.set_query_context("test:HTML_TAG") assert [2] == data.lines(filenames['rendering.py']) data.set_query_context("test:RENDERERS") assert [2, 5, 8, 11] == sorted(data.lines(filenames['rendering.py'])) def test_static_context(self): self.make_plugin_capitalized_testnames('plugin_tests.py') self.make_test_files() # Enable dynamic context plugin for coverage with named context cov = coverage.Coverage(context='mytests') cov.set_option("run:plugins", ['plugin_tests']) # Run the tests self.run_all_functions(cov, 'testsuite') # Static context prefix is preserved data = cov.get_data() expected = [ 'mytests', 'mytests|doctest:HTML_TAG', 'mytests|test:HTML_TAG', 'mytests|test:RENDERERS', ] assert expected == sorted(data.measured_contexts()) def test_plugin_with_test_function(self): self.make_plugin_capitalized_testnames('plugin_tests.py') self.make_test_files() # Enable both a plugin and test_function dynamic context cov = coverage.Coverage() cov.set_option("run:plugins", ['plugin_tests']) cov.set_option("run:dynamic_context", "test_function") # Run the tests self.run_all_functions(cov, 'testsuite') # test_function takes precedence over plugins - only # functions that are not labeled by test_function are # labeled by plugin_tests. data = cov.get_data() filenames = self.get_measured_filenames(data) expected = [ '', 'doctest:HTML_TAG', 'testsuite.test_html_tag', 'testsuite.test_renderers', ] assert expected == sorted(data.measured_contexts()) def assert_context_lines(context, lines): data.set_query_context(context) assert lines == sorted(data.lines(filenames['rendering.py'])) assert_context_lines("doctest:HTML_TAG", [2]) assert_context_lines("testsuite.test_html_tag", [2]) assert_context_lines("testsuite.test_renderers", [2, 5, 8, 11]) def test_multiple_plugins(self): self.make_plugin_capitalized_testnames('plugin_tests.py') self.make_plugin_track_render('plugin_renderers.py') self.make_test_files() # Enable two plugins cov = coverage.Coverage() cov.set_option("run:plugins", ['plugin_renderers', 'plugin_tests']) self.run_all_functions(cov, 'testsuite') # It is important to note, that line 11 (render_bold function) is never # labeled as renderer:bold context, because it is only called from # test_renderers function - so it already falls under test:RENDERERS # context. # # render_paragraph and render_span (lines 5, 8) are directly called by # testsuite.build_full_html, so they get labeled by renderers plugin. data = cov.get_data() filenames = self.get_measured_filenames(data) expected = [ '', 'doctest:HTML_TAG', 'renderer:paragraph', 'renderer:span', 'test:HTML_TAG', 'test:RENDERERS', ] assert expected == sorted(data.measured_contexts()) def assert_context_lines(context, lines): data.set_query_context(context) assert lines == sorted(data.lines(filenames['rendering.py'])) assert_context_lines("test:HTML_TAG", [2]) assert_context_lines("test:RENDERERS", [2, 5, 8, 11]) assert_context_lines("doctest:HTML_TAG", [2]) assert_context_lines("renderer:paragraph", [2, 5]) assert_context_lines("renderer:span", [2, 8])