Files

93 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""Shared mutant-scan harness for the forge probe-refactor verifiers.
``_mutant_scan.py`` (probe_forge) and ``_mutant_scan_triage_dump_pending.py``
were byte-for-byte copies of the same loop -- the only differences were the
source-file set, the subject filename, the verifier command, and the label
column width. Both have been collapsed into thin wrappers that declare those
constants and delegate the loop to :func:`run_scan`, so the scan logic lives
in exactly one place.
A scan copies the verifier + probe files into a fresh temp dir (the real
artifacts in ``wizard-work/forge/`` are never modified), applies each mutant
string-rewrite to the temp copy of the refactored subject, runs the temp copy
of the verifier, and reports ``SURVIVED`` / ``DIED`` per mutant.
Exit 0 = every mutant is killed (verifier robust).
Exit 1 = at least one mutant survived (verifier too weak).
"""
from __future__ import annotations
import shutil
import subprocess
import tempfile
from collections.abc import Callable
from pathlib import Path
#: Root of the forge lane's working output; the files under it are copied --
#: never edited -- into the temp scan directory.
WORK = Path("/root/troupe-standup/wizard-work/forge")
def run_scan(
src_files: list[str],
subject: str,
verifier_cmd: str,
mutants: list[tuple[str, Callable[[str], str]]],
label_width: int,
) -> int:
"""Run one mutant scan and return ``0`` (all killed) or ``1`` (a survivor).
Parameters
----------
src_files:
Filenames, each read from :data:`WORK`, copied into the temp dir.
subject:
Filename in the temp dir whose source each mutant transforms. This is
the refactored probe module under test.
verifier_cmd:
Shell command run with ``cwd`` set to the temp dir; its non-zero exit
is what marks a mutant ``DIED``.
mutants:
``(label, transform)`` pairs. ``transform`` takes the subject source
``str`` and returns the mutated ``str``; a transform that leaves the
source unchanged is reported ``SKIP (no change)``.
label_width:
Column width for the per-mutant label, matching the original wrapper's
``{label:<width>s}`` field so output is byte-identical.
"""
tmp = Path(tempfile.mkdtemp(prefix="mutant_scan_"))
try:
for f in src_files:
shutil.copy2(WORK / f, tmp / f)
ref = tmp / subject
src = ref.read_text()
survivors: list[str] = []
for label, fn in mutants:
mutant_src = fn(src)
if mutant_src == src:
print(f" {label:{label_width}s} SKIP (no change)")
continue
ref.write_text(mutant_src)
p = subprocess.run(
verifier_cmd,
shell=True,
cwd=tmp,
capture_output=True,
text=True,
)
tail_lines = (p.stdout + p.stderr).strip().splitlines()
tail = tail_lines[-1] if tail_lines else "(no output)"
died = p.returncode != 0
status = "DIED" if died else "SURVIVED"
print(
f" {label:{label_width}s} {status:9s} "
f"exit={p.returncode} {tail}"
)
if not died:
survivors.append(label)
print(f"\nSurvivors: {survivors if survivors else 'none'}")
return 1 if survivors else 0
finally:
shutil.rmtree(tmp, ignore_errors=True)