Coverage for coverage / jsonreport.py: 100.000%
82 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"""Json reporting for coverage.py"""
6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
8import datetime 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
9import json 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
10import sys 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
11from collections.abc import Iterable 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
12from typing import IO, TYPE_CHECKING, Any 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
14from coverage import __version__ 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
15from coverage.report_core import get_analysis_to_report 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
16from coverage.results import Analysis, AnalysisNarrower, Numbers 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
17from coverage.types import TLineNo, TMorf 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
19if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
20 from coverage import Coverage
21 from coverage.data import CoverageData
22 from coverage.plugin import FileReporter
25# A type for data that can be JSON-serialized.
26JsonObj = dict[str, Any] 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
28# "Version 1" had no format number at all.
29# 2: add the meta.format field.
30# 3: add region information (functions, classes)
31FORMAT_VERSION = 3 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
34class JsonReporter: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
35 """A reporter for writing JSON coverage results."""
37 report_type = "JSON report" 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
39 def __init__(self, coverage: Coverage) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
40 self.coverage = coverage 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
41 self.config = self.coverage.config 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
42 self.total = Numbers(self.config.precision) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
43 self.report_data: JsonObj = {} 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
45 def make_summary(self, nums: Numbers) -> JsonObj: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
46 """Create a dict summarizing `nums`."""
47 return { 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
48 "covered_lines": nums.n_executed,
49 "num_statements": nums.n_statements,
50 "percent_covered": nums.pc_covered,
51 "percent_covered_display": nums.pc_covered_str,
52 "missing_lines": nums.n_missing,
53 "excluded_lines": nums.n_excluded,
54 "percent_statements_covered": nums.pc_statements,
55 "percent_statements_covered_display": nums.pc_statements_str,
56 }
58 def make_branch_summary(self, nums: Numbers) -> JsonObj: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
59 """Create a dict summarizing the branch info in `nums`."""
60 return { 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
61 "num_branches": nums.n_branches,
62 "num_partial_branches": nums.n_partial_branches,
63 "covered_branches": nums.n_executed_branches,
64 "missing_branches": nums.n_missing_branches,
65 "percent_branches_covered": nums.pc_branches,
66 "percent_branches_covered_display": nums.pc_branches_str,
67 }
69 def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str]) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
70 """Generate a json report for `morfs`.
72 `morfs` is a list of modules or file names.
74 `outfile` is a file object to write the json to.
76 """
77 outfile = outfile or sys.stdout 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
78 coverage_data = self.coverage.get_data() 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
79 coverage_data.set_query_contexts(self.config.report_contexts) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
80 self.report_data["meta"] = { 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
81 "format": FORMAT_VERSION,
82 "version": __version__,
83 "timestamp": datetime.datetime.now().isoformat(),
84 "branch_coverage": coverage_data.has_arcs(),
85 "show_contexts": self.config.json_show_contexts,
86 }
88 measured_files = {} 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
89 for file_reporter, analysis in get_analysis_to_report(self.coverage, morfs): 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
90 measured_files[file_reporter.relative_filename()] = self.report_one_file( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
91 coverage_data,
92 analysis,
93 file_reporter,
94 )
96 self.report_data["files"] = measured_files 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
97 self.report_data["totals"] = self.make_summary(self.total) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
99 if coverage_data.has_arcs(): 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
100 self.report_data["totals"].update(self.make_branch_summary(self.total)) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
102 json.dump( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
103 self.report_data,
104 outfile,
105 indent=(4 if self.config.json_pretty_print else None),
106 )
108 return self.total.n_statements and self.total.pc_covered 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
110 def report_one_file( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
111 self, coverage_data: CoverageData, analysis: Analysis, file_reporter: FileReporter
112 ) -> JsonObj:
113 """Extract the relevant report data for a single file."""
114 nums = analysis.numbers 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
115 self.total += nums 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
116 summary = self.make_summary(nums) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
117 reported_file: JsonObj = { 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
118 "executed_lines": sorted(analysis.executed),
119 "summary": summary,
120 "missing_lines": sorted(analysis.missing),
121 "excluded_lines": sorted(analysis.excluded),
122 }
123 if self.config.json_show_contexts: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
124 reported_file["contexts"] = coverage_data.contexts_by_lineno(analysis.filename) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL'MNOP(QRSTUVWXYZ01)234
125 if coverage_data.has_arcs(): 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
126 summary.update(self.make_branch_summary(nums)) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
127 reported_file["executed_branches"] = list( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
128 _convert_branch_arcs(analysis.executed_branch_arcs()),
129 )
130 reported_file["missing_branches"] = list( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
131 _convert_branch_arcs(analysis.missing_branch_arcs()),
132 )
134 num_lines = len(file_reporter.source().splitlines()) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
135 regions = file_reporter.code_regions() 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
136 for noun, plural in file_reporter.code_region_kinds(): 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
137 outside_lines = set(range(1, num_lines + 1)) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
138 for region in regions: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
139 if region.kind != noun: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KLMN8OPQR9ST!UV#WX$YZ%01234
140 continue 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KLMN8OPQR9ST!UV#WX$YZ%01234
141 outside_lines -= region.lines 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KLMN8OPQR9ST!UV#WX$YZ%01234
143 narrower = AnalysisNarrower(analysis) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
144 narrower.add_regions(r.lines for r in regions if r.kind == noun) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
145 narrower.add_regions([outside_lines]) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
147 reported_file[plural] = region_data = {} 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
148 for region in regions: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
149 if region.kind != noun: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KLMN8OPQR9ST!UV#WX$YZ%01234
150 continue 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KLMN8OPQR9ST!UV#WX$YZ%01234
151 region_data[region.name] = self.make_region_data( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KLMN8OPQR9ST!UV#WX$YZ%01234
152 coverage_data,
153 narrower.narrow(region.lines),
154 )
156 region_data[""] = self.make_region_data( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
157 coverage_data,
158 narrower.narrow(outside_lines),
159 )
160 return reported_file 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
162 def make_region_data(self, coverage_data: CoverageData, narrowed_analysis: Analysis) -> JsonObj: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
163 """Create the data object for one region of a file."""
164 narrowed_nums = narrowed_analysis.numbers 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
165 narrowed_summary = self.make_summary(narrowed_nums) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
166 this_region = { 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
167 "executed_lines": sorted(narrowed_analysis.executed),
168 "summary": narrowed_summary,
169 "missing_lines": sorted(narrowed_analysis.missing),
170 "excluded_lines": sorted(narrowed_analysis.excluded),
171 }
172 if self.config.json_show_contexts: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
173 contexts = coverage_data.contexts_by_lineno(narrowed_analysis.filename) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL'MNOP(QRSTUVWXYZ01)234
174 this_region["contexts"] = contexts 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL'MNOP(QRSTUVWXYZ01)234
175 if coverage_data.has_arcs(): 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
176 narrowed_summary.update(self.make_branch_summary(narrowed_nums)) 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
177 this_region["executed_branches"] = list( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
178 _convert_branch_arcs(narrowed_analysis.executed_branch_arcs()),
179 )
180 this_region["missing_branches"] = list( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
181 _convert_branch_arcs(narrowed_analysis.missing_branch_arcs()),
182 )
183 return this_region 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
186def _convert_branch_arcs( 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
187 branch_arcs: dict[TLineNo, list[TLineNo]],
188) -> Iterable[tuple[TLineNo, TLineNo]]:
189 """Convert branch arcs to a list of two-element tuples."""
190 for source, targets in branch_arcs.items(): 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
191 for target in targets: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234
192 yield source, target 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234