Coverage for coverage / results.py: 100.000%

237 statements  

« 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 

3 

4"""Results of coverage measurement.""" 

5 

6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

7 

8import collections 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

9import dataclasses 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

10from collections.abc import Iterable 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

11from typing import TYPE_CHECKING 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

12 

13from coverage.exceptions import ConfigError 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

14from coverage.misc import nice_pair 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

15from coverage.types import TArc, TLineNo 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

16 

17if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

18 from coverage.data import CoverageData 

19 from coverage.plugin import FileReporter 

20 

21 

22def analysis_from_file_reporter( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

30 statements = file_reporter.lines() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

31 excluded = file_reporter.excluded_lines() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

32 executed = file_reporter.translate_lines(data.lines(filename) or []) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

33 

34 if has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

35 arc_possibilities_set = file_reporter.arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

36 arcs: Iterable[TArc] = data.arcs(filename) or [] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

37 arcs = file_reporter.translate_arcs(arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

38 

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

40 dests = collections.defaultdict(set) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

41 for fromno, tono in arc_possibilities_set: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

42 dests[fromno].add(tono) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

43 single_dests = { 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

45 } 

46 new_arcs = set() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

47 for fromno, tono in arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

48 if fromno != tono: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

49 new_arcs.add((fromno, tono)) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

50 else: 

51 if fromno in single_dests: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

52 new_arcs.add((fromno, single_dests[fromno])) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

53 

54 arcs_executed_set = file_reporter.translate_arcs(new_arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

55 exit_counts = file_reporter.exit_counts() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

56 no_branch = file_reporter.no_branch_lines() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

57 else: 

58 arc_possibilities_set = set() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

59 arcs_executed_set = set() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

60 exit_counts = {} 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

61 no_branch = set() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

62 

63 return Analysis( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

78class Analysis: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

80 

81 precision: int 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

82 filename: str 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

83 has_arcs: bool 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

84 statements: set[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

85 excluded: set[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

86 executed: set[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

87 arc_possibilities_set: set[TArc] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

88 arcs_executed_set: set[TArc] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

89 exit_counts: dict[TLineNo, int] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

90 no_branch: set[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

91 

92 def __post_init__(self) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

93 self.arc_possibilities = sorted(self.arc_possibilities_set) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

94 self.arcs_executed = sorted(self.arcs_executed_set) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

95 self.missing = self.statements - self.executed 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

96 

97 if self.has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

98 n_branches = self._total_branches() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

99 mba = self.missing_branch_arcs() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

100 n_partial_branches = sum(len(v) for k, v in mba.items() if k not in self.missing) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

101 n_missing_branches = sum(len(v) for k, v in mba.items()) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

102 else: 

103 n_branches = n_partial_branches = n_missing_branches = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

104 

105 self.numbers = Numbers( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

125 arcs = self.missing_branch_arcs().items() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

126 else: 

127 arcs = None 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

128 

129 return format_lines(self.statements, self.missing, arcs=arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

130 

131 def arcs_missing(self) -> list[TArc]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

133 missing = ( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

141 

142 def _branch_lines(self) -> list[TLineNo]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

145 

146 def _total_branches(self) -> int: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

148 return sum(count for count in self.exit_counts.values() if count > 1) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

149 

150 def missing_branch_arcs(self) -> dict[TLineNo, list[TLineNo]]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

152 

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

154 

155 """ 

156 missing = self.arcs_missing() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

157 branch_lines = set(self._branch_lines()) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

158 mba = collections.defaultdict(list) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

159 for l1, l2 in missing: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

161 if l1 in branch_lines: 1abcdefghijklmnopqrstuvwxyzABCDEFGH7IJ8KLMNOPQRST9UVWX!YZ#015234

162 mba[l1].append(l2) 1abcdefghijklmnopqrstuvwxyzABCDEFGH7IJ8KLMNOPQRST9UVWX!YZ#015234

163 return mba 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

164 

165 def executed_branch_arcs(self) -> dict[TLineNo, list[TLineNo]]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

174 eba = collections.defaultdict(list) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

175 for l1, l2 in self.arcs_executed: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

176 assert l1 != l2, f"Oops: Didn't think this could happen: {l1 = }, {l2 = }" 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

177 if (l1, l2) not in self.arc_possibilities_set: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

178 continue 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMN'OP6QRSTUVWXYZ#01234

179 if l1 in branch_lines: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

180 eba[l1].append(l2) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

181 return eba 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

182 

183 def branch_stats(self) -> dict[TLineNo, tuple[int, int]]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

192 stats = {} 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

193 for lnum in self._branch_lines(): 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

198 

199 

200TRegionLines = frozenset[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

201 

202 

203class AnalysisNarrower: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

220 self.analysis = analysis 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

221 self.region2arc_possibilities: dict[TRegionLines, set[TArc]] = collections.defaultdict(set) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

222 self.region2arc_executed: dict[TRegionLines, set[TArc]] = collections.defaultdict(set) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

223 self.region2exit_counts: dict[TRegionLines, dict[TLineNo, int]] = collections.defaultdict( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

224 dict 

225 ) 

226 

227 def add_regions(self, liness: Iterable[set[TLineNo]]) -> None: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

245 if r := line2region.get(a): 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

246 region2arcs[r].add((a, b)) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

247 if r := line2region.get(b): 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

248 region2arcs[r].add((a, b)) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

269 excluded = self.analysis.excluded & lines 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

270 executed = self.analysis.executed & lines 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

271 

272 if self.analysis.has_arcs: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

280 arcs_executed_set = set() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

281 exit_counts = {} 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

282 no_branch = set() 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

283 

284 return Analysis( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

299class Numbers: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

308 n_files: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

309 n_statements: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

310 n_excluded: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

311 n_missing: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

312 n_branches: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

313 n_partial_branches: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

314 n_missing_branches: int = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

315 

316 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

317 def n_executed(self) -> int: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

319 return self.n_statements - self.n_missing 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

320 

321 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

322 def n_executed_branches(self) -> int: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

324 return self.n_branches - self.n_missing_branches 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

325 

326 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

327 def ratio_statements(self) -> tuple[int, int]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

329 return self.n_executed, self.n_statements 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

330 

331 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

332 def ratio_branches(self) -> tuple[int, int]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

334 return self.n_executed_branches, self.n_branches 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

335 

336 def _percent(self, numerator: int, denominator: int) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

338 if denominator > 0: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

339 return (100.0 * numerator) / denominator 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

340 return 100.0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

341 

342 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

343 def pc_covered(self) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

345 return self._percent(*self.ratio_covered) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

346 

347 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

348 def pc_statements(self) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

350 return self._percent(*self.ratio_statements) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

351 

352 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

353 def pc_branches(self) -> float: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

355 return self._percent(*self.ratio_branches) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

356 

357 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

358 def pc_covered_str(self) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

367 

368 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

369 def pc_statements_str(self) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

371 return display_covered(self.pc_statements, self.precision) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

372 

373 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

374 def pc_branches_str(self) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

376 return display_covered(self.pc_branches, self.precision) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

377 

378 @property 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

379 def ratio_covered(self) -> tuple[int, int]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

381 numerator = self.n_executed + self.n_executed_branches 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

382 denominator = self.n_statements + self.n_branches 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

383 return numerator, denominator 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

384 

385 def __add__(self, other: Numbers) -> Numbers: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

386 return Numbers( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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

399 assert other == 0 # we only ever call it this way. 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

400 return self 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

401 

402 

403def display_covered(pc: float, precision: int) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

412 if 0 < pc < near0: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

413 pc = near0 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234

414 elif (100.0 - near0) < pc < 100: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

415 pc = 100.0 - near0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

416 else: 

417 pc = round(pc, precision) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

418 return f"{pc:.{precision}f}" 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

419 

420 

421def _line_ranges( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

427 lines = sorted(lines) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

428 

429 pairs = [] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

430 start: TLineNo | None = None 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

431 lidx = 0 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

432 for stmt in statements: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

433 if lidx >= len(lines): 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

434 break 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

435 if stmt == lines[lidx]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

436 lidx += 1 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

437 if not start: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

438 start = stmt 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

439 end = stmt 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

440 elif start: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

441 pairs.append((start, end)) 1abcdefghijklmnopqrstuvwxyzABCDEFGH7IJ8KLMNOP6QRSTUV%WXYZ015234

442 start = None 1abcdefghijklmnopqrstuvwxyzABCDEFGH7IJ8KLMNOP6QRSTUV%WXYZ015234

443 if start: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

444 pairs.append((start, end)) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

445 return pairs 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

446 

447 

448def format_lines( 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

471 if arcs is not None: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

472 line_exits = sorted(arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

473 for line, exits in line_exits: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

474 for ex in sorted(exits): 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOP6QRSTUVWXYZ015234

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

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

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

478 

479 ret = ", ".join(t[-1] for t in sorted(line_items)) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

480 return ret 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

481 

482 

483def should_fail_under(total: float, fail_under: float, precision: int) -> bool: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

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(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

495 msg = f"fail_under={fail_under} is invalid. Must be between 0 and 100." 1abcdefghijklmnopqrstuvwxyzABCDEFGH7IJKL$MNOP6QRST9UVWX!YZ015234

496 raise ConfigError(msg) 1abcdefghijklmnopqrstuvwxyzABCDEFGH7IJKL$MNOP6QRST9UVWX!YZ015234

497 

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

499 if fail_under == 100.0 and total != 100.0: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

500 return True 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234

501 

502 return round(total, precision) < fail_under 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234