test: enforce Wave A quality lock

This commit is contained in:
Joseph Magly
2026-08-14 19:47:46 -04:00
parent 57523ab483
commit 9746c21b63
18 changed files with 1712 additions and 44 deletions
+242
View File
@@ -7,10 +7,28 @@ import json
import math
import re
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Any
@dataclass(frozen=True)
class ChangedModule:
"""One live production module changed between the base and head commits."""
status: str
base_path: str | None
head_path: str
@dataclass(frozen=True)
class ModuleCoverage:
"""Unrounded line and branch percentages for one measured module."""
line: float
branch: float | None
def validate_coverage(
report: dict[str, Any], *, min_line: float, min_branch: float,
file_floors: dict[str, float] | None = None,
@@ -114,6 +132,166 @@ def validate_changed_coverage(
return []
def parse_changed_modules(
raw: bytes, *, roots: tuple[str, ...] = ("obliteratus",),
) -> list[ChangedModule]:
"""Parse ``git diff --name-status -M -z`` for live Python modules."""
tokens = raw.decode("utf-8", "surrogateescape").split("\0")
if tokens and tokens[-1] == "":
tokens.pop()
normalized_roots = tuple(root.rstrip("/") for root in roots if root.rstrip("/"))
def included(path: str) -> bool:
return path.endswith(".py") and any(
path.startswith(f"{root}/") for root in normalized_roots
)
changes: list[ChangedModule] = []
index = 0
while index < len(tokens):
status = tokens[index]
index += 1
if not status:
raise ValueError("changed-module diff contains an empty status")
kind = status[0]
if kind in {"R", "C"}:
if index + 1 >= len(tokens):
raise ValueError(f"changed-module diff is truncated after {status!r}")
old_path, new_path = tokens[index], tokens[index + 1]
index += 2
if kind == "R" and included(old_path) and not included(new_path):
changes.append(ChangedModule("D", old_path, old_path))
continue
if not included(new_path):
continue
base_path = old_path if kind == "R" and included(old_path) else None
changes.append(ChangedModule(status, base_path, new_path))
continue
if index >= len(tokens):
raise ValueError(f"changed-module diff is truncated after {status!r}")
path = tokens[index]
index += 1
if not included(path):
continue
if kind == "D":
changes.append(ChangedModule(status, path, path))
continue
if kind not in {"A", "M", "T"}:
raise ValueError(f"unsupported changed-module status {status!r}")
changes.append(
ChangedModule(status, None if kind == "A" else path, path),
)
return changes
def module_coverage(
report: dict[str, Any], path: str,
) -> tuple[ModuleCoverage | None, str | None]:
"""Return exact per-module line/branch coverage or a validation failure."""
files = report.get("files")
entry = files.get(path) if isinstance(files, dict) else None
if not isinstance(entry, dict):
return None, f"coverage report is missing touched module {path}"
summary = entry.get("summary")
if not isinstance(summary, dict):
return None, f"coverage report is missing summary for touched module {path}"
values: dict[str, int] = {}
for key in ("covered_lines", "num_statements", "covered_branches", "num_branches"):
value = summary.get(key)
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
return None, f"coverage report has invalid {key} for touched module {path}"
values[key] = value
if values["num_statements"] == 0:
return None, f"coverage report measures no statements for touched module {path}"
if values["covered_lines"] > values["num_statements"]:
return None, f"coverage report overcounts covered lines for touched module {path}"
if values["covered_branches"] > values["num_branches"]:
return None, f"coverage report overcounts covered branches for touched module {path}"
line = values["covered_lines"] / values["num_statements"] * 100
branch = (
values["covered_branches"] / values["num_branches"] * 100
if values["num_branches"]
else None
)
return ModuleCoverage(line=line, branch=branch), None
def validate_touched_module_regression(
head_report: dict[str, Any],
base_report: dict[str, Any],
changes: list[ChangedModule],
*,
line_tolerance: float = 0.0,
branch_tolerance: float = 0.0,
new_module_min_line: float = 80.0,
new_module_min_branch: float = 75.0,
) -> list[str]:
"""Reject line or branch regressions in each touched production module."""
def regressed(head: float, base: float, tolerance: float) -> bool:
return base - head > tolerance + 1e-9
failures: list[str] = []
for change in changes:
if change.status.startswith("D"):
failures.append(
f"deleted production module {change.base_path} requires an explicit "
"reviewed coverage-policy exception",
)
continue
head, head_failure = module_coverage(head_report, change.head_path)
if head_failure:
failures.append(head_failure)
continue
assert head is not None
if change.base_path is None:
if head.line < new_module_min_line:
failures.append(
f"new module {change.head_path} line coverage {head.line:.2f}% "
f"is below the {new_module_min_line:.2f}% floor",
)
if head.branch is not None and head.branch < new_module_min_branch:
failures.append(
f"new module {change.head_path} branch coverage {head.branch:.2f}% "
f"is below the {new_module_min_branch:.2f}% floor",
)
continue
base, base_failure = module_coverage(base_report, change.base_path)
if base_failure:
failures.append(f"base {base_failure}")
continue
assert base is not None
if regressed(head.line, base.line, line_tolerance):
failures.append(
f"touched module {change.head_path} line coverage regressed "
f"from {base.line:.2f}% to {head.line:.2f}%",
)
if base.branch is None and head.branch is not None:
if head.branch < new_module_min_branch:
failures.append(
f"touched module {change.head_path} added branches at "
f"{head.branch:.2f}% coverage, below the "
f"{new_module_min_branch:.2f}% floor",
)
elif base.branch is not None and head.branch is not None:
if regressed(head.branch, base.branch, branch_tolerance):
failures.append(
f"touched module {change.head_path} branch coverage regressed "
f"from {base.branch:.2f}% to {head.branch:.2f}%",
)
return failures
def _file_floor(value: str) -> tuple[str, float]:
try:
path, minimum = value.rsplit("=", 1)
@@ -142,6 +320,26 @@ def _parser() -> argparse.ArgumentParser:
"--base-ref",
help="git base commit/ref used to calculate changed executable lines",
)
parser.add_argument(
"--base-report",
type=Path,
help="coverage.py JSON report generated from the base commit",
)
parser.add_argument(
"--touched-module-no-regression",
action="store_true",
help="compare each touched production module against --base-report",
)
parser.add_argument(
"--module-root",
action="append",
default=[],
help="production module root for touched-module comparison (default: obliteratus)",
)
parser.add_argument("--module-line-tolerance", type=float, default=0.0)
parser.add_argument("--module-branch-tolerance", type=float, default=0.0)
parser.add_argument("--new-module-min-line", type=float, default=80.0)
parser.add_argument("--new-module-min-branch", type=float, default=75.0)
return parser
@@ -180,6 +378,48 @@ def main() -> int:
failures.extend(
validate_changed_coverage(report, changed, minimum=args.min_changed),
)
touched_count: int | None = None
if args.touched_module_no_regression:
if not args.base_ref or args.base_report is None:
failures.append(
"touched-module comparison requires --base-ref and --base-report",
)
elif args.module_line_tolerance < 0 or args.module_branch_tolerance < 0:
failures.append("touched-module tolerances must be non-negative")
else:
try:
base_report = json.loads(args.base_report.read_text(encoding="utf-8"))
pathspecs = [
f":(glob){root.rstrip('/')}/**/*.py"
for root in (args.module_root or ["obliteratus"])
]
raw_changes = subprocess.run(
[
"git", "diff", "--name-status", "-M", "-z",
args.base_ref, "HEAD", "--", *pathspecs,
],
check=True,
capture_output=True,
).stdout
module_changes = parse_changed_modules(
raw_changes,
roots=tuple(args.module_root or ["obliteratus"]),
)
except (OSError, json.JSONDecodeError, subprocess.CalledProcessError, ValueError) as exc:
failures.append(f"cannot compare touched-module coverage: {exc}")
else:
touched_count = len(module_changes)
failures.extend(
validate_touched_module_regression(
report,
base_report,
module_changes,
line_tolerance=args.module_line_tolerance,
branch_tolerance=args.module_branch_tolerance,
new_module_min_line=args.new_module_min_line,
new_module_min_branch=args.new_module_min_branch,
),
)
if failures:
for failure in failures:
print(f"coverage gate failed: {failure}")
@@ -196,6 +436,8 @@ def main() -> int:
print(
f"changed-line gate passed: {percentage:.2f}% ({covered}/{executable})",
)
if touched_count is not None:
print(f"touched-module coverage gate passed: {touched_count} module(s)")
return 0