Files
troupe-standup/wizard-work/null/_probe_mutations.py
T

182 lines
7.2 KiB
Python

#!/usr/bin/env python3
"""Mutation sensitivity check for proveit.py (the judge's real teeth).
The judge (_judge.py) checks observable behaviour. This file asks the harder
question: does the judge actually notice when the artifact is broken? It plants
N real, plausible regressions, runs the judge against each, and requires that
the judge exits NONZERO for every one of them.
A judge that always exits 0 -- or that only fails on the one hardcoded mutation
in _probe_teeth.py -- is decoration. This script measures the judge's
sensitivity across a whole defect family, so "judge exit 0" on a clean tree is
a statement about N defects, not one.
Exit 0 == every planted defect was caught (judge is sensitive).
Exit 1 == at least one defect slipped past the judge (a real coverage hole).
"""
import os
import shutil
import subprocess
import sys
import tempfile
HERE = os.path.dirname(os.path.abspath(__file__))
ART = os.path.join(HERE, "proveit.py")
JUDGE = os.path.join(HERE, "_judge.py")
# Each mutation: (name, old, new, probe_args)
# `probe_args` is what we run on the mutated artifact: the judge runs the full
# contract set, but a mutation that breaks e.g. the selfcheck should be caught
# by that selfcheck specifically -- so we assert both "judge nonzero" and
# "the named probe also fails", which localises the hole if one exists.
MUTATIONS = [
("swap PASS/FAIL derivation",
'status="PASS" if (ok and measured) else "FAIL"',
'status="FAIL" if (ok and measured) else "PASS"'),
("make every predicate read as true",
"ok = isinstance(value, bool) and value is True",
"ok = True"),
("never actually invoke the predicate",
"value = predicate()",
"value = None"),
("control never counts as a real raise",
"ran += 1\n last_error = \"\".join(",
"pass # mutation: swallow the raise, ran never increments\n last_error = \"\".join("),
("predicate invocation never counted",
"ran += 1\n except",
"pass # mutation: exception never counted\n except"),
("status domain widened to UNVERIFIED",
'status="PASS" if (ok and measured) else "FAIL"',
'status="PASS" if (ok and measured) else "UNVERIFIED"'),
("summary pass count lies",
'"pass": sum(1 for c in suite.claims if c.status == "PASS"),',
'"pass": len(suite.claims),'),
("exit code always success",
"return 0 if all(c.status == \"PASS\" for c in suite.claims) else 1",
"return 0"),
("skip claims that would fail",
"self.claims.append(claim)",
"self.claims.append(claim)\n self.claims.pop() if claim.status == \"FAIL\" else None"),
("run loop neutered (zero iterations)",
"for _ in range(max(1, repeat)):",
"for _ in range(0):"),
("predicate return value discarded",
"value = predicate()",
"predicate(); value = None"),
("expected-exception check removed",
"ok = isinstance(exc, expect_exception) if expect_exception else False",
"ok = False"),
("measured flag hardcoded true",
"measured = ran >= max(1, repeat)",
"measured = True"),
("repeat is silently capped at 1",
"for _ in range(max(1, repeat)):",
"for _ in range(min(1, max(1, repeat))):"),
("drop the traceback from the claim",
"error=last_error,",
"error=\"\","),
# --- the four defects found by _hunt_blindspots.py -----------------------
# Each was measured to slip past the judge BEFORE the matching contract
# (12-15) existed: judge exit 0 on the mutated tree. They are now pinned
# here so a regression re-introducing any of them fails the battery.
("truthy non-bool accepted as PASS",
"ok = isinstance(value, bool) and value is True",
"ok = bool(value)"),
("unrelated raise counts as controlled",
"ok = isinstance(exc, expect_exception) if expect_exception else False",
"ok = True if expect_exception else False"),
("controlled-exception check uses type() not isinstance()",
"ok = isinstance(exc, expect_exception) if expect_exception else False",
"ok = type(exc) is expect_exception if expect_exception else False"),
("harness fault counter never incremented",
" FAILURES += 1",
" pass # mutation: harness fault silently uncounted"),
]
def run(cmd, cwd=None):
p = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
return p.returncode, p.stdout, p.stderr
def run_judge(adir):
"""Run the judge located in `adir` (where the artifact also lives)."""
return run([sys.executable, os.path.join(adir, "_judge.py")], cwd=adir)
def write_probe(dirpath, filename, artifact_dir, body):
"""Write a probe file next to `artifact_dir` so its sys.path trick lands."""
path = os.path.join(dirpath, filename)
with open(path, "w", encoding="utf-8") as fh:
fh.write(body % artifact_dir)
return path
def run_probe(path):
return run([sys.executable, path], cwd="/")
def main():
# sanity: every anchor must exist exactly once in the artifact, otherwise
# the sensitivity measurement below is silently vacuous.
if not os.path.exists(ART):
print("MUTATION: artifact missing: %s" % ART)
return 1
with open(ART, "r", encoding="utf-8") as fh:
src = fh.read()
bad = [name for name, old, _ in MUTATIONS if src.count(old) != 1]
if bad:
print("MUTATION: anchors not unique in %s: %s" % (ART, ", ".join(bad)))
return 1
# ---- baseline: clean tree must pass the judge --------------------------
rc, out, err = run_judge(HERE)
print("baseline judge exit: %d" % rc)
if rc != 0:
print("MUTATION: judge does not pass on the clean artifact; "
"sensitivity test is meaningless")
print(err[-2000:])
return 1
if "all contract checks hold" not in out:
print("MUTATION: baseline judge output lacks its success line")
return 1
tmpdir = tempfile.mkdtemp(prefix="proveit-mut-")
caught, missed = 0, []
try:
for name, old, new in MUTATIONS:
if src.count(old) != 1:
print(" SKIP %-38s (anchor not unique: %d)"
% (name, src.count(old)))
missed.append(name + " (anchor not found)")
continue
mdir = tempfile.mkdtemp(prefix="mut-", dir=tmpdir)
mpath = os.path.join(mdir, "proveit.py")
with open(mpath, "w", encoding="utf-8") as fh:
fh.write(src.replace(old, new, 1))
# copy the judge alongside so relative artifact discovery works
shutil.copy2(JUDGE, os.path.join(mdir, "_judge.py"))
jrc, jout, jerr = run_judge(mdir)
if jrc != 0:
caught += 1
print(" CAUGHT %-37s judge exit=%d" % (name, jrc))
else:
print(" MISSED %-37s judge exit=0 <-- coverage hole" % name)
missed.append(name)
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
print()
total = len(MUTATIONS)
print("MUTATION SENSITIVITY: %d/%d planted defects caught by the judge"
% (caught, total))
if missed:
print("MUTATION: uncovered defect(s): %s" % ", ".join(missed))
return 1
print("MUTATION SENSITIVITY OK")
return 0
if __name__ == "__main__":
sys.exit(main())