Skip to content

Commit

Permalink
feat: support regions (functions, classes) in JSON reports.
Browse files Browse the repository at this point in the history
  • Loading branch information
devdanzin authored and nedbat committed Jul 11, 2024
1 parent 84d9d3e commit dba6406
Show file tree
Hide file tree
Showing 2 changed files with 189 additions and 7 deletions.
47 changes: 45 additions & 2 deletions coverage/jsonreport.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
if TYPE_CHECKING:
from coverage import Coverage
from coverage.data import CoverageData
from coverage.plugin import FileReporter


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

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

return self.total.n_statements and self.total.pc_covered

def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> dict[str, Any]:
def report_one_file(
self, coverage_data: CoverageData, analysis: Analysis, file_reporter: FileReporter
) -> dict[str, Any]:
"""Extract the relevant report data for a single file."""
nums = analysis.numbers
self.total += nums
Expand All @@ -101,7 +105,7 @@ def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> di
"missing_lines": nums.n_missing,
"excluded_lines": nums.n_excluded,
}
reported_file = {
reported_file: dict[str, Any] = {
"executed_lines": sorted(analysis.executed),
"summary": summary,
"missing_lines": sorted(analysis.missing),
Expand All @@ -122,6 +126,45 @@ def report_one_file(self, coverage_data: CoverageData, analysis: Analysis) -> di
reported_file["missing_branches"] = list(
_convert_branch_arcs(analysis.missing_branch_arcs()),
)

for region in file_reporter.code_regions():
if region.kind not in reported_file:
reported_file[region.kind] = {}
num_lines = len(file_reporter.source().splitlines())
outside_lines = set(range(1, num_lines + 1))
outside_lines -= region.lines
narrowed_analysis = analysis.narrow(region.lines)
narrowed_nums = narrowed_analysis.numbers
narrowed_summary = {
"covered_lines": narrowed_nums.n_executed,
"num_statements": narrowed_nums.n_statements,
"percent_covered": narrowed_nums.pc_covered,
"percent_covered_display": narrowed_nums.pc_covered_str,
"missing_lines": narrowed_nums.n_missing,
"excluded_lines": narrowed_nums.n_excluded,
}
reported_file[region.kind][region.name] = {
"executed_lines": sorted(narrowed_analysis.executed),
"summary": narrowed_summary,
"missing_lines": sorted(narrowed_analysis.missing),
"excluded_lines": sorted(narrowed_analysis.excluded),
}
if self.config.json_show_contexts:
contexts = coverage_data.contexts_by_lineno(narrowed_analysis.filename)
reported_file[region.kind][region.name]["contexts"] = contexts
if coverage_data.has_arcs():
narrowed_summary.update({
"num_branches": narrowed_nums.n_branches,
"num_partial_branches": narrowed_nums.n_partial_branches,
"covered_branches": narrowed_nums.n_executed_branches,
"missing_branches": narrowed_nums.n_missing_branches,
})
reported_file[region.kind][region.name]["executed_branches"] = list(
_convert_branch_arcs(narrowed_analysis.executed_branch_arcs()),
)
reported_file[region.kind][region.name]["missing_branches"] = list(
_convert_branch_arcs(narrowed_analysis.missing_branch_arcs()),
)
return reported_file


Expand Down
149 changes: 144 additions & 5 deletions tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@ def _assert_expected_json_report(
expected_result: dict[str, Any],
) -> None:
"""
Helper that handles common ceremonies so tests can clearly show the
consequences of setting various arguments.
Helper that creates an example file for most tests.
"""
self.make_file("a.py", """\
a = {'b': 1}
Expand All @@ -41,9 +40,47 @@ def _assert_expected_json_report(
if not a:
b = 4
""")
a = self.start_import_stop(cov, "a")
output_path = os.path.join(self.temp_dir, "a.json")
cov.json_report(a, outfile=output_path)
self._compare_json_reports(cov, expected_result, "a")

def _assert_expected_json_report_with_regions(
self,
cov: Coverage,
expected_result: dict[str, Any],
) -> None:
"""
Helper that creates an example file for regions tests.
"""
self.make_file("b.py", """\
a = {'b': 1}
def c():
return 1
class C:
pass
class D:
def e(self):
return 2
def f(self):
return 3
""")
self._compare_json_reports(cov, expected_result, "b")

def _compare_json_reports(
self,
cov: Coverage,
expected_result: dict[str, Any],
mod_name: str,
) -> None:
"""
Helper that handles common ceremonies, comparing JSON reports that
it creates to expected results, so tests can clearly show the
consequences of setting various arguments.
"""
mod = self.start_import_stop(cov, mod_name)
output_path = os.path.join(self.temp_dir, f"{mod_name}.json")
cov.json_report(mod, outfile=output_path)
with open(output_path) as result_file:
parsed_result = json.load(result_file)
self.assert_recent_datetime(
Expand Down Expand Up @@ -140,6 +177,108 @@ def test_simple_line_coverage(self) -> None:
}
self._assert_expected_json_report(cov, expected_result)

def test_regions_coverage(self) -> None:
cov = coverage.Coverage()
expected_result = {
"meta": {
"branch_coverage": False,
"show_contexts": False
},
"files": {
"b.py": {
"executed_lines": [1, 3, 6, 7, 9, 10, 12],
"summary": {
"covered_lines": 7,
"num_statements": 10,
"percent_covered": 70.0,
"percent_covered_display": "70",
"missing_lines": 3,
"excluded_lines": 0
},
"missing_lines": [4, 11, 13],
"excluded_lines": [],
"function": {
"c": {
"executed_lines": [],
"summary": {
"covered_lines": 0,
"num_statements": 1,
"percent_covered": 0.0,
"percent_covered_display": "0",
"missing_lines": 1,
"excluded_lines": 0
},
"missing_lines": [4],
"excluded_lines": []
},
"D.e": {
"executed_lines": [],
"summary": {
"covered_lines": 0,
"num_statements": 1,
"percent_covered": 0.0,
"percent_covered_display": "0",
"missing_lines": 1,
"excluded_lines": 0
},
"missing_lines": [11],
"excluded_lines": []
},
"D.f": {
"executed_lines": [],
"summary": {
"covered_lines": 0,
"num_statements": 1,
"percent_covered": 0.0,
"percent_covered_display": "0",
"missing_lines": 1,
"excluded_lines": 0
},
"missing_lines": [13],
"excluded_lines": []
}
},
"class": {
"C": {
"executed_lines": [],
"summary": {
"covered_lines": 0,
"num_statements": 0,
"percent_covered": 100.0,
"percent_covered_display": "100",
"missing_lines": 0,
"excluded_lines": 0
},
"missing_lines": [],
"excluded_lines": []
},
"D": {
"executed_lines": [],
"summary": {
"covered_lines": 0,
"num_statements": 2,
"percent_covered": 0.0,
"percent_covered_display": "0",
"missing_lines": 2,
"excluded_lines": 0
},
"missing_lines": [11, 13],
"excluded_lines": []
}
}
}
},
"totals": {
"covered_lines": 7,
"num_statements": 10,
"percent_covered": 70.0,
"percent_covered_display": "70",
"missing_lines": 3,
"excluded_lines": 0
}
}
self._assert_expected_json_report_with_regions(cov, expected_result)

def run_context_test(self, relative_files: bool) -> None:
"""A helper for two tests below."""
self.make_file("config", f"""\
Expand Down

0 comments on commit dba6406

Please sign in to comment.