#!/usr/bin/env python3 """Run mutmut and enforce a minimum mutation kill rate.""" from __future__ import annotations import argparse import re import shutil import subprocess import sys from pathlib import Path STATS = re.compile( r"(?P\d+)/(?P\d+)\s+.*? (?P\d+)\s+.*? " r"(?P\d+)\s+.*? (?P\d+)\s+.*? " r"(?P\d+)\s+.*? (?P\d+)\s+.*? " r"(?P\d+)" ) def last_stats(output: str) -> dict[str, int] | None: matches = list(STATS.finditer(output.replace("\r", "\n"))) if not matches: return None return {key: int(value) for key, value in matches[-1].groupdict().items()} def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--min-kill-rate", type=float, required=True) parser.add_argument( "--repository", type=Path, help="Project root to mutate; defaults to the repository root.", ) arguments = parser.parse_args() repository = ( arguments.repository.resolve() if arguments.repository is not None else Path(__file__).resolve().parents[1] ) if not (repository / "pyproject.toml").is_file(): parser.error(f"mutation project has no pyproject.toml: {repository}") mutant_workspace = repository / "mutants" if mutant_workspace.exists(): shutil.rmtree(mutant_workspace) completed = subprocess.run( ["mutmut", "run"], capture_output=True, text=True, check=False, cwd=repository, ) output = completed.stdout + completed.stderr if completed.returncode != 0: sys.stderr.write(output) return completed.returncode stats = last_stats(output) if stats is None or stats["done"] != stats["total"]: sys.stderr.write(output) sys.stderr.write("mutation gate failed: incomplete or unreadable statistics\n") return 2 caught = stats["killed"] + stats["timeout"] catchable = caught + stats["survived"] + stats["uncovered"] if catchable == 0: sys.stderr.write("mutation gate failed: no mutants were generated\n") return 2 rate = 100 * caught / catchable print(f"mutation kill rate: {rate:.1f}% ({caught}/{catchable})") if rate < arguments.min_kill_rate: sys.stderr.write( f"mutation gate failed: {rate:.1f}% is below " f"{arguments.min_kill_rate:.1f}%\n" ) return 1 return 0 if __name__ == "__main__": raise SystemExit(main())