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

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 

3 

4"""Tests for plugins.""" 

5 

6from __future__ import annotations 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

7 

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

12 

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

15 

16import pytest 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

17 

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

25 

26import coverage.plugin 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

27 

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

31 

32 

33class NullConfig(TPluginConfig): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

34 """A plugin configure thing when we don't really need one.""" 

35 

36 def get_plugin_options(self, plugin: str) -> TConfigSectionOut: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

37 return {} # pragma: never called 

38 

39 

40class FakeConfig(TPluginConfig): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

41 """A fake config for use in tests.""" 

42 

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

47 

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

55 

56 

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

65 

66 

67class LoadPluginsTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

68 """Test Plugins construction.""" 

69 

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 

75 

76 class Plugin(CoveragePlugin): 

77 pass 

78 

79 def coverage_init(reg, options): 

80 reg.add_file_tracer(Plugin()) 

81 """, 

82 ) 

83 

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

87 

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

90 

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 

96 

97 class Plugin(CoveragePlugin): 

98 def __init__(self, options): 

99 self.options = options 

100 self.this_is = "me" 

101 

102 def coverage_init(reg, options): 

103 reg.add_file_tracer(Plugin(options)) 

104 """, 

105 ) 

106 

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

109 

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

114 

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 

120 

121 class Plugin(CoveragePlugin): 

122 def __init__(self, options): 

123 self.options = options 

124 self.this_is = "me" 

125 

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 

134 

135 class Plugin(CoveragePlugin): 

136 def __init__(self, options): 

137 self.options = options 

138 

139 def coverage_init(reg, options): 

140 reg.add_file_tracer(Plugin(options)) 

141 """, 

142 ) 

143 

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

146 

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

152 

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

156 

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

161 

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

165 

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

177 

178 

179class PluginTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

180 """Test plugins through the Coverage class.""" 

181 

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 ) 

196 

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

202 

203 with open("evidence.out", encoding="utf-8") as f: 1aBbCDcEdFGeHfIAJgKhLMiNjOPkQlRSmTnUVoWpXYqZr01234

204 assert f.read() == "we are here!" 1aBbCDcEdFGeHfIAJgKhLMiNjOPkQlRSmTnUVoWpXYqZr01234

205 

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

213 

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

222 

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 

228 

229 class Plugin(coverage.CoveragePlugin): 

230 def sys_info(self): 

231 return [("hello", "world")] 

232 

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

246 

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

259 

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 

265 

266 class Plugin(coverage.CoveragePlugin): 

267 pass 

268 

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

279 

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

288 

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

310 

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

315 

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

318 

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

322 

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

326 

327 assert called 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

328 

329 

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.""" 

333 

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

336 

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

339 

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!#$%'()

345 

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

351 

352 

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.""" 

356 

357 

358class GoodFileTracerTest(FileTracerTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

359 """Tests of file tracer plugin happy paths.""" 

360 

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 ) 

377 

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

382 

383 # Import the Python file, executing it. 

384 self.start_import_stop(cov, "simple") 1abscdtefAghuijzklvmnwopxqry

385 

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

392 

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) 

407 

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 

419 

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) 

423 

424 assert helper(42) == 43 

425 assert render("bar_4.html", 2) == "[bar_4.html @ 2]" 

426 assert helper(76) == 77 

427 

428 # quux_5.html will be omitted from the results. 

429 assert render("quux_5.html", 3) == "[quux_5.html @ 3]" 

430 """, 

431 ) 

432 

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

438 

439 self.make_file("bar_4.html", lines(4)) 1abscdtefAghuijzklvmnwopxqry

440 self.make_file("foo_7.html", lines(7)) 1abscdtefAghuijzklvmnwopxqry

441 

