Coverage for tests / test_plugins.py: 100.000%
416 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-30 17:57 +0000
1# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
2# For details: https://github.com/coveragepy/coveragepy/blob/main/NOTICE.txt
4"""Tests for plugins."""
6from __future__ import annotations 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
8import inspect 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
9import io 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
10import math 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
11import os.path 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
13from typing import Any, Iterable 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
14from xml.etree import ElementTree 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
16import pytest 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
18import coverage 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
19from coverage import Coverage 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
20from coverage.plugin_support import Plugins 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
21from coverage.data import line_counts, sorted_lines 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
22from coverage.exceptions import CoverageWarning, NoSource, PluginError 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
23from coverage.misc import import_local_file 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
24from coverage.types import TConfigSectionOut, TLineNo, TPluginConfig 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
26import coverage.plugin 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
28from tests import testenv 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
29from tests.coveragetest import CoverageTest 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
30from tests.helpers import CheckUniqueFilenames, swallow_warnings 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
33class NullConfig(TPluginConfig): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
34 """A plugin configure thing when we don't really need one."""
36 def get_plugin_options(self, plugin: str) -> TConfigSectionOut: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
37 return {} # pragma: never called
40class FakeConfig(TPluginConfig): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
41 """A fake config for use in tests."""
43 def __init__(self, plugin: str, options: dict[str, Any]) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
44 self.plugin = plugin 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
45 self.options = options 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
46 self.asked_for: list[str] = [] 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
48 def get_plugin_options(self, plugin: str) -> TConfigSectionOut: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
49 """Just return the options for `plugin` if this is the right module."""
50 self.asked_for.append(plugin) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
51 if plugin == self.plugin: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
52 return self.options 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
53 else:
54 return {} 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
57def make_plugins( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
58 modules: Iterable[str],
59 config: TPluginConfig,
60) -> Plugins:
61 """Construct a Plugins and call plugins.load_from_config() for convenience."""
62 plugins = Plugins() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
63 plugins.load_from_config(modules, config) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
64 return plugins 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
67class LoadPluginsTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
68 """Test Plugins construction."""
70 def test_implicit_boolean(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
71 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
72 "plugin1.py",
73 """\
74 from coverage import CoveragePlugin
76 class Plugin(CoveragePlugin):
77 pass
79 def coverage_init(reg, options):
80 reg.add_file_tracer(Plugin())
81 """,
82 )
84 config = FakeConfig("plugin1", {}) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
85 plugins = make_plugins([], config) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
86 assert not plugins 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
88 plugins = make_plugins(["plugin1"], config) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
89 assert plugins 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
91 def test_importing_and_configuring(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
92 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
93 "plugin1.py",
94 """\
95 from coverage import CoveragePlugin
97 class Plugin(CoveragePlugin):
98 def __init__(self, options):
99 self.options = options
100 self.this_is = "me"
102 def coverage_init(reg, options):
103 reg.add_file_tracer(Plugin(options))
104 """,
105 )
107 config = FakeConfig("plugin1", {"a": "hello"}) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
108 plugins = list(make_plugins(["plugin1"], config)) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
110 assert len(plugins) == 1 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
111 assert plugins[0].this_is == "me" # type: ignore 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
112 assert plugins[0].options == {"a": "hello"} # type: ignore 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
113 assert config.asked_for == ["plugin1"] 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
115 def test_importing_and_configuring_more_than_one(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
116 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
117 "plugin1.py",
118 """\
119 from coverage import CoveragePlugin
121 class Plugin(CoveragePlugin):
122 def __init__(self, options):
123 self.options = options
124 self.this_is = "me"
126 def coverage_init(reg, options):
127 reg.add_file_tracer(Plugin(options))
128 """,
129 )
130 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
131 "plugin2.py",
132 """\
133 from coverage import CoveragePlugin
135 class Plugin(CoveragePlugin):
136 def __init__(self, options):
137 self.options = options
139 def coverage_init(reg, options):
140 reg.add_file_tracer(Plugin(options))
141 """,
142 )
144 config = FakeConfig("plugin1", {"a": "hello"}) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
145 plugins = list(make_plugins(["plugin1", "plugin2"], config)) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
147 assert len(plugins) == 2 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
148 assert plugins[0].this_is == "me" # type: ignore 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
149 assert plugins[0].options == {"a": "hello"} # type: ignore 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
150 assert plugins[1].options == {} # type: ignore 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
151 assert config.asked_for == ["plugin1", "plugin2"] 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
153 # The order matters...
154 config = FakeConfig("plugin1", {"a": "second"}) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
155 plugins = list(make_plugins(["plugin2", "plugin1"], config)) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
157 assert len(plugins) == 2 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
158 assert plugins[0].options == {} # type: ignore 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
159 assert plugins[1].this_is == "me" # type: ignore 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
160 assert plugins[1].options == {"a": "second"} # type: ignore 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
162 def test_cant_import(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
163 with pytest.raises(ImportError, match="No module named '?plugin_not_there'?"): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
164 _ = make_plugins(["plugin_not_there"], NullConfig()) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
166 def test_plugin_must_define_coverage_init(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
167 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
168 "no_plugin.py",
169 """\
170 from coverage import CoveragePlugin
171 Nothing = 0
172 """,
173 )
174 msg_pat = "Plugin module 'no_plugin' didn't define a coverage_init function" 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
175 with pytest.raises(PluginError, match=msg_pat): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
176 list(make_plugins(["no_plugin"], NullConfig())) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
179class PluginTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
180 """Test plugins through the Coverage class."""
182 def test_plugin_imported(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
183 # Prove that a plugin will be imported.
184 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
185 "my_plugin.py",
186 """\
187 from coverage import CoveragePlugin
188 class Plugin(CoveragePlugin):
189 pass
190 def coverage_init(reg, options):
191 reg.add_noop(Plugin())
192 with open("evidence.out", "w", encoding="utf-8") as f:
193 f.write("we are here!")
194 """,
195 )
197 self.assert_doesnt_exist("evidence.out") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
198 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
199 cov.set_option("run:plugins", ["my_plugin"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
200 cov.start() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
201 cov.stop() # pragma: nested 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
203 with open("evidence.out", encoding="utf-8") as f: 1aBbCDcEdFGeHfIAJgKhLMiNjOPkQlRSmTnUVoWpXYqZr01234
204 assert f.read() == "we are here!" 1aBbCDcEdFGeHfIAJgKhLMiNjOPkQlRSmTnUVoWpXYqZr01234
206 def test_missing_plugin_raises_import_error(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
207 # Prove that a missing plugin will raise an ImportError.
208 with pytest.raises(ImportError, match="No module named '?does_not_exist_woijwoicweo'?"): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
209 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
210 cov.set_option("run:plugins", ["does_not_exist_woijwoicweo"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
211 cov.start() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
212 cov.stop() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
214 def test_bad_plugin_isnt_hidden(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
215 # Prove that a plugin with an error in it will raise the error.
216 self.make_file("plugin_over_zero.py", "1/0") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
217 with pytest.raises(ZeroDivisionError): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
218 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
219 cov.set_option("run:plugins", ["plugin_over_zero"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
220 cov.start() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
221 cov.stop() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
223 def test_plugin_sys_info(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
224 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
225 "plugin_sys_info.py",
226 """\
227 import coverage
229 class Plugin(coverage.CoveragePlugin):
230 def sys_info(self):
231 return [("hello", "world")]
233 def coverage_init(reg, options):
234 reg.add_file_tracer(Plugin())
235 """,
236 )
237 debug_out = io.StringIO() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
238 cov = coverage.Coverage(debug=["sys"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
239 cov._debug_file = debug_out 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
240 cov.set_option("run:plugins", ["plugin_sys_info"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
241 with swallow_warnings( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
242 r"Plugin file tracers \(plugin_sys_info.Plugin\) aren't supported with .*",
243 ):
244 cov.start() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
245 cov.stop() # pragma: nested 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
247 out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] 1aBbCsDcEdFtGeHfIJgKhLuMiNjOPkQlRvSmTnUwVoWpXxYqZr0y1234
248 if testenv.C_TRACER: 1aBbCsDcEdFtGeHfIJgKhLuMiNjOPkQlRvSmTnUwVoWpXxYqZr0y1234
249 assert "plugins.file_tracers: plugin_sys_info.Plugin" in out_lines 1abscdtefghuijklvmnwopxqry
250 else:
251 assert "plugins.file_tracers: plugin_sys_info.Plugin (disabled)" in out_lines 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
252 assert "plugins.configurers: -none-" in out_lines 1aBbCsDcEdFtGeHfIJgKhLuMiNjOPkQlRvSmTnUwVoWpXxYqZr0y1234
253 expected_end = [ 1aBbCsDcEdFtGeHfIJgKhLuMiNjOPkQlRvSmTnUwVoWpXxYqZr0y1234
254 "-- sys: plugin_sys_info.Plugin -------------------------------",
255 "hello: world",
256 "-- end -------------------------------------------------------",
257 ]
258 assert expected_end == out_lines[-len(expected_end) :] 1aBbCsDcEdFtGeHfIJgKhLuMiNjOPkQlRvSmTnUwVoWpXxYqZr0y1234
260 def test_plugin_with_no_sys_info(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
261 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
262 "plugin_no_sys_info.py",
263 """\
264 import coverage
266 class Plugin(coverage.CoveragePlugin):
267 pass
269 def coverage_init(reg, options):
270 reg.add_configurer(Plugin())
271 """,
272 )
273 debug_out = io.StringIO() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
274 cov = coverage.Coverage(debug=["sys"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
275 cov._debug_file = debug_out 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
276 cov.set_option("run:plugins", ["plugin_no_sys_info"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
277 cov.start() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
278 cov.stop() # pragma: nested 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
280 out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
281 assert "plugins.file_tracers: -none-" in out_lines 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
282 assert "plugins.configurers: plugin_no_sys_info.Plugin" in out_lines 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
283 expected_end = [ 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
284 "-- sys: plugin_no_sys_info.Plugin ----------------------------",
285 "-- end -------------------------------------------------------",
286 ]
287 assert expected_end == out_lines[-len(expected_end) :] 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
289 def test_local_files_are_importable(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
290 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
291 "importing_plugin.py",
292 """\
293 from coverage import CoveragePlugin
294 import local_module
295 class MyPlugin(CoveragePlugin):
296 pass
297 def coverage_init(reg, options):
298 reg.add_noop(MyPlugin())
299 """,
300 )
301 self.make_file("local_module.py", "CONST = 1") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
302 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
303 ".coveragerc",
304 """\
305 [run]
306 plugins = importing_plugin
307 """,
308 )
309 self.make_file("main_file.py", "print('MAIN')") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
311 out = self.run_command("coverage run main_file.py") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
312 assert out == "MAIN\n" 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
313 out = self.run_command("coverage html -q") # sneak in a test of -q 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
314 assert out == "" 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
316 def test_coverage_init_plugins(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
317 called = False 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
319 def coverage_init(reg: Plugins) -> None: # pylint: disable=unused-argument 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
320 nonlocal called
321 called = True 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
323 cov = coverage.Coverage(plugins=[coverage_init]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
324 # Calls _init() and loads plugins
325 cov.sys_info() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
327 assert called 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
330@pytest.mark.skipif(testenv.PLUGINS, reason="This core doesn't support plugins.") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
331class PluginWarningOnPyTracerTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
332 """Test that we get a controlled exception when plugins aren't supported."""
334 def test_exception_if_plugins_on_pytracer(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
335 self.make_file("simple.py", "a = 1") 1BCDEFGHIJKLMNOPQ5R6S7T8U9V!W#X$Y%Z'0(1)234
337 cov = coverage.Coverage() 1BCDEFGHIJKLMNOPQ5R6S7T8U9V!W#X$Y%Z'0(1)234
338 cov.set_option("run:plugins", ["tests.plugin1"]) 1BCDEFGHIJKLMNOPQ5R6S7T8U9V!W#X$Y%Z'0(1)234
340 if testenv.PY_TRACER: 1BCDEFGHIJKLMNOPQ5R6S7T8U9V!W#X$Y%Z'0(1)234
341 core = "PyTracer" 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
342 else:
343 assert testenv.SYS_MON 156789!#$%'()
344 core = "SysMonitor" 156789!#$%'()
346 expected_warnings = [ 1BCDEFGHIJKLMNOPQ5R6S7T8U9V!W#X$Y%Z'0(1)234
347 rf"Plugin file tracers \(tests.plugin1.Plugin\) aren't supported with {core}",
348 ]
349 with self.assert_warnings(cov, expected_warnings): 1BCDEFGHIJKLMNOPQ5R6S7T8U9V!W#X$Y%Z'0(1)234
350 self.start_import_stop(cov, "simple") 1BCDEFGHIJKLMNOPQ5R6S7T8U9V!W#X$Y%Z'0(1)234
353@pytest.mark.skipif(not testenv.PLUGINS, reason="Plugins are not supported with this core.") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
354class FileTracerTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
355 """Tests of plugins that implement file_tracer."""
358class GoodFileTracerTest(FileTracerTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
359 """Tests of file tracer plugin happy paths."""
361 def test_plugin1(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
362 self.make_file( 1abscdtefAghuijzklvmnwopxqry
363 "simple.py",
364 """\
365 import try_xyz
366 a = 1
367 b = 2
368 """,
369 )
370 self.make_file( 1abscdtefAghuijzklvmnwopxqry
371 "try_xyz.py",
372 """\
373 c = 3
374 d = 4
375 """,
376 )
378 cov = coverage.Coverage() 1abscdtefAghuijzklvmnwopxqry
379 CheckUniqueFilenames.hook(cov, "_should_trace") 1abscdtefAghuijzklvmnwopxqry
380 CheckUniqueFilenames.hook(cov, "_check_include_omit_etc") 1abscdtefAghuijzklvmnwopxqry
381 cov.set_option("run:plugins", ["tests.plugin1"]) 1abscdtefAghuijzklvmnwopxqry
383 # Import the Python file, executing it.
384 self.start_import_stop(cov, "simple") 1abscdtefAghuijzklvmnwopxqry
386 _, statements, missing, _ = cov.analysis("simple.py") 1abscdtefAghijklvmnwopxqry
387 assert statements == [1, 2, 3] 1abscdtefAghijklvmnwopxqry
388 assert missing == [] 1abscdtefAghijklvmnwopxqry
389 zzfile = os.path.abspath(os.path.join("/src", "try_ABC.zz")) 1abscdtefAghijklvmnwopxqry
390 _, statements, _, _ = cov.analysis(zzfile) 1abscdtefAghijklvmnwopxqry
391 assert statements == [105, 106, 107, 205, 206, 207] 1abscdtefAghijklvmnwopxqry
393 def make_render_and_caller(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
394 """Make the render.py and caller.py files we need."""
395 # plugin2 emulates a dynamic tracing plugin: the caller's locals
396 # are examined to determine the source file and line number.
397 # The plugin is in tests/plugin2.py.
398 self.make_file( 1abscdtefAghuijzklvmnwopxqry
399 "render.py",
400 """\
401 def render(filename, linenum):
402 # This function emulates a template renderer. The plugin
403 # will examine the `filename` and `linenum` locals to
404 # determine the source file and line number.
405 fiddle_around = 1 # not used, just chaff.
406 return "[{} @ {}]".format(filename, linenum)
408 def helper(x):
409 # This function is here just to show that not all code in
410 # this file will be part of the dynamic tracing.
411 return x+1
412 """,
413 )
414 self.make_file( 1abscdtefAghuijzklvmnwopxqry
415 "caller.py",
416 """\
417 import sys
418 from render import helper, render
420 assert render("foo_7.html", 4) == "[foo_7.html @ 4]"
421 # Render foo_7.html again to try the CheckUniqueFilenames asserts.
422 render("foo_7.html", 4)
424 assert helper(42) == 43
425 assert render("bar_4.html", 2) == "[bar_4.html @ 2]"
426 assert helper(76) == 77
428 # quux_5.html will be omitted from the results.
429 assert render("quux_5.html", 3) == "[quux_5.html @ 3]"
430 """,
431 )
433 # will try to read the actual source files, so make some
434 # source files.
435 def lines(n: int) -> str: 1abscdtefAghuijzklvmnwopxqry
436 """Make a string with n lines of text."""
437 return "".join("line %d\n" % i for i in range(n)) 1abscdtefAghuijzklvmnwopxqry
439 self.make_file("bar_4.html", lines(4)) 1abscdtefAghuijzklvmnwopxqry
440 self.make_file("foo_7.html", lines(7)) 1abscdtefAghuijzklvmnwopxqry
442 def test_plugin2(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
443 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry
445 cov = coverage.Coverage(omit=["*quux*"]) 1abscdtefAghuijzklvmnwopxqry
446 CheckUniqueFilenames.hook(cov, "_should_trace") 1abscdtefAghuijzklvmnwopxqry
447 CheckUniqueFilenames.hook(cov, "_check_include_omit_etc") 1abscdtefAghuijzklvmnwopxqry
448 cov.set_option("run:plugins", ["tests.plugin2"]) 1abscdtefAghuijzklvmnwopxqry
450 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry
452 # The way plugin2 works, a file named foo_7.html will be claimed to
453 # have 7 lines in it. If render() was called with line number 4,
454 # then the plugin will claim that lines 4 and 5 were executed.
455 _, statements, missing, _ = cov.analysis("foo_7.html") 1abscdtefghuijzklmnopqr
456 assert statements == [1, 2, 3, 4, 5, 6, 7] 1abscdtefghuijzklmnopqr
457 assert missing == [1, 2, 3, 6, 7] 1abscdtefghuijzklmnopqr
458 assert "foo_7.html" in line_counts(cov.get_data()) 1abscdtefghuijzklmnopqr
460 _, statements, missing, _ = cov.analysis("bar_4.html") 1abscdtefghuijzklmnopqr
461 assert statements == [1, 2, 3, 4] 1abscdtefghuijzklmnopqr
462 assert missing == [1, 4] 1abscdtefghuijzklmnopqr
463 assert "bar_4.html" in line_counts(cov.get_data()) 1abscdtefghuijzklmnopqr
465 assert "quux_5.html" not in line_counts(cov.get_data()) 1abscdtefghuijzklmnopqr
467 def test_plugin2_with_branch(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
468 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry
470 cov = coverage.Coverage(branch=True, omit=["*quux*"]) 1abscdtefAghuijzklvmnwopxqry
471 CheckUniqueFilenames.hook(cov, "_should_trace") 1abscdtefAghuijzklvmnwopxqry
472 CheckUniqueFilenames.hook(cov, "_check_include_omit_etc") 1abscdtefAghuijzklvmnwopxqry
473 cov.set_option("run:plugins", ["tests.plugin2"]) 1abscdtefAghuijzklvmnwopxqry
475 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry
477 # The way plugin2 works, a file named foo_7.html will be claimed to
478 # have 7 lines in it. If render() was called with line number 4,
479 # then the plugin will claim that lines 4 and 5 were executed.
480 analysis = cov._analyze("foo_7.html") 1abscdtefAghuijzklvmnwopxqry
481 assert analysis.statements == {1, 2, 3, 4, 5, 6, 7} 1abscdtefAghuijzklvmnwopxqry
482 # Plugins don't do branch coverage yet.
483 assert analysis.has_arcs is True 1abscdtefAghuijzklvmnwopxqry
484 assert analysis.arc_possibilities == [] 1abscdtefAghuijzklvmnwopxqry
486 assert analysis.missing == {1, 2, 3, 6, 7} 1abscdtefAghuijzklvmnwopxqry
488 def test_plugin2_with_text_report(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
489 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry
491 cov = coverage.Coverage(branch=True, omit=["*quux*"]) 1abscdtefAghuijzklvmnwopxqry
492 cov.set_option("run:plugins", ["tests.plugin2"]) 1abscdtefAghuijzklvmnwopxqry
494 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry
496 repout = io.StringIO() 1abscdtefAghuijzklvmnwopxqry
497 total = cov.report(file=repout, include=["*.html"], omit=["uni*.html"], show_missing=True) 1abscdtefAghuijzklvmnwopxqry
498 report = repout.getvalue().splitlines() 1abscdtefAghuijzklvmnwopxqry
499 expected = [ 1abscdtefAghuijzklvmnwopxqry
500 "Name Stmts Miss Branch BrPart Cover Missing",
501 "--------------------------------------------------------",
502 "bar_4.html 4 2 0 0 50% 1, 4",
503 "foo_7.html 7 5 0 0 29% 1-3, 6-7",
504 "--------------------------------------------------------",
505 "TOTAL 11 7 0 0 36%",
506 ]
507 assert expected == report 1abscdtefAghuijzklvmnwopxqry
508 assert math.isclose(total, 4 / 11 * 100) 1abscdtefAghuijzklvmnwopxqry
510 def test_plugin2_with_html_report(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
511 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry
513 cov = coverage.Coverage(branch=True, omit=["*quux*"]) 1abscdtefAghuijzklvmnwopxqry
514 cov.set_option("run:plugins", ["tests.plugin2"]) 1abscdtefAghuijzklvmnwopxqry
516 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry
518 total = cov.html_report(include=["*.html"], omit=["uni*.html"]) 1abscdtefAghuijzklvmnwopxqry
519 assert math.isclose(total, 4 / 11 * 100) 1abscdtefAghuijzklvmnwopxqry
521 self.assert_exists("htmlcov/index.html") 1abscdtefAghuijzklvmnwopxqry
522 self.assert_exists("htmlcov/bar_4_html.html") 1abscdtefAghuijzklvmnwopxqry
523 self.assert_exists("htmlcov/foo_7_html.html") 1abscdtefAghuijzklvmnwopxqry
525 def test_plugin2_with_xml_report(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
526 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry
528 cov = coverage.Coverage(branch=True, omit=["*quux*"]) 1abscdtefAghuijzklvmnwopxqry
529 cov.set_option("run:plugins", ["tests.plugin2"]) 1abscdtefAghuijzklvmnwopxqry
531 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry
533 total = cov.xml_report(include=["*.html"], omit=["uni*.html"]) 1abscdtefAghuijzklvmnwopxqry
534 assert math.isclose(total, 4 / 11 * 100) 1abscdtefAghuijzklvmnwopxqry
536 dom = ElementTree.parse("coverage.xml") 1abscdtefAghuijzklvmnwopxqry
537 classes = {} 1abscdtefAghuijzklvmnwopxqry
538 for elt in dom.findall(".//class"): 1abscdtefAghuijzklvmnwopxqry
539 classes[elt.get("name")] = elt 1abscdtefAghuijzklvmnwopxqry
541 assert classes["bar_4.html"].attrib == { 1abscdtefAghuijzklvmnwopxqry
542 "branch-rate": "1",
543 "complexity": "0",
544 "filename": "bar_4.html",
545 "line-rate": "0.5",
546 "name": "bar_4.html",
547 }
548 assert classes["foo_7.html"].attrib == { 1abscdtefAghuijzklvmnwopxqry
549 "branch-rate": "1",
550 "complexity": "0",
551 "filename": "foo_7.html",
552 "line-rate": "0.2857",
553 "name": "foo_7.html",
554 }
556 def test_defer_to_python(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
557 # A plugin that measures, but then wants built-in python reporting.
558 self.make_file( 1abscdtefAghuijzklvmnwopxqry
559 "fairly_odd_plugin.py",
560 """\
561 # A plugin that claims all the odd lines are executed, and none of
562 # the even lines, and then punts reporting off to the built-in
563 # Python reporting.
564 import coverage.plugin
565 class Plugin(coverage.CoveragePlugin):
566 def file_tracer(self, filename):
567 return OddTracer(filename)
568 def file_reporter(self, filename):
569 return "python"
571 class OddTracer(coverage.plugin.FileTracer):
572 def __init__(self, filename):
573 self.filename = filename
574 def source_filename(self):
575 return self.filename
576 def line_number_range(self, frame):
577 lineno = frame.f_lineno
578 if lineno % 2:
579 return (lineno, lineno)
580 else:
581 return (-1, -1)
583 def coverage_init(reg, options):
584 reg.add_file_tracer(Plugin())
585 """,
586 )
587 self.make_file( 1abscdtefAghuijzklvmnwopxqry
588 "unsuspecting.py",
589 """\
590 a = 1
591 b = 2
592 c = 3
593 d = 4
594 e = 5
595 f = 6
596 """,
597 )
598 cov = coverage.Coverage(include=["unsuspecting.py"]) 1abscdtefAghuijzklvmnwopxqry
599 cov.set_option("run:plugins", ["fairly_odd_plugin"]) 1abscdtefAghuijzklvmnwopxqry
600 self.start_import_stop(cov, "unsuspecting") 1abscdtefAghuijzklvmnwopxqry
602 repout = io.StringIO() 1abscdtefAghuijzklvmnwopxqry
603 total = cov.report(file=repout, show_missing=True) 1abscdtefAghuijzklvmnwopxqry
604 report = repout.getvalue().splitlines() 1abscdtefAghuijzklvmnwopxqry
605 expected = [ 1abscdtefAghuijzklvmnwopxqry
606 "Name Stmts Miss Cover Missing",
607 "-----------------------------------------------",
608 "unsuspecting.py 6 3 50% 2, 4, 6",
609 "-----------------------------------------------",
610 "TOTAL 6 3 50%",
611 ]
612 assert expected == report 1abscdtefAghuijzklvmnwopxqry
613 assert total == 50 1abscdtefAghuijzklvmnwopxqry
615 def test_find_unexecuted(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
616 self.make_file( 1abscdtefAghuijzklvmnwopxqry
617 "unexecuted_plugin.py",
618 """\
619 import os
620 import coverage.plugin
621 class Plugin(coverage.CoveragePlugin):
622 def file_tracer(self, filename):
623 if filename.endswith("foo.py"):
624 return MyTracer(filename)
625 def file_reporter(self, filename):
626 return MyReporter(filename)
627 def find_executable_files(self, src_dir):
628 # Check that src_dir is the right value
629 files = os.listdir(src_dir)
630 assert "foo.py" in files
631 assert "unexecuted_plugin.py" in files
632 return ["chimera.py"]
634 class MyTracer(coverage.plugin.FileTracer):
635 def __init__(self, filename):
636 self.filename = filename
637 def source_filename(self):
638 return self.filename
639 def line_number_range(self, frame):
640 return (999, 999)
642 class MyReporter(coverage.FileReporter):
643 def lines(self):
644 return {99, 999, 9999}
646 def coverage_init(reg, options):
647 reg.add_file_tracer(Plugin())
648 """,
649 )
650 self.make_file("foo.py", "a = 1") 1abscdtefAghuijzklvmnwopxqry
651 cov = coverage.Coverage(source=["."]) 1abscdtefAghuijzklvmnwopxqry
652 cov.set_option("run:plugins", ["unexecuted_plugin"]) 1abscdtefAghuijzklvmnwopxqry
653 self.start_import_stop(cov, "foo") 1abscdtefAghuijzklvmnwopxqry
655 # The file we executed claims to have run line 999.
656 _, statements, missing, _ = cov.analysis("foo.py") 1abscdtefAghuijzklvmnwopxqry
657 assert statements == [99, 999, 9999] 1abscdtefAghuijzklvmnwopxqry
658 assert missing == [99, 9999] 1abscdtefAghuijzklvmnwopxqry
660 # The completely missing file is in the results.
661 _, statements, missing, _ = cov.analysis("chimera.py") 1abscdtefAghuijzklvmnwopxqry
662 assert statements == [99, 999, 9999] 1abscdtefAghuijzklvmnwopxqry
663 assert missing == [99, 999, 9999] 1abscdtefAghuijzklvmnwopxqry
665 # But completely new filenames are not in the results.
666 assert len(cov.get_data().measured_files()) == 3 1abscdtefAghuijzklvmnwopxqry
667 with pytest.raises(NoSource): 1abscdtefAghuijzklvmnwopxqry
668 cov.analysis("fictional.py") 1abscdtefAghuijzklvmnwopxqry
671class BadFileTracerTest(FileTracerTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
672 """Test error handling around file tracer plugins."""
674 def run_plugin(self, module_name: str) -> Coverage: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
675 """Run a plugin with the given module_name.
677 Uses a few fixed Python files.
679 Returns the Coverage object.
681 """
682 self.make_file( 1abscdtefAghuijzklvmnwopxqry
683 "simple.py",
684 """\
685 import other, another
686 a = other.f(2)
687 b = other.f(3)
688 c = another.g(4)
689 d = another.g(5)
690 """,
691 )
692 # The names of these files are important: some plugins apply themselves
693 # to "*other.py".
694 self.make_file( 1abscdtefAghuijzklvmnwopxqry
695 "other.py",
696 """\
697 def f(x):
698 return x+1
699 """,
700 )
701 self.make_file( 1abscdtefAghuijzklvmnwopxqry
702 "another.py",
703 """\
704 def g(x):
705 return x-1
706 """,
707 )
709 cov = coverage.Coverage() 1abscdtefAghuijzklvmnwopxqry
710 cov.set_option("run:plugins", [module_name]) 1abscdtefAghuijzklvmnwopxqry
711 self.start_import_stop(cov, "simple") 1abscdtefAghuijzklvmnwopxqry
712 cov.save() # pytest-cov does a save after stop, so we'll do it too. 1abscdtefAghuijzklvmnwopxqry
713 return cov 1abscdtefAghuijzklvmnwopxqry
715 def run_bad_plugin( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)
716 self,
717 module_name: str,
718 plugin_name: str,
719 our_error: bool = True,
720 excmsg: str | None = None,
721 excmsgs: list[str] | None = None,
722 ) -> None:
723 """Run a file, and see that the plugin failed.
725 `module_name` and `plugin_name` is the module and name of the plugin to
726 use.
728 `our_error` is True if the error reported to the user will be an
729 explicit error in our test code, marked with an '# Oh noes!' comment.
731 `excmsg`, if provided, is text that must appear in the stderr.
733 `excmsgs`, if provided, is a list of messages, one of which must
734 appear in the stderr.
736 The plugin will be disabled, and we check that a warning is output
737 explaining why.
739 """
740 with pytest.warns(Warning) as warns: 1abscdtefAghuijzklvmnwopxqry
741 self.run_plugin(module_name) 1abscdtefAghuijzklvmnwopxqry
743 stderr = self.stderr() 1abscdtefAghuijzklvmnwopxqry
744 stderr += "".join(str(w.message) for w in warns) 1abscdtefAghuijzklvmnwopxqry
745 if our_error: 1abscdtefAghuijzklvmnwopxqry
746 # The exception we're causing should only appear once.
747 assert stderr.count("# Oh noes!") == 1 1abscdtefAghuijzklvmnwopxqry
749 # There should be a warning explaining what's happening, but only one.
750 # The message can be in two forms:
751 # Disabling plug-in '...' due to previous exception
752 # or:
753 # Disabling plug-in '...' due to an exception:
754 print([str(w) for w in warns.list]) 1abscdtefAghuijzklvmnwopxqry
755 warnings = [w for w in warns.list if issubclass(w.category, CoverageWarning)] 1abscdtefAghuijzklvmnwopxqry
756 assert len(warnings) == 1 1abscdtefAghuijzklvmnwopxqry
757 warnmsg = str(warnings[0].message) 1abscdtefAghuijzklvmnwopxqry
758 assert f"Disabling plug-in '{module_name}.{plugin_name}' due to " in warnmsg 1abscdtefAghuijzklvmnwopxqry
760 if excmsg: 1abscdtefAghuijzklvmnwopxqry
761 assert excmsg in stderr 1abscdtefAghuijzklvmnwopxqry
762 if excmsgs: 1abscdtefAghuijzklvmnwopxqry
763 found_exc = any(em in stderr for em in excmsgs) # pragma: part covered 1abscdtefAghuijzklvmnwopxqry
764 assert found_exc, f"expected one of {excmsgs} in stderr" 1abscdtefAghuijzklvmnwopxqry
766 def test_file_tracer_has_no_file_tracer_method(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
767 self.make_file( 1abscdtefAghuijzklvmnwopxqry
768 "bad_plugin.py",
769 """\
770 class Plugin(object):
771 pass
773 def coverage_init(reg, options):
774 reg.add_file_tracer(Plugin())
775 """,
776 )
777 self.run_bad_plugin("bad_plugin", "Plugin", our_error=False) 1abscdtefAghuijzklvmnwopxqry
779 def test_file_tracer_has_inherited_sourcefilename_method(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
780 self.make_file( 1abscdtefAghuijzklvmnwopxqry
781 "bad_plugin.py",
782 """\
783 import coverage
784 class Plugin(coverage.CoveragePlugin):
785 def file_tracer(self, filename):
786 # Just grab everything.
787 return FileTracer()
789 class FileTracer(coverage.FileTracer):
790 pass
792 def coverage_init(reg, options):
793 reg.add_file_tracer(Plugin())
794 """,
795 )
796 self.run_bad_plugin( 1abscdtefAghuijzklvmnwopxqry
797 "bad_plugin",
798 "Plugin",
799 our_error=False,
800 excmsg="Class 'bad_plugin.FileTracer' needs to implement source_filename()",
801 )
803 def test_plugin_has_inherited_filereporter_method(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
804 self.make_file( 1abscdtefAghuijzklvmnwopxqry
805 "bad_plugin.py",
806 """\
807 import coverage
808 class Plugin(coverage.CoveragePlugin):
809 def file_tracer(self, filename):
810 # Just grab everything.
811 return FileTracer()
813 class FileTracer(coverage.FileTracer):
814 def source_filename(self):
815 return "foo.xxx"
817 def coverage_init(reg, options):
818 reg.add_file_tracer(Plugin())
819 """,
820 )
821 cov = self.run_plugin("bad_plugin") 1abscdtefAghuijzklvmnwopxqry
822 expected_msg = "Plugin 'bad_plugin.Plugin' needs to implement file_reporter()" 1abscdtefAghuijzklvmnwopxqry
823 with pytest.raises(NotImplementedError, match=expected_msg): 1abscdtefAghuijzklvmnwopxqry
824 cov.report() 1abscdtefAghuijzklvmnwopxqry
826 def test_file_tracer_fails(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
827 self.make_file( 1abscdtefAghuijzklvmnwopxqry
828 "bad_plugin.py",
829 """\
830 import coverage.plugin
831 class Plugin(coverage.plugin.CoveragePlugin):
832 def file_tracer(self, filename):
833 17/0 # Oh noes!
835 def coverage_init(reg, options):
836 reg.add_file_tracer(Plugin())
837 """,
838 )
839 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry
841 def test_file_tracer_fails_eventually(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
842 # Django coverage plugin can report on a few files and then fail.
843 # https://github.com/coveragepy/coveragepy/issues/1011
844 self.make_file( 1abscdtefAghuijzklvmnwopxqry
845 "bad_plugin.py",
846 """\
847 import os.path
848 import coverage.plugin
849 class Plugin(coverage.plugin.CoveragePlugin):
850 def __init__(self):
851 self.calls = 0
853 def file_tracer(self, filename):
854 print(filename)
855 self.calls += 1
856 if self.calls <= 2:
857 return FileTracer(filename)
858 else:
859 17/0 # Oh noes!
861 class FileTracer(coverage.FileTracer):
862 def __init__(self, filename):
863 self.filename = filename
864 def source_filename(self):
865 return os.path.basename(self.filename).replace(".py", ".foo")
866 def line_number_range(self, frame):
867 return -1, -1
869 def coverage_init(reg, options):
870 reg.add_file_tracer(Plugin())
871 """,
872 )
873 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry
875 def test_file_tracer_returns_wrong(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
876 self.make_file( 1abscdtefAghuijzklvmnwopxqry
877 "bad_plugin.py",
878 """\
879 import coverage.plugin
880 class Plugin(coverage.plugin.CoveragePlugin):
881 def file_tracer(self, filename):
882 return 3.14159
884 def coverage_init(reg, options):
885 reg.add_file_tracer(Plugin())
886 """,
887 )
888 self.run_bad_plugin( 1abscdtefAghuijzklvmnwopxqry
889 "bad_plugin",
890 "Plugin",
891 our_error=False,
892 excmsg="'float' object has no attribute",
893 )
895 def test_has_dynamic_source_filename_fails(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
896 self.make_file( 1abscdtefAghuijzklvmnwopxqry
897 "bad_plugin.py",
898 """\
899 import coverage.plugin
900 class Plugin(coverage.plugin.CoveragePlugin):
901 def file_tracer(self, filename):
902 return BadFileTracer()
904 class BadFileTracer(coverage.plugin.FileTracer):
905 def has_dynamic_source_filename(self):
906 23/0 # Oh noes!
908 def coverage_init(reg, options):
909 reg.add_file_tracer(Plugin())
910 """,
911 )
912 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry
914 def test_source_filename_fails(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
915 self.make_file( 1abscdtefAghuijzklvmnwopxqry
916 "bad_plugin.py",
917 """\
918 import coverage.plugin
919 class Plugin(coverage.plugin.CoveragePlugin):
920 def file_tracer(self, filename):
921 return BadFileTracer()
923 class BadFileTracer(coverage.plugin.FileTracer):
924 def source_filename(self):
925 42/0 # Oh noes!
927 def coverage_init(reg, options):
928 reg.add_file_tracer(Plugin())
929 """,
930 )
931 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry
933 def test_source_filename_returns_wrong(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
934 self.make_file( 1abscdtefAghuijzklvmnwopxqry
935 "bad_plugin.py",
936 """\
937 import coverage.plugin
938 class Plugin(coverage.plugin.CoveragePlugin):
939 def file_tracer(self, filename):
940 return BadFileTracer()
942 class BadFileTracer(coverage.plugin.FileTracer):
943 def source_filename(self):
944 return 17.3
946 def coverage_init(reg, options):
947 reg.add_file_tracer(Plugin())
948 """,
949 )
950 self.run_bad_plugin( 1abscdtefAghuijzklvmnwopxqry
951 "bad_plugin",
952 "Plugin",
953 our_error=False,
954 excmsgs=[
955 "expected str, bytes or os.PathLike object, not float",
956 "'float' object has no attribute",
957 "object of type 'float' has no len()",
958 "'float' object is unsubscriptable",
959 ],
960 )
962 def test_dynamic_source_filename_fails(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
963 self.make_file( 1abscdtefAghuijzklvmnwopxqry
964 "bad_plugin.py",
965 """\
966 import coverage.plugin
967 class Plugin(coverage.plugin.CoveragePlugin):
968 def file_tracer(self, filename):
969 if filename.endswith("other.py"):
970 return BadFileTracer()
972 class BadFileTracer(coverage.plugin.FileTracer):
973 def has_dynamic_source_filename(self):
974 return True
975 def dynamic_source_filename(self, filename, frame):
976 101/0 # Oh noes!
978 def coverage_init(reg, options):
979 reg.add_file_tracer(Plugin())
980 """,
981 )
982 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry
984 def test_line_number_range_raises_error(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
985 self.make_file( 1abscdtefAghuijzklvmnwopxqry
986 "bad_plugin.py",
987 """\
988 import coverage.plugin
989 class Plugin(coverage.plugin.CoveragePlugin):
990 def file_tracer(self, filename):
991 if filename.endswith("other.py"):
992 return BadFileTracer()
994 class BadFileTracer(coverage.plugin.FileTracer):
995 def source_filename(self):
996 return "something.foo"
998 def line_number_range(self, frame):
999 raise Exception("borked!")
1001 def coverage_init(reg, options):
1002 reg.add_file_tracer(Plugin())
1003 """,
1004 )
1005 self.run_bad_plugin( 1abscdtefAghuijzklvmnwopxqry
1006 "bad_plugin",
1007 "Plugin",
1008 our_error=False,
1009 excmsg="borked!",
1010 )
1012 def test_line_number_range_returns_non_tuple(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1013 self.make_file( 1abscdtefAghuijzklvmnwopxqry
1014 "bad_plugin.py",
1015 """\
1016 import coverage.plugin
1017 class Plugin(coverage.plugin.CoveragePlugin):
1018 def file_tracer(self, filename):
1019 if filename.endswith("other.py"):
1020 return BadFileTracer()
1022 class BadFileTracer(coverage.plugin.FileTracer):
1023 def source_filename(self):
1024 return "something.foo"
1026 def line_number_range(self, frame):
1027 return 42.23
1029 def coverage_init(reg, options):
1030 reg.add_file_tracer(Plugin())
1031 """,
1032 )
1033 self.run_bad_plugin( 1abscdtefAghuijzklvmnwopxqry
1034 "bad_plugin",
1035 "Plugin",
1036 our_error=False,
1037 excmsg="line_number_range must return 2-tuple",
1038 )
1040 def test_line_number_range_returns_triple(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1041 self.make_file( 1abscdtefAghuijzklvmnwopxqry
1042 "bad_plugin.py",
1043 """\
1044 import coverage.plugin
1045 class Plugin(coverage.plugin.CoveragePlugin):
1046 def file_tracer(self, filename):
1047 if filename.endswith("other.py"):
1048 return BadFileTracer()
1050 class BadFileTracer(coverage.plugin.FileTracer):
1051 def source_filename(self):
1052 return "something.foo"
1054 def line_number_range(self, frame):
1055 return (1, 2, 3)
1057 def coverage_init(reg, options):
1058 reg.add_file_tracer(Plugin())
1059 """,
1060 )
1061 self.run_bad_plugin( 1abscdtefAghuijzklvmnwopxqry
1062 "bad_plugin",
1063 "Plugin",
1064 our_error=False,
1065 excmsg="line_number_range must return 2-tuple",
1066 )
1068 def test_line_number_range_returns_pair_of_strings(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1069 self.make_file( 1abscdtefAghuijzklvmnwopxqry
1070 "bad_plugin.py",
1071 """\
1072 import coverage.plugin
1073 class Plugin(coverage.plugin.CoveragePlugin):
1074 def file_tracer(self, filename):
1075 if filename.endswith("other.py"):
1076 return BadFileTracer()
1078 class BadFileTracer(coverage.plugin.FileTracer):
1079 def source_filename(self):
1080 return "something.foo"
1082 def line_number_range(self, frame):
1083 return ("5", "7")
1085 def coverage_init(reg, options):
1086 reg.add_file_tracer(Plugin())
1087 """,
1088 )
1089 self.run_bad_plugin( 1abscdtefAghuijzklvmnwopxqry
1090 "bad_plugin",
1091 "Plugin",
1092 our_error=False,
1093 excmsgs=[
1094 "an integer is required",
1095 "cannot be interpreted as an integer",
1096 ],
1097 )
1100class ConfigurerPluginTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1101 """Test configuring plugins."""
1103 run_in_temp_dir = False 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1105 def test_configurer_plugin(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1106 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1107 cov.set_option("run:plugins", ["tests.plugin_config"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1108 cov.start() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1109 cov.stop() # pragma: nested 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
1110 excluded = cov.get_option("report:exclude_lines") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1111 assert isinstance(excluded, list) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1112 assert "pragma: custom" in excluded 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1113 assert "pragma: or whatever" in excluded 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1116@pytest.mark.skipif(not testenv.DYN_CONTEXTS, reason="No dynamic contexts with this core") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1117class DynamicContextPluginTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1118 """Tests of plugins that implement `dynamic_context`."""
1120 def make_plugin_capitalized_testnames(self, filename: str) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1121 """Create a dynamic context plugin that capitalizes the part after 'test_'."""
1122 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1123 filename,
1124 """\
1125 from coverage import CoveragePlugin
1127 class Plugin(CoveragePlugin):
1128 def dynamic_context(self, frame):
1129 name = frame.f_code.co_name
1130 if name.startswith(("test_", "doctest_")):
1131 parts = name.split("_", 1)
1132 return "%s:%s" % (parts[0], parts[1].upper())
1133 return None
1135 def coverage_init(reg, options):
1136 reg.add_dynamic_context(Plugin())
1137 """,
1138 )
1140 def make_plugin_track_render(self, filename: str) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1141 """Make a dynamic context plugin that tracks 'render_' functions."""
1142 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1143 filename,
1144 """\
1145 from coverage import CoveragePlugin
1147 class Plugin(CoveragePlugin):
1148 def dynamic_context(self, frame):
1149 name = frame.f_code.co_name
1150 if name.startswith("render_"):
1151 return 'renderer:' + name[7:]
1152 return None
1154 def coverage_init(reg, options):
1155 reg.add_dynamic_context(Plugin())
1156 """,
1157 )
1159 def make_test_files(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1160 """Make some files to use while testing dynamic context plugins."""
1161 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1162 "rendering.py",
1163 """\
1164 def html_tag(tag, content):
1165 return f'<{tag}>{content}</{tag}>'
1167 def render_paragraph(text):
1168 return html_tag('p', text)
1170 def render_span(text):
1171 return html_tag('span', text)
1173 def render_bold(text):
1174 return html_tag('b', text)
1175 """,
1176 )
1178 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1179 "testsuite.py",
1180 """\
1181 import rendering
1183 def test_html_tag() -> None:
1184 assert rendering.html_tag('b', 'hello') == '<b>hello</b>'
1186 def doctest_html_tag():
1187 assert eval('''
1188 rendering.html_tag('i', 'text') == '<i>text</i>'
1189 '''.strip())
1191 def test_renderers() -> None:
1192 assert rendering.render_paragraph('hello') == '<p>hello</p>'
1193 assert rendering.render_bold('wide') == '<b>wide</b>'
1194 assert rendering.render_span('world') == '<span>world</span>'
1196 def build_full_html():
1197 html = '<html><body>%s</body></html>' % (
1198 rendering.render_paragraph(
1199 rendering.render_span('hello')))
1200 return html
1201 """,
1202 )
1204 def run_all_functions(self, cov: Coverage, suite_name: str) -> None: # pragma: nested 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1205 """Run all functions in `suite_name` under coverage."""
1206 cov.start() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1207 suite = import_local_file(suite_name) 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
1208 try: 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
1209 # Call all functions in this module
1210 for name in dir(suite): 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
1211 variable = getattr(suite, name) 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
1212 if inspect.isfunction(variable): 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
1213 variable() 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
1214 finally:
1215 cov.stop() 1BCDEFGHIJKLMNOPQRSTUVWXYZ01234
1217 def test_plugin_standalone(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1218 self.make_plugin_capitalized_testnames("plugin_tests.py") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1219 self.make_test_files() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1221 # Enable dynamic context plugin
1222 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1223 cov.set_option("run:plugins", ["plugin_tests"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1225 # Run the tests
1226 self.run_all_functions(cov, "testsuite") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1228 # Labeled coverage is collected
1229 data = cov.get_data() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1230 filenames = self.get_measured_filenames(data) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1231 expected = ["", "doctest:HTML_TAG", "test:HTML_TAG", "test:RENDERERS"] 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1232 assert expected == sorted(data.measured_contexts()) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1233 data.set_query_context("doctest:HTML_TAG") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1234 assert [2] == sorted_lines(data, filenames["rendering.py"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1235 data.set_query_context("test:HTML_TAG") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1236 assert [2] == sorted_lines(data, filenames["rendering.py"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1237 data.set_query_context("test:RENDERERS") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1238 assert [2, 5, 8, 11] == sorted_lines(data, filenames["rendering.py"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1240 def test_static_context(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1241 self.make_plugin_capitalized_testnames("plugin_tests.py") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1242 self.make_test_files() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1244 # Enable dynamic context plugin for coverage with named context
1245 cov = coverage.Coverage(context="mytests") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1246 cov.set_option("run:plugins", ["plugin_tests"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1248 # Run the tests
1249 self.run_all_functions(cov, "testsuite") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1251 # Static context prefix is preserved
1252 data = cov.get_data() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1253 expected = [ 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1254 "mytests",
1255 "mytests|doctest:HTML_TAG",
1256 "mytests|test:HTML_TAG",
1257 "mytests|test:RENDERERS",
1258 ]
1259 assert expected == sorted(data.measured_contexts()) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1261 def test_plugin_with_test_function(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1262 self.make_plugin_capitalized_testnames("plugin_tests.py") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1263 self.make_test_files() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1265 # Enable both a plugin and test_function dynamic context
1266 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1267 cov.set_option("run:plugins", ["plugin_tests"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1268 cov.set_option("run:dynamic_context", "test_function") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1270 # Run the tests
1271 self.run_all_functions(cov, "testsuite") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1273 # test_function takes precedence over plugins - only
1274 # functions that are not labeled by test_function are
1275 # labeled by plugin_tests.
1276 data = cov.get_data() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1277 filenames = self.get_measured_filenames(data) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1278 expected = [ 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1279 "",
1280 "doctest:HTML_TAG",
1281 "testsuite.test_html_tag",
1282 "testsuite.test_renderers",
1283 ]
1284 assert expected == sorted(data.measured_contexts()) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1286 def assert_context_lines(context: str, lines: list[TLineNo]) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1287 data.set_query_context(context) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1288 assert lines == sorted_lines(data, filenames["rendering.py"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1290 assert_context_lines("doctest:HTML_TAG", [2]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1291 assert_context_lines("testsuite.test_html_tag", [2]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1292 assert_context_lines("testsuite.test_renderers", [2, 5, 8, 11]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1294 def test_multiple_plugins(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234
1295 self.make_plugin_capitalized_testnames("plugin_tests.py") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1296 self.make_plugin_track_render("plugin_renderers.py") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1297 self.make_test_files() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1299 # Enable two plugins
1300 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1301 cov.set_option("run:plugins", ["plugin_renderers", "plugin_tests"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1303 self.run_all_functions(cov, "testsuite") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1305 # It is important to note, that line 11 (render_bold function) is never
1306 # labeled as renderer:bold context, because it is only called from
1307 # test_renderers function - so it already falls under test:RENDERERS
1308 # context.
1309 #
1310 # render_paragraph and render_span (lines 5, 8) are directly called by
1311 # testsuite.build_full_html, so they get labeled by renderers plugin.
1312 data = cov.get_data() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1313 filenames = self.get_measured_filenames(data) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1314 expected = [ 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1315 "",
1316 "doctest:HTML_TAG",
1317 "renderer:paragraph",
1318 "renderer:span",
1319 "test:HTML_TAG",
1320 "test:RENDERERS",
1321 ]
1322 assert expected == sorted(data.measured_contexts()) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1324 def assert_context_lines(context: str, lines: list[TLineNo]) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1325 data.set_query_context(context) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1326 assert lines == sorted_lines(data, filenames["rendering.py"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1328 assert_context_lines("test:HTML_TAG", [2]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1329 assert_context_lines("test:RENDERERS", [2, 5, 8, 11]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1330 assert_context_lines("doctest:HTML_TAG", [2]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1331 assert_context_lines("renderer:paragraph", [2, 5]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234
1332 assert_context_lines("renderer:span", [2, 8]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234