77 lines
2.6 KiB
Python
77 lines
2.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate per-file coverage against an explicit contract.
|
|
|
|
Why: the repo is ratcheting from an overall coverage floor toward per-file
|
|
coverage guarantees, and CI needs a machine-checkable list of the files that are
|
|
in phase 1 of that ratchet.
|
|
|
|
Inputs:
|
|
- `coverage_json`: slipcover JSON artifact with per-file summaries.
|
|
- `contract_json`: JSON object containing a `files` map of file paths to
|
|
minimum coverage percentages.
|
|
|
|
Output:
|
|
- Exit 0 when every contracted file meets or exceeds its minimum.
|
|
- Exit 1 with a concise report when any file falls short or is missing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def _load_json(path: Path) -> dict[str, Any]:
|
|
"""Load a JSON file and return the parsed object."""
|
|
return json.loads(path.read_text(encoding="utf-8"))
|
|
|
|
|
|
def _file_percent(entry: dict[str, Any]) -> float | None:
|
|
summary = entry.get("summary") if isinstance(entry.get("summary"), dict) else {}
|
|
percent = summary.get("percent_covered")
|
|
return float(percent) if isinstance(percent, (int, float)) else None
|
|
|
|
|
|
def check_coverage_contract(coverage_json: Path, contract_json: Path) -> int:
|
|
"""Check the supplied coverage artifact against the contract file."""
|
|
coverage = _load_json(coverage_json)
|
|
contract = _load_json(contract_json)
|
|
files = coverage.get("files") if isinstance(coverage.get("files"), dict) else {}
|
|
contracted = contract.get("files") if isinstance(contract.get("files"), dict) else {}
|
|
|
|
failures: list[str] = []
|
|
for path, minimum in sorted(contracted.items()):
|
|
entry = files.get(path)
|
|
if not isinstance(entry, dict):
|
|
failures.append(f"{path}: missing from coverage report (minimum {minimum}%)")
|
|
continue
|
|
percent = _file_percent(entry)
|
|
if percent is None:
|
|
failures.append(f"{path}: no per-file percentage in coverage report")
|
|
continue
|
|
if percent < float(minimum):
|
|
failures.append(f"{path}: {percent:.1f}% < {float(minimum):.1f}%")
|
|
|
|
if failures:
|
|
print("Coverage contract failed:")
|
|
for failure in failures:
|
|
print(f"- {failure}")
|
|
return 1
|
|
|
|
print(f"Coverage contract passed for {len(contracted)} files.")
|
|
return 0
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("coverage_json", type=Path)
|
|
parser.add_argument("contract_json", type=Path)
|
|
args = parser.parse_args()
|
|
return check_coverage_contract(args.coverage_json, args.contract_json)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|