442 def test_plugin2(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

443 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry

444 

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

449 

450 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry

451 

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

459 

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

464 

465 assert "quux_5.html" not in line_counts(cov.get_data()) 1abscdtefghuijzklmnopqr

466 

467 def test_plugin2_with_branch(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

468 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry

469 

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

474 

475 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry

476 

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

485 

486 assert analysis.missing == {1, 2, 3, 6, 7} 1abscdtefAghuijzklvmnwopxqry

487 

488 def test_plugin2_with_text_report(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

489 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry

490 

491 cov = coverage.Coverage(branch=True, omit=["*quux*"]) 1abscdtefAghuijzklvmnwopxqry

492 cov.set_option("run:plugins", ["tests.plugin2"]) 1abscdtefAghuijzklvmnwopxqry

493 

494 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry

495 

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

509 

510 def test_plugin2_with_html_report(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

511 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry

512 

513 cov = coverage.Coverage(branch=True, omit=["*quux*"]) 1abscdtefAghuijzklvmnwopxqry

514 cov.set_option("run:plugins", ["tests.plugin2"]) 1abscdtefAghuijzklvmnwopxqry

515 

516 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry

517 

518 total = cov.html_report(include=["*.html"], omit=["uni*.html"]) 1abscdtefAghuijzklvmnwopxqry

519 assert math.isclose(total, 4 / 11 * 100) 1abscdtefAghuijzklvmnwopxqry

520 

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

524 

525 def test_plugin2_with_xml_report(self) -> None: 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

526 self.make_render_and_caller() 1abscdtefAghuijzklvmnwopxqry

527 

528 cov = coverage.Coverage(branch=True, omit=["*quux*"]) 1abscdtefAghuijzklvmnwopxqry

529 cov.set_option("run:plugins", ["tests.plugin2"]) 1abscdtefAghuijzklvmnwopxqry

530 

531 self.start_import_stop(cov, "caller") 1abscdtefAghuijzklvmnwopxqry

532 

533 total = cov.xml_report(include=["*.html"], omit=["uni*.html"]) 1abscdtefAghuijzklvmnwopxqry

534 assert math.isclose(total, 4 / 11 * 100) 1abscdtefAghuijzklvmnwopxqry

535 

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

540 

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 } 

555 

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" 

570 

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) 

582 

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

601 

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

614 

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"] 

633 

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) 

641 

642 class MyReporter(coverage.FileReporter): 

643 def lines(self): 

644 return {99, 999, 9999} 

645 

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

654 

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

659 

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

664 

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

669 

670 

671class BadFileTracerTest(FileTracerTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

672 """Test error handling around file tracer plugins.""" 

673 

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. 

676 

677 Uses a few fixed Python files. 

678 

679 Returns the Coverage object. 

680 

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 ) 

708 

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

714 

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. 

724 

725 `module_name` and `plugin_name` is the module and name of the plugin to 

726 use. 

727 

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. 

730 

731 `excmsg`, if provided, is text that must appear in the stderr. 

732 

733 `excmsgs`, if provided, is a list of messages, one of which must 

734 appear in the stderr. 

735 

736 The plugin will be disabled, and we check that a warning is output 

737 explaining why. 

738 

739 """ 

740 with pytest.warns(Warning) as warns: 1abscdtefAghuijzklvmnwopxqry

741 self.run_plugin(module_name) 1abscdtefAghuijzklvmnwopxqry

742 

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

748 

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

759 

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

765 

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 

772 

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

778 

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() 

788 

789 class FileTracer(coverage.FileTracer): 

790 pass 

791 

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 ) 

802 

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() 

812 

813 class FileTracer(coverage.FileTracer): 

814 def source_filename(self): 

815 return "foo.xxx" 

816 

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

825 

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! 

834 

835 def coverage_init(reg, options): 

836 reg.add_file_tracer(Plugin()) 

837 """, 

838 ) 

839 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry

840 

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 

852 

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! 

860 

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 

868 

869 def coverage_init(reg, options): 

870 reg.add_file_tracer(Plugin()) 

871 """, 

872 ) 

873 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry

874 

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 

883 

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 ) 

894 

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() 

903 

904 class BadFileTracer(coverage.plugin.FileTracer): 

905 def has_dynamic_source_filename(self): 

906 23/0 # Oh noes! 

907 

908 def coverage_init(reg, options): 

909 reg.add_file_tracer(Plugin()) 

910 """, 

911 ) 

912 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry

913 

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() 

922 

923 class BadFileTracer(coverage.plugin.FileTracer): 

924 def source_filename(self): 

925 42/0 # Oh noes! 

926 

