#!/usr/bin/env python3 """Validate machine-readable mutmut CI statistics against a score floor.""" from __future__ import annotations import argparse import json import math from pathlib import Path from typing import Any def mutation_score(stats: dict[str, Any]) -> tuple[int, int, float]: """Return killed, total, and percentage after validating mutmut statistics.""" killed = stats.get("killed") total = stats.get("total") if isinstance(killed, bool) or not isinstance(killed, int) or killed < 0: raise ValueError("mutation statistics require a non-negative integer 'killed'") if isinstance(total, bool) or not isinstance(total, int) or total <= 0: raise ValueError("mutation statistics require a positive integer 'total'") if killed > total: raise ValueError("mutation statistics cannot report more killed mutants than total") return killed, total, killed / total * 100 def validate_mutation_stats( stats: dict[str, Any], *, minimum: float, ) -> list[str]: """Return failures for an incomplete run or a score below the policy floor.""" if not math.isfinite(minimum) or minimum < 0 or minimum > 100: return ["minimum mutation score must be between 0 and 100"] try: killed, total, score = mutation_score(stats) except ValueError as exc: return [str(exc)] failures: list[str] = [] if stats.get("check_was_interrupted_by_user"): failures.append("mutation run was interrupted") if score < minimum: failures.append( f"mutation score {score:.2f}% ({killed}/{total}) is below " f"the {minimum:.2f}% floor", ) return failures def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("stats", type=Path, help="mutmut-cicd-stats.json path") parser.add_argument("--minimum", type=float, required=True) return parser def main() -> int: args = _parser().parse_args() try: stats = json.loads(args.stats.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: print(f"mutation gate failed: cannot read statistics: {exc}") return 1 if not isinstance(stats, dict): print("mutation gate failed: statistics root must be an object") return 1 failures = validate_mutation_stats(stats, minimum=args.minimum) if failures: for failure in failures: print(f"mutation gate failed: {failure}") return 1 killed, total, score = mutation_score(stats) print(f"mutation gate passed: {score:.2f}% ({killed}/{total})") return 0 if __name__ == "__main__": raise SystemExit(main())