diff --git a/coverage/jsonreport.py b/coverage/jsonreport.py index 9e515c202..18b05b3ac 100644 --- a/coverage/jsonreport.py +++ b/coverage/jsonreport.py @@ -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. @@ -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 @@ -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 @@ -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), @@ -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 diff --git a/tests/test_json.py b/tests/test_json.py index c5ef71cb8..b221bc167 100644 --- a/tests/test_json.py +++ b/tests/test_json.py @@ -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} @@ -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( @@ -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"""\