Coverage for coverage / results.py: 100.000%

237 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"""Results of coverage measurement.""" 

5 

6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

7 

8import collections 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

9import dataclasses 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

10from collections.abc import Iterable 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

11from typing import TYPE_CHECKING 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

12 

13from coverage.exceptions import ConfigError 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

14from coverage.misc import nice_pair 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

15from coverage.types import TArc, TLineNo 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

16 

17if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

18 from coverage.data import CoverageData 

19 from coverage.plugin import FileReporter 

20 

21 

22def analysis_from_file_reporter( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

23 data: CoverageData, 

24 precision: int, 

25 file_reporter: FileReporter, 

26 filename: str, 

27) -> Analysis: 

28 """Create an Analysis from a FileReporter.""" 

29 has_arcs = data.has_arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

30 statements = file_reporter.lines() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

31 excluded = file_reporter.excluded_lines() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

32 executed = file_reporter.translate_lines(data.lines(filename) or []) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

33 

34 if has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

35 arc_possibilities_set = file_reporter.arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

36 arcs: Iterable[TArc] = data.arcs(filename) or [] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

37 arcs = file_reporter.translate_arcs(arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

38 

39 # Reduce the set of arcs to the ones that could be branches. 

40 dests = collections.defaultdict(set) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

41 for fromno, tono in arc_possibilities_set: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

42 dests[fromno].add(tono) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

43 single_dests = { 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

44 fromno: list(tonos)[0] for fromno, tonos in dests.items() if len(tonos) == 1 

45 } 

46 new_arcs = set() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

47 for fromno, tono in arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

48 if fromno != tono: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

49 new_arcs.add((fromno, tono)) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

50 else: 

51 if fromno in single_dests: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

52 new_arcs.add((fromno, single_dests[fromno])) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

53 

54 arcs_executed_set = file_reporter.translate_arcs(new_arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

55 exit_counts = file_reporter.exit_counts() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

56 no_branch = file_reporter.no_branch_lines() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

57 else: 

58 arc_possibilities_set = set() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

59 arcs_executed_set = set() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

60 exit_counts = {} 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

61 no_branch = set() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

62 

63 return Analysis( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

64 precision=precision, 

65 filename=filename, 

66 has_arcs=has_arcs, 

67 statements=statements, 

68 excluded=excluded, 

69 executed=executed, 

70 arc_possibilities_set=arc_possibilities_set, 

71 arcs_executed_set=arcs_executed_set, 

72 exit_counts=exit_counts, 

73 no_branch=no_branch, 

74 ) 

75 

76 

77@dataclasses.dataclass 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

78class Analysis: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

79 """The results of analyzing a FileReporter.""" 

80 

81 precision: int 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

82 filename: str 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

83 has_arcs: bool 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

84 statements: set[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

85 excluded: set[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

86 executed: set[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

87 arc_possibilities_set: set[TArc] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

88 arcs_executed_set: set[TArc] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

89 exit_counts: dict[TLineNo, int] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

90 no_branch: set[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

91 

92 def __post_init__(self) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

93 self.arc_possibilities = sorted(self.arc_possibilities_set) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

94 self.arcs_executed = sorted(self.arcs_executed_set) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

95 self.missing = self.statements - self.executed 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

96 

97 if self.has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

98 n_branches = self._total_branches() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

99 mba = self.missing_branch_arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

100 n_partial_branches = sum(len(v) for k, v in mba.items() if k not in self.missing) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

101 n_missing_branches = sum(len(v) for k, v in mba.items()) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

102 else: 

103 n_branches = n_partial_branches = n_missing_branches = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

104 

105 self.numbers = Numbers( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

106 precision=self.precision, 

107 n_files=1, 

108 n_statements=len(self.statements), 

109 n_excluded=len(self.excluded), 

110 n_missing=len(self.missing), 

111 n_branches=n_branches, 

112 n_partial_branches=n_partial_branches, 

113 n_missing_branches=n_missing_branches, 

114 ) 

115 

116 def missing_formatted(self, branches: bool = False) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

117 """The missing line numbers, formatted nicely. 

118 

119 Returns a string like "1-2, 5-11, 13-14". 

120 

121 If `branches` is true, includes the missing branch arcs also. 

122 

123 """ 

124 if branches and self.has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

125 arcs = self.missing_branch_arcs().items() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

126 else: 

127 arcs = None 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

128 

129 return format_lines(self.statements, self.missing, arcs=arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

130 

131 def arcs_missing(self) -> list[TArc]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

132 """Returns a sorted list of the un-executed arcs in the code.""" 

133 missing = ( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

134 p 

135 for p in self.arc_possibilities 

136 if p not in self.arcs_executed_set 

137 and p[0] not in self.no_branch 

138 and p[1] not in self.excluded 

139 ) 

140 return sorted(missing) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

141 

142 def _branch_lines(self) -> list[TLineNo]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

143 """Returns a list of line numbers that have more than one exit.""" 

144 return [l1 for l1, count in self.exit_counts.items() if count > 1] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

145 

146 def _total_branches(self) -> int: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

147 """How many total branches are there?""" 

148 return sum(count for count in self.exit_counts.values() if count > 1) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

149 

150 def missing_branch_arcs(self) -> dict[TLineNo, list[TLineNo]]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

151 """Return arcs that weren't executed from branch lines. 

152 

153 Returns {l1:[l2a,l2b,...], ...} 

154 

155 """ 

156 missing = self.arcs_missing() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

157 branch_lines = set(self._branch_lines()) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

158 mba = collections.defaultdict(list) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

159 for l1, l2 in missing: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

160 assert l1 != l2, f"In {self.filename}, didn't expect {l1} == {l2}" 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR6ST7UVWXYZ501234

161 if l1 in branch_lines: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR6ST7UVWXYZ501234

162 mba[l1].append(l2) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR6STUVWXYZ501234

163 return mba 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

164 

165 def executed_branch_arcs(self) -> dict[TLineNo, list[TLineNo]]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

166 """Return arcs that were executed from branch lines. 

167 

168 Only include ones that we considered possible. 

169 

170 Returns {l1:[l2a,l2b,...], ...} 

171 

172 """ 

173 branch_lines = set(self._branch_lines()) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

174 eba = collections.defaultdict(list) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

175 for l1, l2 in self.arcs_executed: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

176 assert l1 != l2, f"Oops: Didn't think this could happen: {l1 = }, {l2 = }" 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

177 if (l1, l2) not in self.arc_possibilities_set: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

178 continue 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQR6STUVWX9YZ01234

179 if l1 in branch_lines: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

180 eba[l1].append(l2) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

181 return eba 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

182 

183 def branch_stats(self) -> dict[TLineNo, tuple[int, int]]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

184 """Get stats about branches. 

185 

186 Returns a dict mapping line numbers to a tuple: 

187 (total_exits, taken_exits). 

188 

189 """ 

190 

191 missing_arcs = self.missing_branch_arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

192 stats = {} 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

193 for lnum in self._branch_lines(): 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

194 exits = self.exit_counts[lnum] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

195 missing = len(missing_arcs[lnum]) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

196 stats[lnum] = (exits, exits - missing) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

197 return stats 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

198 

199 

200TRegionLines = frozenset[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

201 

202 

203class AnalysisNarrower: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

204 """ 

205 For reducing an `Analysis` to a subset of its lines. 

206 

207 Originally this was a simpler method on Analysis, but that led to quadratic 

208 behavior. This class does the bulk of the work up-front to provide the 

209 same results in linear time. 

210 

211 Create an AnalysisNarrower from an Analysis, bulk-add region lines to it 

212 with `add_regions`, then individually request new narrowed Analysis objects 

213 for each region with `narrow`. Doing most of the work in limited calls to 

214 `add_regions` lets us avoid poor performance. 

215 """ 

216 

217 # In this class, regions are represented by a frozenset of their lines. 

218 

219 def __init__(self, analysis: Analysis) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

220 self.analysis = analysis 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

221 self.region2arc_possibilities: dict[TRegionLines, set[TArc]] = collections.defaultdict(set) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

222 self.region2arc_executed: dict[TRegionLines, set[TArc]] = collections.defaultdict(set) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

223 self.region2exit_counts: dict[TRegionLines, dict[TLineNo, int]] = collections.defaultdict( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

224 dict 

225 ) 

226 

227 def add_regions(self, liness: Iterable[set[TLineNo]]) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

228 """ 

229 Pre-process a number of sets of line numbers. Later calls to `narrow` 

230 with one of these sets will provide a narrowed Analysis. 

231 """ 

232 if self.analysis.has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

233 line2region: dict[TLineNo, TRegionLines] = {} 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

234 

235 for lines in liness: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

236 fzlines = frozenset(lines) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

237 for line in lines: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

238 line2region[line] = fzlines 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

239 

240 def collect_arcs( 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

241 arc_set: set[TArc], 

242 region2arcs: dict[TRegionLines, set[TArc]], 

243 ) -> None: 

244 for a, b in arc_set: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

245 if r := line2region.get(a): 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

246 region2arcs[r].add((a, b)) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

247 if r := line2region.get(b): 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

248 region2arcs[r].add((a, b)) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

249 

250 collect_arcs(self.analysis.arc_possibilities_set, self.region2arc_possibilities) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

251 collect_arcs(self.analysis.arcs_executed_set, self.region2arc_executed) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

252 

253 for lno, num in self.analysis.exit_counts.items(): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

254 if r := line2region.get(lno): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

255 self.region2exit_counts[r][lno] = num 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

256 

257 def narrow(self, lines: set[TLineNo]) -> Analysis: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

258 """Create a narrowed Analysis. 

259 

260 The current analysis is copied to make a new one that only considers 

261 the lines in `lines`. 

262 """ 

263 

264 # Technically, the set intersections in this method are still O(N**2) 

265 # since this method is called N times, but they're very fast and moving 

266 # them to `add_regions` won't avoid the quadratic time. 

267 

268 statements = self.analysis.statements & lines 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

269 excluded = self.analysis.excluded & lines 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

270 executed = self.analysis.executed & lines 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

271 

272 if self.analysis.has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

273 fzlines = frozenset(lines) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

274 arc_possibilities_set = self.region2arc_possibilities[fzlines] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

275 arcs_executed_set = self.region2arc_executed[fzlines] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

276 exit_counts = self.region2exit_counts[fzlines] 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

277 no_branch = self.analysis.no_branch & lines 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

278 else: 

279 arc_possibilities_set = set() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

280 arcs_executed_set = set() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

281 exit_counts = {} 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

282 no_branch = set() 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

283 

284 return Analysis( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

285 precision=self.analysis.precision, 

286 filename=self.analysis.filename, 

287 has_arcs=self.analysis.has_arcs, 

288 statements=statements, 

289 excluded=excluded, 

290 executed=executed, 

291 arc_possibilities_set=arc_possibilities_set, 

292 arcs_executed_set=arcs_executed_set, 

293 exit_counts=exit_counts, 

294 no_branch=no_branch, 

295 ) 

296 

297 

298@dataclasses.dataclass 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

299class Numbers: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

300 """The numerical results of measuring coverage. 

301 

302 This holds the basic statistics from `Analysis`, and is used to roll 

303 up statistics across files. 

304 

305 """ 

306 

307 precision: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

308 n_files: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

309 n_statements: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

310 n_excluded: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

311 n_missing: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

312 n_branches: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

313 n_partial_branches: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

314 n_missing_branches: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

315 

316 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

317 def n_executed(self) -> int: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

318 """Returns the number of executed statements.""" 

319 return self.n_statements - self.n_missing 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

320 

321 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

322 def n_executed_branches(self) -> int: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

323 """Returns the number of executed branches.""" 

324 return self.n_branches - self.n_missing_branches 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

325 

326 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

327 def ratio_statements(self) -> tuple[int, int]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

328 """Return numerator/denominator for statement coverage.""" 

329 return self.n_executed, self.n_statements 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

330 

331 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

332 def ratio_branches(self) -> tuple[int, int]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

333 """Return numerator/denominator for branch coverage.""" 

334 return self.n_executed_branches, self.n_branches 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

335 

336 def _percent(self, numerator: int, denominator: int) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

337 """Helper for pc_* properties.""" 

338 if denominator > 0: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

339 return (100.0 * numerator) / denominator 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

340 return 100.0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

341 

342 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

343 def pc_covered(self) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

344 """Returns a single percentage value for coverage.""" 

345 return self._percent(*self.ratio_covered) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

346 

347 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

348 def pc_statements(self) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

349 """Returns the percentage covered for statements.""" 

350 return self._percent(*self.ratio_statements) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

351 

352 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

353 def pc_branches(self) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

354 """Returns the percentage covered for branches.""" 

355 return self._percent(*self.ratio_branches) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

356 

357 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

358 def pc_covered_str(self) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

359 """Returns the percent covered, as a string, without a percent sign. 

360 

361 Note that "0" is only returned when the value is truly zero, and "100" 

362 is only returned when the value is truly 100. Rounding can never 

363 result in either "0" or "100". 

364 

365 """ 

366 return display_covered(self.pc_covered, self.precision) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

367 

368 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

369 def pc_statements_str(self) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

370 """Returns the statement percent covered without a percent sign.""" 

371 return display_covered(self.pc_statements, self.precision) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

372 

373 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

374 def pc_branches_str(self) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

375 """Returns the branch percent covered without a percent sign.""" 

376 return display_covered(self.pc_branches, self.precision) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

377 

378 @property 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

379 def ratio_covered(self) -> tuple[int, int]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

380 """Return a numerator and denominator for the coverage ratio.""" 

381 numerator = self.n_executed + self.n_executed_branches 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

382 denominator = self.n_statements + self.n_branches 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

383 return numerator, denominator 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

384 

385 def __add__(self, other: Numbers) -> Numbers: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

386 return Numbers( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

387 self.precision, 

388 self.n_files + other.n_files, 

389 self.n_statements + other.n_statements, 

390 self.n_excluded + other.n_excluded, 

391 self.n_missing + other.n_missing, 

392 self.n_branches + other.n_branches, 

393 self.n_partial_branches + other.n_partial_branches, 

394 self.n_missing_branches + other.n_missing_branches, 

395 ) 

396 

397 def __radd__(self, other: int) -> Numbers: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

398 # Implementing 0+Numbers allows us to sum() a list of Numbers. 

399 assert other == 0 # we only ever call it this way. 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

400 return self 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

401 

402 

403def display_covered(pc: float, precision: int) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

404 """Return a displayable total percentage, as a string. 

405 

406 Note that "0" is only returned when the value is truly zero, and "100" 

407 is only returned when the value is truly 100. Rounding can never 

408 result in either "0" or "100". 

409 

410 """ 

411 near0 = 1.0 / 10**precision 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

412 if 0 < pc < near0: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

413 pc = near0 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

414 elif (100.0 - near0) < pc < 100: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

415 pc = 100.0 - near0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

416 else: 

417 pc = round(pc, precision) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

418 return f"{pc:.{precision}f}" 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

419 

420 

421def _line_ranges( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

422 statements: Iterable[TLineNo], 

423 lines: Iterable[TLineNo], 

424) -> list[tuple[TLineNo, TLineNo]]: 

425 """Produce a list of ranges for `format_lines`.""" 

426 statements = sorted(statements) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

427 lines = sorted(lines) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

428 

429 pairs = [] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

430 start: TLineNo | None = None 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

431 lidx = 0 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

432 for stmt in statements: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

433 if lidx >= len(lines): 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

434 break 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

435 if stmt == lines[lidx]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

436 lidx += 1 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

437 if not start: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

438 start = stmt 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

439 end = stmt 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

440 elif start: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

441 pairs.append((start, end)) 1abcdefghijklmnopqrstuvwxyzABCDEFGH#IJ$KLMNOPQRSTUVWXYZ5018234

442 start = None 1abcdefghijklmnopqrstuvwxyzABCDEFGH#IJ$KLMNOPQRSTUVWXYZ5018234

443 if start: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

444 pairs.append((start, end)) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

445 return pairs 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

446 

447 

448def format_lines( 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018

449 statements: Iterable[TLineNo], 

450 lines: Iterable[TLineNo], 

451 arcs: Iterable[tuple[TLineNo, list[TLineNo]]] | None = None, 

452) -> str: 

453 """Nicely format a list of line numbers. 

454 

455 Format a list of line numbers for printing by coalescing groups of lines as 

456 long as the lines represent consecutive statements. This will coalesce 

457 even if there are gaps between statements. 

458 

459 For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and 

460 `lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14". 

461 

462 Both `lines` and `statements` can be any iterable. All of the elements of 

463 `lines` must be in `statements`, and all of the values must be positive 

464 integers. 

465 

466 If `arcs` is provided, they are (start,[end,end,end]) pairs that will be 

467 included in the output as long as start isn't in `lines`. 

468 

469 """ 

470 line_items = [(pair[0], nice_pair(pair)) for pair in _line_ranges(statements, lines)] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

471 if arcs is not None: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

472 line_exits = sorted(arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

473 for line, exits in line_exits: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

474 for ex in sorted(exits): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

475 if line not in lines and ex not in lines: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

476 dest = ex if ex > 0 else "exit" 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

477 line_items.append((line, f"{line}->{dest}")) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

478 

479 ret = ", ".join(t[-1] for t in sorted(line_items)) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

480 return ret 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

481 

482 

483def should_fail_under(total: float, fail_under: float, precision: int) -> bool: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

484 """Determine if a total should fail due to fail-under. 

485 

486 `total` is a float, the coverage measurement total. `fail_under` is the 

487 fail_under setting to compare with. `precision` is the number of digits 

488 to consider after the decimal point. 

489 

490 Returns True if the total should fail. 

491 

492 """ 

493 # We can never achieve higher than 100% coverage, or less than zero. 

494 if not (0 <= fail_under <= 100.0): 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

495 msg = f"fail_under={fail_under} is invalid. Must be between 0 and 100." 1abcdefghijklmnopqrstuvwxyzABCDEF!GHIJKL%MN'OPQRST7UVWX9YZ5018234

496 raise ConfigError(msg) 1abcdefghijklmnopqrstuvwxyzABCDEF!GHIJKL%MN'OPQRST7UVWX9YZ5018234

497 

498 # Special case for fail_under=100, it must really be 100. 

499 if fail_under == 100.0 and total != 100.0: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

500 return True 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234

501 

502 return round(total, precision) < fail_under 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234