Coverage for coverage / jsonreport.py: 100.000%
82 statements
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-29 20:34 +0000
« prev ^ index » next coverage.py v7.12.1a0.dev1, created at 2025-11-29 20:34 +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 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
8import datetime 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
9import json 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
10import sys 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
11from collections.abc import Iterable 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
12from typing import IO, TYPE_CHECKING, Any 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
14from coverage import __version__ 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
15from coverage.report_core import get_analysis_to_report 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
16from coverage.results import Analysis, AnalysisNarrower, Numbers 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
17from coverage.types import TLineNo, TMorf 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
19if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
34class JsonReporter: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
35 """A reporter for writing JSON coverage results."""
37 report_type = "JSON report" 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
39 def __init__(self, coverage: Coverage) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
40 self.coverage = coverage 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
41 self.config = self.coverage.config 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
42 self.total = Numbers(self.config.precision) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
43 self.report_data: JsonObj = {} 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
45 def make_summary(self, nums: Numbers) -> JsonObj: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
46 """Create a dict summarizing `nums`."""
47 return { 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
59 """Create a dict summarizing the branch info in `nums`."""
60 return { 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
78 coverage_data = self.coverage.get_data() 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
79 coverage_data.set_query_contexts(self.config.report_contexts) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
80 self.report_data["meta"] = { 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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 = {} 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
89 for file_reporter, analysis in get_analysis_to_report(self.coverage, morfs): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
90 measured_files[file_reporter.relative_filename()] = self.report_one_file( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
91 coverage_data,
92 analysis,
93 file_reporter,
94 )
96 self.report_data["files"] = measured_files 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
97 self.report_data["totals"] = self.make_summary(self.total) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
99 if coverage_data.has_arcs(): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
100 self.report_data["totals"].update(self.make_branch_summary(self.total)) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
102 json.dump( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
110 def report_one_file( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
115 self.total += nums 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
116 summary = self.make_summary(nums) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
117 reported_file: JsonObj = { 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
124 reported_file["contexts"] = coverage_data.contexts_by_lineno(analysis.filename) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV(WXYZ012345678
125 if coverage_data.has_arcs(): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
126 summary.update(self.make_branch_summary(nums)) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
127 reported_file["executed_branches"] = list( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
128 _convert_branch_arcs(analysis.executed_branch_arcs()),
129 )
130 reported_file["missing_branches"] = list( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
131 _convert_branch_arcs(analysis.missing_branch_arcs()),
132 )
134 num_lines = len(file_reporter.source().splitlines()) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
135 regions = file_reporter.code_regions() 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
136 for noun, plural in file_reporter.code_region_kinds(): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
137 outside_lines = set(range(1, num_lines + 1)) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
138 for region in regions: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
139 if region.kind != noun: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UVWX'YZ012345678
140 continue 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UVWX'YZ012345678
141 outside_lines -= region.lines 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UVWX'YZ012345678
143 narrower = AnalysisNarrower(analysis) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
144 narrower.add_regions(r.lines for r in regions if r.kind == noun) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
145 narrower.add_regions([outside_lines]) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
147 reported_file[plural] = region_data = {} 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
148 for region in regions: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
149 if region.kind != noun: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UVWX'YZ012345678
150 continue 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UVWX'YZ012345678
151 region_data[region.name] = self.make_region_data( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UVWX'YZ012345678
152 coverage_data,
153 narrower.narrow(region.lines),
154 )
156 region_data[""] = self.make_region_data( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
157 coverage_data,
158 narrower.narrow(outside_lines),
159 )
160 return reported_file 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
162 def make_region_data(self, coverage_data: CoverageData, narrowed_analysis: Analysis) -> JsonObj: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
163 """Create the data object for one region of a file."""
164 narrowed_nums = narrowed_analysis.numbers 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
165 narrowed_summary = self.make_summary(narrowed_nums) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
166 this_region = { 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
173 contexts = coverage_data.contexts_by_lineno(narrowed_analysis.filename) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV(WXYZ012345678
174 this_region["contexts"] = contexts 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUV(WXYZ012345678
175 if coverage_data.has_arcs(): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
176 narrowed_summary.update(self.make_branch_summary(narrowed_nums)) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
177 this_region["executed_branches"] = list( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
178 _convert_branch_arcs(narrowed_analysis.executed_branch_arcs()),
179 )
180 this_region["missing_branches"] = list( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
181 _convert_branch_arcs(narrowed_analysis.missing_branch_arcs()),
182 )
183 return this_region 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
186def _convert_branch_arcs( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
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(): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
191 for target in targets: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678
192 yield source, target 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKL9MN!OP#QR$ST%UV(WX'YZ)012345678