ariadne/scripts/check_coverage_contract.py

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())