Skip to content

Commit dba6406

Browse files
devdanzinnedbat
authored andcommitted
feat: support regions (functions, classes) in JSON reports.
1 parent 84d9d3e commit dba6406

File tree

2 files changed

+189
-7
lines changed

2 files changed

+189
-7
lines changed

coverage/jsonreport.py

+45-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
if TYPE_CHECKING:
2020
from coverage import Coverage
2121
from coverage.data import CoverageData
22+
from coverage.plugin import FileReporter
2223

2324

2425
# "Version 1" had no format number at all.
@@ -60,6 +61,7 @@ def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str]) -> float:
6061
measured_files[file_reporter.relative_filename()] = self.report_one_file(
6162
coverage_data,
6263
analysis,
64+
file_reporter,
6365
)
6466

6567
self.report_data["files"] = measured_files
@@ -89,7 +91,9 @@ def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str]) -> float:
8991

9092
return self.total.n_statements and self.total.pc_covered
9193

92-
def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> dict[str, Any]:
94+
def report_one_file(
95+
self, coverage_data: CoverageData, analysis: Analysis, file_reporter: FileReporter
96+
) -> dict[str, Any]:
9397
"""Extract the relevant report data for a single file."""
9498
nums = analysis.numbers
9599
self.total += nums
@@ -101,7 +105,7 @@ def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> di
101105
"missing_lines": nums.n_missing,
102106
"excluded_lines": nums.n_excluded,
103107
}
104-
reported_file = {
108+
reported_file: dict[str, Any] = {
105109
"executed_lines": sorted(analysis.executed),
106110
"summary": summary,
107111
"missing_lines": sorted(analysis.missing),
@@ -122,6 +126,45 @@ def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> di
122126
reported_file["missing_branches"] = list(
123127
_convert_branch_arcs(analysis.missing_branch_arcs()),
124128
)
129+
130+
for region in file_reporter.code_regions():
131+
if region.kind not in reported_file:
132+
reported_file[region.kind] = {}
133+
num_lines = len(file_reporter.source().splitlines())
134+
outside_lines = set(range(1, num_lines + 1))
135+
outside_lines -= region.lines
136+
narrowed_analysis = analysis.narrow(region.lines)
137+
narrowed_nums = narrowed_analysis.numbers
138+
narrowed_summary = {
139+
"covered_lines": narrowed_nums.n_executed,
140+
"num_statements": narrowed_nums.n_statements,
141+
"percent_covered": narrowed_nums.pc_covered,
142+
"percent_covered_display": narrowed_nums.pc_covered_str,
143+
"missing_lines": narrowed_nums.n_missing,
144+
"excluded_lines": narrowed_nums.n_excluded,
145+
}
146+
reported_file[region.kind][region.name] = {
147+
"executed_lines": sorted(narrowed_analysis.executed),
148+
"summary": narrowed_summary,
149+
"missing_lines": sorted(narrowed_analysis.missing),
150+
"excluded_lines": sorted(narrowed_analysis.excluded),
151+
}
152+
if self.config.json_show_contexts:
153+
contexts = coverage_data.contexts_by_lineno(narrowed_analysis.filename)
154+
reported_file[region.kind][region.name]["contexts"] = contexts
155+
if coverage_data.has_arcs():
156+
narrowed_summary.update({
157+
"num_branches": narrowed_nums.n_branches,
158+
"num_partial_branches": narrowed_nums.n_partial_branches,
159+
"covered_branches": narrowed_nums.n_executed_branches,
160+
"missing_branches": narrowed_nums.n_missing_branches,
161+
})
162+
reported_file[region.kind][region.name]["executed_branches"] = list(
163+
_convert_branch_arcs(narrowed_analysis.executed_branch_arcs()),
164+
)
165+
reported_file[region.kind][region.name]["missing_branches"] = list(
166+
_convert_branch_arcs(narrowed_analysis.missing_branch_arcs()),
167+
)
125168
return reported_file
126169

127170

tests/test_json.py

+144-5
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,7 @@ def _assert_expected_json_report(
2727
expected_result: dict[str, Any],
2828
) -> None:
2929
"""
30-
Helper that handles common ceremonies so tests can clearly show the
31-
consequences of setting various arguments.
30+
Helper that creates an example file for most tests.
3231
"""
3332
self.make_file("a.py", """\
3433
a = {'b': 1}
@@ -41,9 +40,47 @@ def _assert_expected_json_report(
4140
if not a:
4241
b = 4
4342
""")
44-
a = self.start_import_stop(cov, "a")
45-
output_path = os.path.join(self.temp_dir, "a.json")
46-
cov.json_report(a, outfile=output_path)
43+
self._compare_json_reports(cov, expected_result, "a")
44+
45+
def _assert_expected_json_report_with_regions(
46+
self,
47+
cov: Coverage,
48+
expected_result: dict[str, Any],
49+
) -> None:
50+
"""
51+
Helper that creates an example file for regions tests.
52+
"""
53+
self.make_file("b.py", """\
54+
a = {'b': 1}
55+
56+
def c():
57+
return 1
58+
59+
class C:
60+
pass
61+
62+
class D:
63+
def e(self):
64+
return 2
65+
def f(self):
66+
return 3
67+
""")
68+
self._compare_json_reports(cov, expected_result, "b")
69+
70+
def _compare_json_reports(
71+
self,
72+
cov: Coverage,
73+
expected_result: dict[str, Any],
74+
mod_name: str,
75+
) -> None:
76+
"""
77+
Helper that handles common ceremonies, comparing JSON reports that
78+
it creates to expected results, so tests can clearly show the
79+
consequences of setting various arguments.
80+
"""
81+
mod = self.start_import_stop(cov, mod_name)
82+
output_path = os.path.join(self.temp_dir, f"{mod_name}.json")
83+
cov.json_report(mod, outfile=output_path)
4784
with open(output_path) as result_file:
4885
parsed_result = json.load(result_file)
4986
self.assert_recent_datetime(
@@ -140,6 +177,108 @@ def test_simple_line_coverage(self) -> None:
140177
}
141178
self._assert_expected_json_report(cov, expected_result)
142179

180+
def test_regions_coverage(self) -> None:
181+
cov = coverage.Coverage()
182+
expected_result = {
183+
"meta": {
184+
"branch_coverage": False,
185+
"show_contexts": False
186+
},
187+
"files": {
188+
"b.py": {
189+
"executed_lines": [1, 3, 6, 7, 9, 10, 12],
190+
"summary": {
191+
"covered_lines": 7,
192+
"num_statements": 10,
193+
"percent_covered": 70.0,
194+
"percent_covered_display": "70",
195+
"missing_lines": 3,
196+
"excluded_lines": 0
197+
},
198+
"missing_lines": [4, 11, 13],
199+
"excluded_lines": [],
200+
"function": {
201+
"c": {
202+
"executed_lines": [],
203+
"summary": {
204+
"covered_lines": 0,
205+
"num_statements": 1,
206+
"percent_covered": 0.0,
207+
"percent_covered_display": "0",
208+
"missing_lines": 1,
209+
"excluded_lines": 0
210+
},
211+
"missing_lines": [4],
212+
"excluded_lines": []
213+
},
214+
"D.e": {
215+
"executed_lines": [],
216+
"summary": {
217+
"covered_lines": 0,
218+
"num_statements": 1,
219+
"percent_covered": 0.0,
220+
"percent_covered_display": "0",
221+
"missing_lines": 1,
222+
"excluded_lines": 0
223+
},
224+
"missing_lines": [11],
225+
"excluded_lines": []
226+
},
227+
"D.f": {
228+
"executed_lines": [],
229+
"summary": {
230+
"covered_lines": 0,
231+
"num_statements": 1,
232+
"percent_covered": 0.0,
233+
"percent_covered_display": "0",
234+
"missing_lines": 1,
235+
"excluded_lines": 0
236+
},
237+
"missing_lines": [13],
238+
"excluded_lines": []
239+
}
240+
},
241+
"class": {
242+
"C": {
243+
"executed_lines": [],
244+
"summary": {
245+
"covered_lines": 0,
246+
"num_statements": 0,
247+
"percent_covered": 100.0,
248+
"percent_covered_display": "100",
249+
"missing_lines": 0,
250+
"excluded_lines": 0
251+
},
252+
"missing_lines": [],
253+
"excluded_lines": []
254+
},
255+
"D": {
256+
"executed_lines": [],
257+
"summary": {
258+
"covered_lines": 0,
259+
"num_statements": 2,
260+
"percent_covered": 0.0,
261+
"percent_covered_display": "0",
262+
"missing_lines": 2,
263+
"excluded_lines": 0
264+
},
265+
"missing_lines": [11, 13],
266+
"excluded_lines": []
267+
}
268+
}
269+
}
270+
},
271+
"totals": {
272+
"covered_lines": 7,
273+
"num_statements": 10,
274+
"percent_covered": 70.0,
275+
"percent_covered_display": "70",
276+
"missing_lines": 3,
277+
"excluded_lines": 0
278+
}
279+
}
280+
self._assert_expected_json_report_with_regions(cov, expected_result)
281+
143282
def run_context_test(self, relative_files: bool) -> None:
144283
"""A helper for two tests below."""
145284
self.make_file("config", f"""\

0 commit comments

Comments
 (0)