soteria/scripts/coverage_hygiene_check.py

118 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Enforce non-regressing Go coverage against a checked-in baseline."""
from __future__ import annotations
import argparse
import json
import re
import sys
from collections import defaultdict
from pathlib import Path
COVER_RE = re.compile(
r"^(?P<path>.+?):(?P<start>\d+)\.(?P<start_col>\d+),(?P<end>\d+)\.(?P<end_col>\d+)\s+(?P<stmts>\d+)\s+(?P<count>\d+)$"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--root", default=".")
parser.add_argument("--coverprofile", required=True)
parser.add_argument("--min-total", type=float, default=0.0)
parser.add_argument("--baseline", required=True)
parser.add_argument("--summary-json", default="")
return parser.parse_args()
def normalize_path(raw: str, root_name: str) -> str:
marker = f"/{root_name}/"
if marker in raw:
return raw.split(marker, 1)[1]
path = Path(raw)
return path.as_posix()
def parse_coverprofile(path: Path, root_name: str) -> tuple[dict[str, tuple[int, int]], float]:
per_file: dict[str, list[int]] = defaultdict(lambda: [0, 0])
total_statements = 0
total_covered = 0
for line in path.read_text(encoding="utf-8").splitlines():
if not line or line.startswith("mode:"):
continue
match = COVER_RE.match(line)
if not match:
continue
rel = normalize_path(match.group("path"), root_name)
stmts = int(match.group("stmts"))
count = int(match.group("count"))
per_file[rel][0] += stmts
total_statements += stmts
if count > 0:
per_file[rel][1] += stmts
total_covered += stmts
normalized = {k: (v[0], v[1]) for k, v in per_file.items()}
total_pct = 100.0 if total_statements == 0 else (total_covered * 100.0 / total_statements)
return normalized, total_pct
def load_baseline(path: Path) -> dict[str, float]:
baseline: dict[str, float] = {}
for raw in path.read_text(encoding="utf-8").splitlines():
line = raw.strip()
if not line or line.startswith("#"):
continue
parts = line.split("\t")
if len(parts) < 2:
continue
rel = parts[0]
try:
pct = float(parts[1])
except ValueError:
continue
baseline[rel] = pct
return baseline
def main() -> int:
args = parse_args()
root = Path(args.root).resolve()
coverprofile = Path(args.coverprofile).resolve()
baseline = load_baseline(Path(args.baseline).resolve())
if not coverprofile.exists():
print(f"Coverage hygiene check failed: missing coverprofile {coverprofile}")
return 1
per_file, total_pct = parse_coverprofile(coverprofile, root.name)
violations: list[str] = []
for rel, floor in sorted(baseline.items()):
stmts, covered = per_file.get(rel, (0, 0))
pct = 100.0 if stmts == 0 else (covered * 100.0 / stmts)
if pct + 1e-9 < floor:
violations.append(f"{rel}: {pct:.2f}% < baseline {floor:.2f}%")
if total_pct + 1e-9 < args.min_total:
violations.append(f"total coverage {total_pct:.2f}% < floor {args.min_total:.2f}%")
summary = {
"total_percent": round(total_pct, 3),
"checked_files": len(baseline),
"violations": len(violations),
}
if args.summary_json:
Path(args.summary_json).write_text(json.dumps(summary, indent=2), encoding="utf-8")
if violations:
print("Coverage hygiene check failed:")
for item in violations:
print(item)
return 1
print(f"Coverage hygiene checks: ok (total {total_pct:.2f}%)")
return 0
if __name__ == "__main__":
sys.exit(main())