test: add Gate 3 numerical oracle contracts

This commit is contained in:
Joseph Magly
2026-08-15 20:09:21 -04:00
parent 369417fbb4
commit 51529ad1d7
25 changed files with 3443 additions and 170 deletions
+4 -1
View File
@@ -10,6 +10,9 @@ 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")
@@ -48,7 +51,7 @@ def validate_mutation_stats(
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)
parser.add_argument("--minimum", type=float, default=DEFAULT_MUTATION_SCORE_MINIMUM)
return parser
+224
View File
@@ -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())
+1 -1
View File
@@ -18,7 +18,7 @@ BASELINE_FLOORS = {
"changed_line": 95.0,
"mature_cpu_statement": 92.0,
"mature_cpu_branch": 80.0,
"mutation_score": 75.0,
"mutation_score": 85.0,
"warning_budget": 0.0,
}
@@ -0,0 +1,60 @@
"""Fail-closed mutmut prepared-artifact reuse hooks for CI."""
from __future__ import annotations
import os
if (
os.environ.get("OBLITERATUS_MUTMUT_REUSE_COVERAGE") == "1"
or os.environ.get("OBLITERATUS_MUTMUT_REUSE_STATS") == "1"
or os.environ.get("OBLITERATUS_MUTMUT_SUBPROCESS_PREFLIGHT") == "1"
):
from mutmut import __main__ as mutmut_main
from scripts.prepare_mutation_coverage import validate_execution_manifest
from scripts.prepare_mutation_coverage import validate_manifest
def store_lines_covered_by_tests_from_prepared_artifacts() -> None:
validate_manifest()
print("Reusing prepared mutmut covered-line artifacts")
mutmut_main.store_lines_covered_by_tests = store_lines_covered_by_tests_from_prepared_artifacts
if os.environ.get("OBLITERATUS_MUTMUT_REUSE_STATS") == "1":
def collect_or_load_prepared_stats(
runner,
*,
mutants_caught_by_type_checker=None,
apply_config_invalidation=False,
invalidate_stale_callers=True,
) -> None:
del runner, mutants_caught_by_type_checker
del apply_config_invalidation, invalidate_stale_callers
validate_execution_manifest()
if not mutmut_main.load_stats():
raise RuntimeError("prepared mutmut stats failed to load")
print("Reusing prepared mutmut test-selection stats")
mutmut_main.collect_or_load_stats = collect_or_load_prepared_stats
if os.environ.get("OBLITERATUS_MUTMUT_SUBPROCESS_PREFLIGHT") == "1":
_original_execute_pytest = mutmut_main.PytestRunner.execute_pytest
def execute_pytest_with_subprocess_preflight(self, params, **kwargs):
if kwargs:
return _original_execute_pytest(self, params, **kwargs)
if os.environ.get("MUTANT_UNDER_TEST") not in {"", "fail"}:
return _original_execute_pytest(self, params, **kwargs)
import subprocess
import sys
full_params = ["--rootdir=.", "--tb=native", *params, *self._pytest_add_cli_args]
result = subprocess.run([sys.executable, "-m", "pytest", *full_params], check=False)
if result.returncode == 4:
raise mutmut_main.BadTestExecutionCommandsException(full_params)
return int(result.returncode)
mutmut_main.PytestRunner.execute_pytest = execute_pytest_with_subprocess_preflight
+451
View File
@@ -0,0 +1,451 @@
#!/usr/bin/env python3
"""Prepare mutmut artifacts in throwaway processes before execution."""
from __future__ import annotations
import argparse
import hashlib
import inspect
import json
import os
from pathlib import Path
import sys
from typing import Any
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.check_mutation_targets import exact_python_targets
from scripts.check_mutation_targets import load_mutmut_config
from scripts.check_mutation_targets import SUPPORTED_MUTMUT_VERSION
MANIFEST_PATH = Path("mutants/.covered-lines-prepass.json")
STATS_PATH = Path("mutants/mutmut-stats.json")
HOOK_HASH_PATHS = (
Path("scripts/prepare_mutation_coverage.py"),
Path("scripts/run_prepared_mutmut.py"),
Path("scripts/mutmut_coverage_sitecustomize/sitecustomize.py"),
)
META_KEYS = {
"exit_code_by_key",
"hash_by_function_name",
"type_check_error_by_key",
"durations_by_key",
"estimated_durations_by_key",
}
def _sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _validated_relative_file(raw_path: str, *, label: str) -> Path:
path = Path(raw_path)
if path.is_absolute() or ".." in path.parts:
raise RuntimeError(f"{label} must be a project-relative file: {raw_path}")
if not path.is_file():
raise RuntimeError(f"{label} is missing: {raw_path}")
return path
def _hash_relative_files(raw_paths: list[str], *, label: str) -> dict[str, str]:
return {
str(path): _sha256(path)
for path in (_validated_relative_file(raw_path, label=label) for raw_path in raw_paths)
}
def _validated_mutation_path(path: Path) -> Path:
if path.is_absolute() or ".." in path.parts:
raise RuntimeError(f"mutation path must be project-relative: {path}")
return path
def _owned_mutants_root() -> Path:
root = Path.cwd().resolve()
expected = root / "mutants"
if expected.is_symlink():
raise RuntimeError(
"mutants directory must be project-owned as the literal project-owned mutants directory",
)
return expected
def _owned_mutation_artifact(path: Path, suffix: str) -> Path:
relative = _validated_mutation_path(path)
root = _owned_mutants_root()
artifact = Path("mutants") / f"{relative}{suffix}"
resolved = artifact.resolve() if artifact.exists() else artifact.resolve(strict=False)
try:
resolved.relative_to(root)
except ValueError as exc:
raise RuntimeError(f"mutation artifact escapes project-owned mutants/: {artifact}") from exc
return artifact
def _mutmut_version() -> str:
import mutmut
return getattr(mutmut, "__version__", "")
def assert_supported_mutmut() -> None:
"""Fail closed if the mutmut internals this script calls have changed."""
from mutmut import __main__ as mutmut_main
if _mutmut_version() != SUPPORTED_MUTMUT_VERSION:
raise RuntimeError(
f"unsupported mutmut version {_mutmut_version()!r}; expected {SUPPORTED_MUTMUT_VERSION}",
)
expected = {
"copy_src_dir": ("() -> 'None'",),
"copy_also_copy_files": ("() -> 'None'",),
"setup_source_paths": ("() -> 'None'",),
"create_mutants": ("(max_children: 'int') -> 'MutantGenerationStats'",),
"collect_or_load_stats": (
"(runner: 'TestRunner', *, mutants_caught_by_type_checker: 'dict[str, Any] | None' = None, "
"apply_config_invalidation: 'bool' = False, invalidate_stale_callers: 'bool' = True) -> 'None'",
"(runner, *, mutants_caught_by_type_checker=None, apply_config_invalidation=False, "
"invalidate_stale_callers=True) -> 'None'",
),
"load_stats": ("() -> 'bool'",),
}
for name, signatures in expected.items():
actual = str(inspect.signature(getattr(mutmut_main, name)))
if actual not in signatures:
raise RuntimeError(f"mutmut internal {name} signature changed: {actual}")
def configured_paths() -> tuple[list[Path], list[Path]]:
"""Return all mutatable paths and exact required paths from the loaded config."""
from mutmut import __main__ as mutmut_main
from mutmut.configuration import Config
Config.ensure_loaded()
if not Config.get().mutate_only_covered_lines:
raise RuntimeError("[tool.mutmut].mutate_only_covered_lines must be true")
mutatable = sorted(mutmut_main.walk_mutatable_files(), key=str)
required = exact_python_targets(load_mutmut_config(Path("pyproject.toml")))
missing = [path for path in required if path not in mutatable]
if missing:
joined = ", ".join(str(path) for path in missing)
raise RuntimeError(f"required mutation target is not mutatable by mutmut config: {joined}")
if not mutatable:
raise RuntimeError("mutmut configuration produced no mutatable files")
return mutatable, required
def remove_mutation_artifacts(paths: list[Path]) -> None:
"""Remove generated files whose freshness matters for covered-line reuse."""
for path in paths:
for suffix in ("", ".meta", ".spans"):
artifact = _owned_mutation_artifact(path, suffix)
if artifact.exists():
artifact.unlink()
if MANIFEST_PATH.exists():
MANIFEST_PATH.unlink()
def _read_meta(path: Path) -> dict[str, Any]:
meta_path = Path("mutants") / f"{path}.meta"
try:
metadata = json.loads(meta_path.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise RuntimeError(f"missing mutmut metadata for {path}") from exc
except json.JSONDecodeError as exc:
raise RuntimeError(f"malformed mutmut metadata for {path}: {exc}") from exc
if not isinstance(metadata, dict) or set(metadata) != META_KEYS:
raise RuntimeError(f"unexpected mutmut metadata schema for {path}: {sorted(metadata)}")
if not isinstance(metadata["exit_code_by_key"], dict):
raise RuntimeError(f"mutmut metadata exit_code_by_key is not an object for {path}")
return metadata
def _validate_spans(path: Path, metadata: dict[str, Any]) -> None:
from mutmut.mutation.data import MutantLineSpans
spans = MutantLineSpans.load(path)
if spans is None:
raise RuntimeError(f"missing or unsupported mutmut spans index for {path}")
generated_names = {key.rpartition(".")[2] for key in metadata["exit_code_by_key"]}
missing = sorted(generated_names - set(spans.span_by_function_name))
if missing:
raise RuntimeError(f"mutmut spans index for {path} is missing mutants: {missing[:5]}")
def validate_prepared_artifacts(
*,
mutatable: list[Path],
required: list[Path],
covered_lines: dict[str, set[int]] | None = None,
) -> dict[str, Any]:
"""Validate generated artifacts and return a fresh-source manifest."""
from mutmut.configuration import Config
source_hashes: dict[str, str] = {}
selected_test_hashes: dict[str, str] = {}
hook_hashes: dict[str, str] = {}
covered_line_counts: dict[str, int] = {}
mutant_counts: dict[str, int] = {}
for path in mutatable:
source = _validated_mutation_path(Path(path))
mutant = _owned_mutation_artifact(path, "")
if not source.is_file():
raise RuntimeError(f"configured mutation source does not exist: {path}")
if not mutant.is_file() or source.stat().st_mtime >= mutant.stat().st_mtime:
raise RuntimeError(f"mutmut artifact for {path} is missing or stale")
metadata = _read_meta(path)
_validate_spans(path, metadata)
source_hashes[str(path)] = _sha256(source)
mutant_counts[str(path)] = len(metadata["exit_code_by_key"])
if covered_lines is not None:
key = str((Path("mutants") / path).absolute())
covered_line_counts[str(path)] = len(covered_lines.get(key, set()))
for path in required:
if mutant_counts.get(str(path), 0) == 0:
raise RuntimeError(f"required mutation target produced zero covered mutants: {path}")
if covered_lines is not None and covered_line_counts.get(str(path), 0) == 0:
raise RuntimeError(f"required mutation target had no covered lines: {path}")
config = Config.get()
selected_test_hashes = _hash_relative_files(
list(config.pytest_add_cli_args_test_selection),
label="selected mutation test file",
)
hook_hashes = _hash_relative_files([str(path) for path in HOOK_HASH_PATHS], label="mutation hook")
return {
"version": 1,
"mutmut_version": _mutmut_version(),
"mutate_only_covered_lines": config.mutate_only_covered_lines,
"source_paths": [str(path) for path in config.source_paths],
"only_mutate": list(config.only_mutate),
"pytest_add_cli_args": list(config.pytest_add_cli_args),
"pytest_add_cli_args_test_selection": list(config.pytest_add_cli_args_test_selection),
"required_mutation_targets": [str(path) for path in required],
"mutatable_paths": [str(path) for path in mutatable],
"source_hashes": source_hashes,
"selected_test_hashes": selected_test_hashes,
"hook_hashes": hook_hashes,
"covered_line_counts": covered_line_counts,
"mutant_counts": mutant_counts,
}
def prepare(max_children: int) -> dict[str, Any]:
"""Collect coverage, generate covered-only mutants, validate them, and write a manifest."""
import mutmut
from mutmut import __main__ as mutmut_main
from mutmut.code_coverage import gather_coverage
from mutmut.state import state
assert_supported_mutmut()
mutatable, required = configured_paths()
Path("mutants").mkdir(exist_ok=True)
remove_mutation_artifacts(mutatable)
mutmut_main.copy_src_dir()
mutmut_main.copy_also_copy_files()
mutmut_main.setup_source_paths()
source_files = list(mutmut_main.walk_source_files())
covered_lines = gather_coverage(mutmut_main.PytestRunner(), source_files)
mutmut._covered_lines = covered_lines
stats = mutmut_main.create_mutants(max_children)
state().current_function_hashes.clear()
manifest = validate_prepared_artifacts(
mutatable=mutatable,
required=required,
covered_lines=covered_lines,
)
manifest["generation_stats"] = {
"mutated": stats.mutated,
"ignored": stats.ignored,
"unmodified": stats.unmodified,
}
MANIFEST_PATH.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return manifest
def validate_coverage_manifest() -> dict[str, Any]:
"""Validate that a prior prepass still matches the current sources and config."""
assert_supported_mutmut()
mutatable, required = configured_paths()
try:
manifest = json.loads(MANIFEST_PATH.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise RuntimeError(f"missing covered-line prepass manifest: {MANIFEST_PATH}") from exc
except json.JSONDecodeError as exc:
raise RuntimeError(f"malformed covered-line prepass manifest: {exc}") from exc
expected = validate_prepared_artifacts(mutatable=mutatable, required=required)
comparable_keys = (
"version",
"mutmut_version",
"mutate_only_covered_lines",
"source_paths",
"only_mutate",
"pytest_add_cli_args",
"pytest_add_cli_args_test_selection",
"required_mutation_targets",
"mutatable_paths",
"source_hashes",
"selected_test_hashes",
"hook_hashes",
)
for key in comparable_keys:
if manifest.get(key) != expected[key]:
raise RuntimeError(f"covered-line prepass manifest is stale for {key}")
return manifest
def validate_manifest() -> dict[str, Any]:
"""Backward-compatible name for covered-line manifest validation."""
return validate_coverage_manifest()
def _read_stats() -> dict[str, Any]:
try:
stats = json.loads(STATS_PATH.read_text(encoding="utf-8"))
except FileNotFoundError as exc:
raise RuntimeError(f"missing prepared mutmut stats: {STATS_PATH}") from exc
except json.JSONDecodeError as exc:
raise RuntimeError(f"malformed prepared mutmut stats: {exc}") from exc
if not isinstance(stats, dict):
raise RuntimeError("prepared mutmut stats root must be an object")
return stats
def validate_stats_artifacts() -> dict[str, Any]:
"""Validate prepared mutmut test-selection stats and return manifest fields."""
stats = _read_stats()
expected_keys = {
"tests_by_mangled_function_name",
"duration_by_test",
"stats_time",
"function_hashes",
"function_dependencies",
"config_fingerprint",
"watched_file_hashes",
"git_commit",
}
if set(stats) != expected_keys:
raise RuntimeError(f"unexpected mutmut stats schema: {sorted(stats)}")
tests_by_function = stats["tests_by_mangled_function_name"]
duration_by_test = stats["duration_by_test"]
function_hashes = stats["function_hashes"]
if not isinstance(tests_by_function, dict) or not tests_by_function:
raise RuntimeError("prepared mutmut stats have no test-to-function mapping")
if not isinstance(duration_by_test, dict) or not duration_by_test:
raise RuntimeError("prepared mutmut stats have no test duration mapping")
if not isinstance(function_hashes, dict) or not function_hashes:
raise RuntimeError("prepared mutmut stats have no function hash baseline")
associated_tests = {
test
for tests in tests_by_function.values()
if isinstance(tests, list)
for test in tests
}
if not associated_tests:
raise RuntimeError("prepared mutmut stats do not associate any tests with mutants")
missing_durations = sorted(associated_tests - set(duration_by_test))
if missing_durations:
raise RuntimeError(
"prepared mutmut stats reference tests without durations: "
f"{missing_durations[:5]}",
)
return {
"stats_hash": _sha256(STATS_PATH),
"stats_function_count": len(function_hashes),
"stats_test_count": len(duration_by_test),
"stats_association_count": sum(
len(tests) for tests in tests_by_function.values() if isinstance(tests, list)
),
}
def prepare_stats(max_children: int) -> dict[str, Any]:
"""Build mutmut test-selection stats in a fresh process, then exit."""
from mutmut import __main__ as mutmut_main
from mutmut.state import state
manifest = validate_coverage_manifest()
if STATS_PATH.exists():
STATS_PATH.unlink()
mutmut_main.copy_src_dir()
mutmut_main.copy_also_copy_files()
mutmut_main.setup_source_paths()
mutmut_main.create_mutants(max_children)
runner = mutmut_main.PytestRunner()
runner.prepare_main_test_run()
mutmut_main.collect_or_load_stats(
runner,
mutants_caught_by_type_checker={},
apply_config_invalidation=False,
invalidate_stale_callers=False,
)
if not state().current_function_hashes:
raise RuntimeError("mutmut stats prepass did not populate function hashes")
manifest.update(validate_stats_artifacts())
MANIFEST_PATH.write_text(json.dumps(manifest, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return manifest
def validate_execution_manifest() -> dict[str, Any]:
"""Validate coverage artifacts plus prepared test-selection stats."""
manifest = validate_coverage_manifest()
stats_fields = validate_stats_artifacts()
for key, value in stats_fields.items():
if manifest.get(key) != value:
raise RuntimeError(f"prepared mutmut execution manifest is stale for {key}")
return manifest
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"command",
choices=("prepare", "prepare-coverage", "prepare-stats", "validate", "validate-execution"),
)
parser.add_argument("--max-children", type=int, default=4)
return parser
def main() -> int:
args = _parser().parse_args()
try:
if args.command in {"prepare", "prepare-coverage"}:
manifest = prepare(args.max_children)
total = sum(manifest["mutant_counts"].values())
print(f"prepared covered-line mutation artifacts for {total} mutants")
elif args.command == "prepare-stats":
manifest = prepare_stats(args.max_children)
print(
"prepared mutmut test-selection stats for "
f"{manifest['stats_association_count']} function/test associations",
)
elif args.command == "validate-execution":
validate_execution_manifest()
print("prepared mutation execution artifacts are fresh")
else:
validate_coverage_manifest()
print("covered-line mutation artifacts are fresh")
except Exception as exc:
print(f"mutation coverage preparation failed: {exc}")
return 1
return 0
if __name__ == "__main__":
exit_code = main()
sys.stdout.flush()
sys.stderr.flush()
os._exit(exit_code)
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Run mutmut with prepared coverage and stats artifacts in a fresh interpreter."""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import shutil
PROJECT_ROOT = Path(__file__).resolve().parents[1]
SITECUSTOMIZE = PROJECT_ROOT / "scripts" / "mutmut_coverage_sitecustomize"
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("mutmut_args", nargs=argparse.REMAINDER)
return parser
def main() -> int:
args = _parser().parse_args()
mutmut = shutil.which("mutmut")
if mutmut is None:
print("prepared mutmut runner failed: mutmut executable is not on PATH")
return 1
pythonpath = os.environ.get("PYTHONPATH")
prefixes = [str(SITECUSTOMIZE), str(PROJECT_ROOT)]
os.environ["PYTHONPATH"] = os.pathsep.join(
[*prefixes, *([pythonpath] if pythonpath else [])],
)
os.environ["OBLITERATUS_MUTMUT_REUSE_COVERAGE"] = "1"
os.environ["OBLITERATUS_MUTMUT_REUSE_STATS"] = "1"
os.environ["OBLITERATUS_MUTMUT_SUBPROCESS_PREFLIGHT"] = "1"
command = [mutmut, *(args.mutmut_args or ["run"])]
os.execv(mutmut, command)
raise AssertionError("os.execv returned unexpectedly")
if __name__ == "__main__":
raise SystemExit(main())