mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-08-18 00:47:23 +02:00
82 lines
2.7 KiB
Python
82 lines
2.7 KiB
Python
#!/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
|
|
|
|
|
|
DEFAULT_MUTATION_SCORE_MINIMUM = 85.0
|
|
|
|
|
|
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, default=DEFAULT_MUTATION_SCORE_MINIMUM)
|
|
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())
|