utils/coverage_gaps/models.py
Krzysztof kuhy Rudnicki 6c71d531bb repo contract + coverage_gaps gates, test_changed, dependency bumps
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-04 16:27:28 +02:00

70 lines
2.3 KiB
Python

"""Format-neutral data model shared by every parser and renderer."""
from __future__ import annotations
from dataclasses import dataclass, field
class ReportError(Exception):
"""The input is unreadable, unrecognised or malformed (exit code 2)."""
@dataclass
class FileAccumulator:
"""Mutable per-file collector that merges repeated records for one path.
Reports may list a file more than once (lcov re-runs, Cobertura inner
classes). A line or branch counts as covered if ANY record covered it.
"""
lines: dict[int, bool] = field(default_factory=dict)
branches: dict[tuple[int, int, int], bool] = field(default_factory=dict)
def add_line(self, line: int, covered: bool) -> None:
"""Record a line, OR-merging with earlier records of the same line."""
self.lines[line] = self.lines.get(line, False) or covered
def add_branch(self, line: int, block: int, index: int, covered: bool) -> None:
"""Record one branch outcome, OR-merging on (line, block, index)."""
key = (line, block, index)
self.branches[key] = self.branches.get(key, False) or covered
@dataclass(frozen=True)
class FileGap:
"""Coverage state of one source file."""
path: str
total_lines: int
missed_lines: tuple[int, ...]
total_branches: int
missed_branch_lines: tuple[int, ...]
missed_branches: int
@property
def missed(self) -> int:
"""Missed lines plus missed branches: the sort key."""
return len(self.missed_lines) + self.missed_branches
@property
def percent(self) -> float:
"""Percent covered over lines and branches combined (100 if empty)."""
total = self.total_lines + self.total_branches
if total == 0:
return 100.0
return 100.0 * (total - self.missed) / total
def build_gap(path: str, acc: FileAccumulator) -> FileGap:
"""Freeze an accumulator into a FileGap."""
missed_lines = tuple(sorted(n for n, ok in acc.lines.items() if not ok))
missed_br = [key for key, ok in acc.branches.items() if not ok]
return FileGap(
path=path,
total_lines=len(acc.lines),
missed_lines=missed_lines,
total_branches=len(acc.branches),
missed_branch_lines=tuple(sorted({key[0] for key in missed_br})),
missed_branches=len(missed_br),
)