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

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 

3 

4"""Json reporting for coverage.py""" 

5 

6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234

7 

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

13 

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

18 

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 

23 

24 

25# A type for data that can be JSON-serialized. 

26JsonObj = dict[str, Any] 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234

27 

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

32 

33 

34class JsonReporter: 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234

35 """A reporter for writing JSON coverage results.""" 

36 

37 report_type = "JSON report" 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234

38 

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

44 

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 } 

57 

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 } 

68 

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`. 

71 

72 `morfs` is a list of modules or file names. 

73 

74 `outfile` is a file object to write the json to. 

75 

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 } 

87 

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 ) 

95 

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

98 

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

101 

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 ) 

107 

108 return self.total.n_statements and self.total.pc_covered 1abcdefghijklmnopqrstuvwxyzABCDEF5GH6IJ7KL'MN8OP(QR9ST!UV#WX$YZ%01)234

109 

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 ) 

133 

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

142 

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

146 

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 ) 

155 

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

161 

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

184 

185 

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