93 lines
3.4 KiB
Python
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)
|