mirror of
https://github.com/elder-plinius/OBLITERATUS.git
synced 2026-08-17 16:37:30 +02:00
test: add Gate 3 numerical oracle contracts
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Fail closed when configured mutmut targets produce no mutants."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from importlib import metadata
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try: # Python 3.11+
|
||||
import tomllib
|
||||
except ModuleNotFoundError: # pragma: no cover - exercised on Python 3.10
|
||||
import tomli as tomllib # type: ignore[no-redef]
|
||||
|
||||
|
||||
GLOB_MARKERS = frozenset("*?[")
|
||||
SUPPORTED_MUTMUT_VERSION = "3.7.0"
|
||||
MUTMUT_NO_TEST_EXIT_CODES = frozenset({5, 33})
|
||||
|
||||
|
||||
def load_mutmut_config(pyproject: Path) -> dict[str, Any]:
|
||||
"""Return the `[tool.mutmut]` table from pyproject.toml."""
|
||||
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
||||
mutmut_config = data.get("tool", {}).get("mutmut", {})
|
||||
if not isinstance(mutmut_config, dict):
|
||||
raise ValueError("[tool.mutmut] must be a table")
|
||||
return mutmut_config
|
||||
|
||||
|
||||
def exact_python_targets(mutmut_config: dict[str, Any]) -> list[Path]:
|
||||
"""Return exact `.py` paths that must enumerate mutants."""
|
||||
raw_targets = mutmut_config.get("required_mutation_targets", mutmut_config.get("only_mutate", []))
|
||||
if not isinstance(raw_targets, list) or not all(isinstance(path, str) for path in raw_targets):
|
||||
raise ValueError(
|
||||
"[tool.mutmut].required_mutation_targets must be a list of strings",
|
||||
)
|
||||
|
||||
targets: list[Path] = []
|
||||
strict_required = "required_mutation_targets" in mutmut_config
|
||||
for raw_path in raw_targets:
|
||||
path = Path(raw_path)
|
||||
invalid = (
|
||||
path.is_absolute()
|
||||
or ".." in path.parts
|
||||
or not raw_path.endswith(".py")
|
||||
or any(marker in raw_path for marker in GLOB_MARKERS)
|
||||
)
|
||||
if invalid:
|
||||
if strict_required:
|
||||
raise ValueError(f"invalid required mutation target: {raw_path}")
|
||||
continue
|
||||
targets.append(path)
|
||||
return sorted(targets, key=str)
|
||||
|
||||
|
||||
def _owned_mutants_dir(project_root: Path, mutants_dir: Path) -> Path:
|
||||
if mutants_dir != Path("mutants"):
|
||||
raise ValueError(
|
||||
"mutants directory must be project-owned as the literal project-owned mutants directory",
|
||||
)
|
||||
root = project_root.resolve()
|
||||
configured = root / "mutants"
|
||||
if configured.is_symlink():
|
||||
raise ValueError(
|
||||
"mutants directory must be project-owned as the literal project-owned mutants directory",
|
||||
)
|
||||
return configured
|
||||
|
||||
|
||||
def _owned_artifact(project_root: Path, mutants_dir: Path, target: Path, suffix: str) -> Path:
|
||||
configured = _owned_mutants_dir(project_root, mutants_dir)
|
||||
artifact = configured / f"{target}{suffix}"
|
||||
resolved = artifact.resolve() if artifact.exists() else artifact.resolve(strict=False)
|
||||
try:
|
||||
resolved.relative_to(configured)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"mutation artifact escapes project-owned mutants/: {artifact}") from exc
|
||||
return artifact
|
||||
|
||||
|
||||
def _validate_owned_source(project_root: Path, target: Path) -> Path:
|
||||
source_path = project_root / target
|
||||
if not source_path.is_file():
|
||||
raise ValueError(f"configured mutation target does not exist: {target}")
|
||||
try:
|
||||
source_path.resolve().relative_to(project_root.resolve())
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"configured mutation target escapes project root: {target}") from exc
|
||||
return source_path
|
||||
|
||||
|
||||
def mutant_count(meta_path: Path) -> int:
|
||||
"""Return the number of generated mutants recorded in a mutmut metadata file."""
|
||||
try:
|
||||
metadata = json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
return 0
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"{meta_path} is not valid JSON: {exc}") from exc
|
||||
|
||||
exit_code_by_key = metadata.get("exit_code_by_key")
|
||||
if not isinstance(exit_code_by_key, dict):
|
||||
raise ValueError(f"{meta_path} does not contain an exit_code_by_key object")
|
||||
return len(exit_code_by_key)
|
||||
|
||||
|
||||
def _installed_mutmut_version() -> str | None:
|
||||
try:
|
||||
return metadata.version("mutmut")
|
||||
except metadata.PackageNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def assert_supported_mutmut_no_test_codes() -> None:
|
||||
version = _installed_mutmut_version()
|
||||
if version != SUPPORTED_MUTMUT_VERSION:
|
||||
raise ValueError(
|
||||
f"unsupported mutmut version {version!r}; expected {SUPPORTED_MUTMUT_VERSION} "
|
||||
"before interpreting no-test exit codes",
|
||||
)
|
||||
|
||||
|
||||
def no_test_mutants(meta_path: Path) -> list[str]:
|
||||
"""Return required-target mutants that mutmut marked as having no covering tests."""
|
||||
try:
|
||||
metadata = json.loads(meta_path.read_text(encoding="utf-8"))
|
||||
except FileNotFoundError:
|
||||
return []
|
||||
except json.JSONDecodeError as exc:
|
||||
raise ValueError(f"{meta_path} is not valid JSON: {exc}") from exc
|
||||
|
||||
exit_code_by_key = metadata.get("exit_code_by_key")
|
||||
if not isinstance(exit_code_by_key, dict):
|
||||
raise ValueError(f"{meta_path} does not contain an exit_code_by_key object")
|
||||
no_tests = sorted(
|
||||
key for key, exit_code in exit_code_by_key.items()
|
||||
if exit_code in MUTMUT_NO_TEST_EXIT_CODES
|
||||
)
|
||||
if no_tests:
|
||||
assert_supported_mutmut_no_test_codes()
|
||||
return no_tests
|
||||
|
||||
|
||||
def stale_or_empty_targets(
|
||||
targets: list[Path], *, project_root: Path = Path("."), mutants_dir: Path = Path("mutants"),
|
||||
) -> list[Path]:
|
||||
"""Return configured targets whose mutant metadata is missing or empty."""
|
||||
stale: list[Path] = []
|
||||
_owned_mutants_dir(project_root, mutants_dir)
|
||||
for target in targets:
|
||||
_validate_owned_source(project_root, target)
|
||||
if mutant_count(_owned_artifact(project_root, mutants_dir, target, ".meta")) == 0:
|
||||
stale.append(target)
|
||||
return stale
|
||||
|
||||
|
||||
def prepare_required_targets(
|
||||
pyproject: Path = Path("pyproject.toml"), *, mutants_dir: Path = Path("mutants"),
|
||||
) -> list[Path]:
|
||||
"""Remove stale copied mutant files so mutmut regenerates required target metadata."""
|
||||
targets = exact_python_targets(load_mutmut_config(pyproject))
|
||||
stale = stale_or_empty_targets(targets, project_root=pyproject.parent, mutants_dir=mutants_dir)
|
||||
for target in stale:
|
||||
for suffix in ("", ".meta", ".spans"):
|
||||
artifact = _owned_artifact(pyproject.parent, mutants_dir, target, suffix)
|
||||
if artifact.exists():
|
||||
artifact.unlink()
|
||||
return stale
|
||||
|
||||
|
||||
def validate_required_targets(
|
||||
pyproject: Path = Path("pyproject.toml"), *, mutants_dir: Path = Path("mutants"),
|
||||
) -> list[str]:
|
||||
"""Return fail-closed messages for exact mutation targets with zero generated mutants."""
|
||||
targets = exact_python_targets(load_mutmut_config(pyproject))
|
||||
failures: list[str] = []
|
||||
for target in stale_or_empty_targets(targets, project_root=pyproject.parent, mutants_dir=mutants_dir):
|
||||
failures.append(f"configured mutation target produced zero mutants: {target}")
|
||||
for target in targets:
|
||||
no_tests = no_test_mutants(_owned_artifact(pyproject.parent, mutants_dir, target, ".meta"))
|
||||
if no_tests:
|
||||
failures.append(
|
||||
f"configured mutation target has {len(no_tests)} mutant(s) with no tests: {target}",
|
||||
)
|
||||
return failures
|
||||
|
||||
|
||||
def _parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("command", choices=("prepare", "check"))
|
||||
parser.add_argument("--pyproject", type=Path, default=Path("pyproject.toml"))
|
||||
parser.add_argument("--mutants-dir", type=Path, default=Path("mutants"))
|
||||
return parser
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = _parser().parse_args()
|
||||
try:
|
||||
if args.command == "prepare":
|
||||
stale = prepare_required_targets(args.pyproject, mutants_dir=args.mutants_dir)
|
||||
if stale:
|
||||
joined = ", ".join(str(path) for path in stale)
|
||||
print(f"mutation target guard invalidated stale metadata for: {joined}")
|
||||
else:
|
||||
print("mutation target guard found no stale required targets")
|
||||
return 0
|
||||
|
||||
failures = validate_required_targets(args.pyproject, mutants_dir=args.mutants_dir)
|
||||
except (OSError, ValueError) as exc:
|
||||
print(f"mutation target guard failed: {exc}")
|
||||
return 1
|
||||
|
||||
if failures:
|
||||
for failure in failures:
|
||||
print(f"mutation target guard failed: {failure}")
|
||||
return 1
|
||||
print("mutation target guard passed")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user