|
7 | 7 |
|
8 | 8 | import collections
|
9 | 9 | import dataclasses
|
10 |
| -from collections.abc import Container, Iterable |
| 10 | +from collections.abc import Iterable |
11 | 11 | from typing import TYPE_CHECKING
|
12 | 12 |
|
13 | 13 | from coverage.exceptions import ConfigError
|
@@ -113,45 +113,6 @@ def __post_init__(self) -> None:
|
113 | 113 | n_missing_branches=n_missing_branches,
|
114 | 114 | )
|
115 | 115 |
|
116 |
| - def narrow(self, lines: Container[TLineNo]) -> Analysis: |
117 |
| - """Create a narrowed Analysis. |
118 |
| -
|
119 |
| - The current analysis is copied to make a new one that only considers |
120 |
| - the lines in `lines`. |
121 |
| - """ |
122 |
| - |
123 |
| - statements = {lno for lno in self.statements if lno in lines} |
124 |
| - excluded = {lno for lno in self.excluded if lno in lines} |
125 |
| - executed = {lno for lno in self.executed if lno in lines} |
126 |
| - |
127 |
| - if self.has_arcs: |
128 |
| - arc_possibilities_set = { |
129 |
| - (a, b) for a, b in self.arc_possibilities_set if a in lines or b in lines |
130 |
| - } |
131 |
| - arcs_executed_set = { |
132 |
| - (a, b) for a, b in self.arcs_executed_set if a in lines or b in lines |
133 |
| - } |
134 |
| - exit_counts = {lno: num for lno, num in self.exit_counts.items() if lno in lines} |
135 |
| - no_branch = {lno for lno in self.no_branch if lno in lines} |
136 |
| - else: |
137 |
| - arc_possibilities_set = set() |
138 |
| - arcs_executed_set = set() |
139 |
| - exit_counts = {} |
140 |
| - no_branch = set() |
141 |
| - |
142 |
| - return Analysis( |
143 |
| - precision=self.precision, |
144 |
| - filename=self.filename, |
145 |
| - has_arcs=self.has_arcs, |
146 |
| - statements=statements, |
147 |
| - excluded=excluded, |
148 |
| - executed=executed, |
149 |
| - arc_possibilities_set=arc_possibilities_set, |
150 |
| - arcs_executed_set=arcs_executed_set, |
151 |
| - exit_counts=exit_counts, |
152 |
| - no_branch=no_branch, |
153 |
| - ) |
154 |
| - |
155 | 116 | def missing_formatted(self, branches: bool = False) -> str:
|
156 | 117 | """The missing line numbers, formatted nicely.
|
157 | 118 |
|
@@ -236,6 +197,104 @@ def branch_stats(self) -> dict[TLineNo, tuple[int, int]]:
|
236 | 197 | return stats
|
237 | 198 |
|
238 | 199 |
|
| 200 | +TRegionLines = frozenset[TLineNo] |
| 201 | + |
| 202 | + |
| 203 | +class AnalysisNarrower: |
| 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: |
| 220 | + self.analysis = analysis |
| 221 | + self.region2arc_possibilities: dict[TRegionLines, set[TArc]] = collections.defaultdict(set) |
| 222 | + self.region2arc_executed: dict[TRegionLines, set[TArc]] = collections.defaultdict(set) |
| 223 | + self.region2exit_counts: dict[TRegionLines, dict[TLineNo, int]] = collections.defaultdict( |
| 224 | + dict |
| 225 | + ) |
| 226 | + |
| 227 | + def add_regions(self, liness: Iterable[set[TLineNo]]) -> None: |
| 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: |
| 233 | + line2region: dict[TLineNo, TRegionLines] = {} |
| 234 | + |
| 235 | + for lines in liness: |
| 236 | + fzlines = frozenset(lines) |
| 237 | + for line in lines: |
| 238 | + line2region[line] = fzlines |
| 239 | + |
| 240 | + def collect_arcs( |
| 241 | + arc_set: set[TArc], |
| 242 | + region2arcs: dict[TRegionLines, set[TArc]], |
| 243 | + ) -> None: |
| 244 | + for a, b in arc_set: |
| 245 | + if r := line2region.get(a): |
| 246 | + region2arcs[r].add((a, b)) |
| 247 | + if r := line2region.get(b): |
| 248 | + region2arcs[r].add((a, b)) |
| 249 | + |
| 250 | + collect_arcs(self.analysis.arc_possibilities_set, self.region2arc_possibilities) |
| 251 | + collect_arcs(self.analysis.arcs_executed_set, self.region2arc_executed) |
| 252 | + |
| 253 | + for lno, num in self.analysis.exit_counts.items(): |
| 254 | + if r := line2region.get(lno): |
| 255 | + self.region2exit_counts[r][lno] = num |
| 256 | + |
| 257 | + def narrow(self, lines: set[TLineNo]) -> Analysis: |
| 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 |
| 269 | + excluded = self.analysis.excluded & lines |
| 270 | + executed = self.analysis.executed & lines |
| 271 | + |
| 272 | + if self.analysis.has_arcs: |
| 273 | + fzlines = frozenset(lines) |
| 274 | + arc_possibilities_set = self.region2arc_possibilities[fzlines] |
| 275 | + arcs_executed_set = self.region2arc_executed[fzlines] |
| 276 | + exit_counts = self.region2exit_counts[fzlines] |
| 277 | + no_branch = self.analysis.no_branch & lines |
| 278 | + else: |
| 279 | + arc_possibilities_set = set() |
| 280 | + arcs_executed_set = set() |
| 281 | + exit_counts = {} |
| 282 | + no_branch = set() |
| 283 | + |
| 284 | + return Analysis( |
| 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 | + |
239 | 298 | @dataclasses.dataclass
|
240 | 299 | class Numbers:
|
241 | 300 | """The numerical results of measuring coverage.
|
|
0 commit comments