927 def coverage_init(reg, options): 

928 reg.add_file_tracer(Plugin()) 

929 """, 

930 ) 

931 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry

932 

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() 

941 

942 class BadFileTracer(coverage.plugin.FileTracer): 

943 def source_filename(self): 

944 return 17.3 

945 

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 ) 

961 

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() 

971 

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! 

977 

978 def coverage_init(reg, options): 

979 reg.add_file_tracer(Plugin()) 

980 """, 

981 ) 

982 self.run_bad_plugin("bad_plugin", "Plugin") 1abscdtefAghuijzklvmnwopxqry

983 

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() 

993 

994 class BadFileTracer(coverage.plugin.FileTracer): 

995 def source_filename(self): 

996 return "something.foo" 

997 

998 def line_number_range(self, frame): 

999 raise Exception("borked!") 

1000 

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 ) 

1011 

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() 

1021 

1022 class BadFileTracer(coverage.plugin.FileTracer): 

1023 def source_filename(self): 

1024 return "something.foo" 

1025 

1026 def line_number_range(self, frame): 

1027 return 42.23 

1028 

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 ) 

1039 

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() 

1049 

1050 class BadFileTracer(coverage.plugin.FileTracer): 

1051 def source_filename(self): 

1052 return "something.foo" 

1053 

1054 def line_number_range(self, frame): 

1055 return (1, 2, 3) 

1056 

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 ) 

1067 

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() 

1077 

1078 class BadFileTracer(coverage.plugin.FileTracer): 

1079 def source_filename(self): 

1080 return "something.foo" 

1081 

1082 def line_number_range(self, frame): 

1083 return ("5", "7") 

1084 

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 ) 

1098 

1099 

1100class ConfigurerPluginTest(CoverageTest): 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

1101 """Test configuring plugins.""" 

1102 

1103 run_in_temp_dir = False 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQ5lR6vS7mT8nU9wV!oW#pX$xY%qZ'r0(y1)234

1104 

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

1114 

1115 

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`.""" 

1119 

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 

1126 

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 

1134 

1135 def coverage_init(reg, options): 

1136 reg.add_dynamic_context(Plugin()) 

1137 """, 

1138 ) 

1139 

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 

1146 

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 

1153 

1154 def coverage_init(reg, options): 

1155 reg.add_dynamic_context(Plugin()) 

1156 """, 

1157 ) 

1158 

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}>' 

1166 

1167 def render_paragraph(text): 

1168 return html_tag('p', text) 

1169 

1170 def render_span(text): 

1171 return html_tag('span', text) 

1172 

1173 def render_bold(text): 

1174 return html_tag('b', text) 

1175 """, 

1176 ) 

1177 

1178 self.make_file( 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1179 "testsuite.py", 

1180 """\ 

1181 import rendering 

1182 

1183 def test_html_tag() -> None: 

1184 assert rendering.html_tag('b', 'hello') == '<b>hello</b>' 

1185 

1186 def doctest_html_tag(): 

1187 assert eval(''' 

1188 rendering.html_tag('i', 'text') == '<i>text</i>' 

1189 '''.strip()) 

1190 

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>' 

1195 

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 ) 

1203 

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

1216 

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

1220 

1221 # Enable dynamic context plugin 

1222 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1223 cov.set_option("run:plugins", ["plugin_tests"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1224 

1225 # Run the tests 

1226 self.run_all_functions(cov, "testsuite") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1227 

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

1239 

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

1243 

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

1247 

1248 # Run the tests 

1249 self.run_all_functions(cov, "testsuite") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1250 

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

1260 

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

1264 

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

1269 

1270 # Run the tests 

1271 self.run_all_functions(cov, "testsuite") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1272 

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

1285 

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

1289 

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

1293 

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

1298 

1299 # Enable two plugins 

1300 cov = coverage.Coverage() 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1301 cov.set_option("run:plugins", ["plugin_renderers", "plugin_tests"]) 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1302 

1303 self.run_all_functions(cov, "testsuite") 1aBbCsDcEdFtGeHfIAJgKhLuMiNjOzPkQlRvSmTnUwVoWpXxYqZr0y1234

1304 

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

1323 

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

1327 

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