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
« 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"""Results of coverage measurement."""
6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
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
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
17if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
18 from coverage.data import CoverageData
19 from coverage.plugin import FileReporter
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
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
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
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
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 )
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."""
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
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
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
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 )
116 def missing_formatted(self, branches: bool = False) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
117 """The missing line numbers, formatted nicely.
119 Returns a string like "1-2, 5-11, 13-14".
121 If `branches` is true, includes the missing branch arcs also.
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
129 return format_lines(self.statements, self.missing, arcs=arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
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
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
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
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.
153 Returns {l1:[l2a,l2b,...], ...}
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
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.
168 Only include ones that we considered possible.
170 Returns {l1:[l2a,l2b,...], ...}
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
183 def branch_stats(self) -> dict[TLineNo, tuple[int, int]]: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
184 """Get stats about branches.
186 Returns a dict mapping line numbers to a tuple:
187 (total_exits, taken_exits).
189 """
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
200TRegionLines = frozenset[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
203class AnalysisNarrower: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
204 """
205 For reducing an `Analysis` to a subset of its lines.
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.
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 """
217 # In this class, regions are represented by a frozenset of their lines.
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 )
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
235 for lines in liness: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
236 fzlines = frozenset(lines) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
237 for line in lines: 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
238 line2region[line] = fzlines 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
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
250 collect_arcs(self.analysis.arc_possibilities_set, self.region2arc_possibilities) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
251 collect_arcs(self.analysis.arcs_executed_set, self.region2arc_executed) 1abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234
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
257 def narrow(self, lines: set[TLineNo]) -> Analysis: 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
258 """Create a narrowed Analysis.
260 The current analysis is copied to make a new one that only considers
261 the lines in `lines`.
262 """
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.
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
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
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 )
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.
302 This holds the basic statistics from `Analysis`, and is used to roll
303 up statistics across files.
305 """
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
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
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
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
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
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
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
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
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
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.
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".
365 """
366 return display_covered(self.pc_covered, self.precision) 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234
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
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
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
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 )
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
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.
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".
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
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
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
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.
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.
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".
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.
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`.
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
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
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.
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.
490 Returns True if the total should fail.
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
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
502 return round(total, precision) < fail_under 1abcdefghijklmnopqrstuvwxyzABCDEF(GH7IJ8KL$MN'OP6QR)ST9UV%WX!YZ#015234