80 lines
2.7 KiB
Python
Executable File
80 lines
2.7 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Enforce per-file coverage thresholds from SlipCover JSON output."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
|
|
|
|
def _normalize_report_path(path: str, cwd: Path) -> str:
|
|
"""Return a stable repository-relative path for a coverage report entry."""
|
|
|
|
candidate = Path(path)
|
|
if candidate.is_absolute():
|
|
try:
|
|
return candidate.relative_to(cwd).as_posix()
|
|
except ValueError:
|
|
return candidate.as_posix()
|
|
return candidate.as_posix()
|
|
|
|
|
|
def _production_files(root: Path, cwd: Path) -> set[str]:
|
|
"""List production Python files that must appear in the coverage report."""
|
|
|
|
required: set[str] = set()
|
|
for path in root.rglob("*.py"):
|
|
if path.name == "__init__.py" or "__pycache__" in path.parts:
|
|
continue
|
|
try:
|
|
required.add(path.relative_to(cwd).as_posix())
|
|
except ValueError:
|
|
required.add(path.as_posix())
|
|
return required
|
|
|
|
|
|
def main() -> int:
|
|
"""Check each production file against a minimum coverage percentage."""
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("coverage_json")
|
|
parser.add_argument("--root", default="atlasbot")
|
|
parser.add_argument("--threshold", type=float, default=95.0)
|
|
args = parser.parse_args()
|
|
|
|
data = json.loads(Path(args.coverage_json).read_text(encoding="utf-8"))
|
|
files = data.get("files") if isinstance(data, dict) else {}
|
|
cwd = Path.cwd().resolve()
|
|
root = Path(args.root)
|
|
root_path = (root if root.is_absolute() else cwd / root).resolve()
|
|
root_prefix = root_path.relative_to(cwd).as_posix() if root_path.is_relative_to(cwd) else root_path.as_posix()
|
|
covered_paths: set[str] = set()
|
|
violations: list[str] = []
|
|
|
|
for path, payload in sorted(files.items()):
|
|
normalized_path = _normalize_report_path(path, cwd)
|
|
if not normalized_path.startswith(f"{root_prefix}/"):
|
|
continue
|
|
summary = payload.get("summary") if isinstance(payload, dict) else {}
|
|
percent = summary.get("percent_covered") if isinstance(summary, dict) else None
|
|
if not isinstance(percent, (int, float)):
|
|
violations.append(f"{normalized_path}: coverage percent missing")
|
|
continue
|
|
covered_paths.add(normalized_path)
|
|
if float(percent) < args.threshold:
|
|
violations.append(f"{normalized_path}: {float(percent):.2f}% < {args.threshold:.2f}%")
|
|
|
|
for path in sorted(_production_files(root_path, cwd) - covered_paths):
|
|
violations.append(f"{path}: missing from coverage report")
|
|
|
|
if violations:
|
|
for violation in sorted(violations):
|
|
print(violation)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|