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
« 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
4"""Results of coverage measurement."""
6from __future__ import annotations 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
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
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
17if TYPE_CHECKING: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
18 from coverage.data import CoverageData
19 from coverage.plugin import FileReporter
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
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
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
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
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 )
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."""
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
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
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
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 )
116 def missing_formatted(self, branches: bool = False) -> str: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
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!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
129 return format_lines(self.statements, self.missing, arcs=arcs) 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
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
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
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
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.
153 Returns {l1:[l2a,l2b,...], ...}
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
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.
168 Only include ones that we considered possible.
170 Returns {l1:[l2a,l2b,...], ...}
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
183 def branch_stats(self) -> dict[TLineNo, tuple[int, int]]: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
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!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
200TRegionLines = frozenset[TLineNo] 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
203class AnalysisNarrower: 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
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!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 )
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
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!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
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!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
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!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
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
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 )
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.
302 This holds the basic statistics from `Analysis`, and is used to roll
303 up statistics across files.
305 """
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
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
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
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
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
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
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
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
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
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.
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!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234
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
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
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
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 )
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
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.
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!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
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
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
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.
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!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
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
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.
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!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
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
502 return round(total, precision) < fail_under 1abcdefghijklmnopqrstuvwxyzABCDEF!GH#IJ$KL%MN'OP(QR6ST7UV)WX9YZ5018234