635 lines
25 KiB
Python
635 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
"""Closed-artifact regression guard: a repaired blob stays repaired.
|
|
|
|
Q127. `899ce4d` ("spark: add verified Fibonacci calculator with assertions", a
|
|
sweep of ten files) rewrote two files that two closed items had repaired the day
|
|
before, and put the pre-repair bytes back:
|
|
|
|
wizard-work/spark/snippet.py Q109 -> reverted to the 3857b60 blob
|
|
wizard-work/spark/python-env-check.py Q120 -> reverted to the c166bf1 blob
|
|
|
|
Neither item's record was edited and neither cell was deleted, so both gates
|
|
went red at HEAD and stayed red: `bash wizard-work/spark/CHECK-env-verifiers`
|
|
read `Ran 16 tests ... FAILED (failures=9)` rc 1, and
|
|
`python3 wizard-work/spark/test_python_env_check.py` read
|
|
`Ran 7 tests ... FAILED (failures=5, errors=2)` rc 1. A red gate is only a guard
|
|
if something runs it, and the spark lane's own CHECK runs none of its cells
|
|
(it grades absolute paths under /root instead -- the Q109/Q120 class).
|
|
|
|
This file is the missing run: it pins the bytes each repair landed, so a later
|
|
whole-file overwrite is a named fault rather than a silent regression. The pins
|
|
are data, not a heuristic, and the pre-repair digests are data too, so the
|
|
diagnosis can say "reverted" instead of "changed".
|
|
|
|
Legs (each prints PASS/FAIL/INFO and names its subject):
|
|
pin:<path> sha256 equals the blob the repair commit landed
|
|
cell:<name> the item's own cell still runs green
|
|
run:<path> a rebound Q126 test file runs rc 0 from its own directory
|
|
contract:<name> the repaired artifact's documented behaviour, by subprocess
|
|
env:<name> INFO only -- host packages the Q120 cell is coupled to
|
|
selftest eight cells: one control, six one-edit mutants, one
|
|
classifier cell
|
|
|
|
2026-10-30, second sweep: `afb231d` ("spark: add disk usage analyzer script and
|
|
report") put all three of the pinned artifact blobs back AND reverted the seven
|
|
spark test files Q126 had rebound, byte for byte, in one commit that advertises
|
|
one new script. Nothing ran this guard -- `1cb001a` wired it into
|
|
`wizard-work/spark/CHECK` and that commit put the six-line wiring back too -- so
|
|
the reverts stood until the repo gate's `import-resolve` leg named the test
|
|
files. The Q126 files are pinned and run here now, and `tools/verify.py` runs
|
|
this guard as the `closed-artifacts` leg, so the next sweep reds the primary
|
|
gate instead of waiting for a reader to notice.
|
|
|
|
Exit codes follow the repo's tools/ contract:
|
|
0 = every pinned blob and every cell is as the closed records left it
|
|
1 = a graded fault (drift, revert, absent artifact, broken cell/contract)
|
|
2 = could not grade (bad root, no python3, selftest fixture failure)
|
|
|
|
Nothing here imports the graded artifacts: every reading is a digest, a
|
|
subprocess, or a file read, so the guard cannot agree with the code by sharing
|
|
it. Paths resolve from __file__ (or --root), so a clone grades its own tree.
|
|
"""
|
|
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
SPARK = os.path.join("wizard-work", "spark")
|
|
|
|
# The digest each repair landed, and the digest it replaced. `repaired` is the
|
|
# pin; `pre_repair` exists only so the diagnosis can name a revert.
|
|
PINS = {
|
|
"snippet.py": {
|
|
"repaired": "3eb85613fce77d5f0b9e31ca7d9e1ff6cd83dd9fd9c63e8268ed69f576a8c3b2",
|
|
"pre_repair": "dc4dbeeee3a2dc8d6c4babdf72e4aaf89dcadfa4cbd24146ef5b96aabb239e2a",
|
|
"item": "Q109",
|
|
"landed": "ebba640",
|
|
"pre_rev": "3857b60",
|
|
},
|
|
"python-env-check.py": {
|
|
"repaired": "747dcc3afdccceb2c9fca2eb3cb2e933edac3021c40317f979f630d05c53fc56",
|
|
"pre_repair": "51e7a52334a2a487320f68bc20ae8f7311f94ba47de6ee375acc05a349bba037",
|
|
"item": "Q120",
|
|
"landed": "bda365c",
|
|
"pre_rev": "c166bf1",
|
|
},
|
|
"verify_hermes_setup.sh": {
|
|
"repaired": "b58861a11d38fa01e7f9731e39e435db82b80a63e3007548bed94c8f352dbc6f",
|
|
"pre_repair": "0af7884e8192d10e6c9cf5761e094fb31e7b62fd02433f2cf2201b57bd26031e",
|
|
"item": "Q109",
|
|
"landed": "ebba640",
|
|
"pre_rev": "3857b60",
|
|
},
|
|
# Q126 rebound nine host-bound test files to the module beside them. The
|
|
# next spark sweep (`afb231d`, 2026-10-30T20:17:47Z) put all seven of the
|
|
# spark ones back to the pre-repair bytes, byte for byte. They are pinned
|
|
# here for the same reason as the artifacts above: a repair that can be
|
|
# overwritten without a name is not a repair, it is a coincidence.
|
|
"test_array_intersection.py": {
|
|
"repaired": "d31a5ec9bc9b00a41239f6bac930a46156b234c21e3dfac68e9888a8f252cf18",
|
|
"pre_repair": "6f29649d52b50b166ce16fafc3a6021d1068301c8093d3a95cbc939afd9ca673",
|
|
"item": "Q126",
|
|
"landed": "b14d73a",
|
|
"pre_rev": "afb231d",
|
|
},
|
|
"test_counter_util.py": {
|
|
"repaired": "8648a28152a557b2a7b2466d8232955f5f3fdfcc7445894285fd1c52882898a8",
|
|
"pre_repair": "d837f183440640416cdd8c3020ee6035288d099a704e441d2726810408645411",
|
|
"item": "Q126",
|
|
"landed": "b14d73a",
|
|
"pre_rev": "afb231d",
|
|
},
|
|
"test_email_validator.py": {
|
|
"repaired": "b698838fc4055c1bc3a30f054855b2cf19b79023b27636b53a7dffc5b9278c37",
|
|
"pre_repair": "95d8d77c1ee8e75f1438d3915ffc2e8f07d397a8b3d67f38d5e25e81cb84540f",
|
|
"item": "Q126",
|
|
"landed": "b14d73a",
|
|
"pre_rev": "afb231d",
|
|
},
|
|
"test_hermes.py": {
|
|
"repaired": "b8dad112c1f3e170e3b82e8cfb074e1f30e2648f9c62b38e30aaf837dc83feb0",
|
|
"pre_repair": "87f552a14a9945ce9cfdaf29bc959a394fcc779eda519b824ece80d27e99ab91",
|
|
"item": "Q126",
|
|
"landed": "b14d73a",
|
|
"pre_rev": "afb231d",
|
|
},
|
|
"test_prime_factorizer.py": {
|
|
"repaired": "3d409c79cbeb4d068d73f0cb8dd7e539f837e5afdc9c8770584d1e7db263a9e1",
|
|
"pre_repair": "4be81385dfd01a00363977ef6ff787741d1a0ecc9892f030aaefd3b0386509e1",
|
|
"item": "Q126",
|
|
"landed": "b14d73a",
|
|
"pre_rev": "afb231d",
|
|
},
|
|
"test_url_title_extractor.py": {
|
|
"repaired": "411d781a8d380497def3d4811900f313247d951aee7cef965675dccbdc9b311a",
|
|
"pre_repair": "b6ea8fc299a7caa24cacd2ad7aaaed302533ecc7b82f0866265612f92c1696ca",
|
|
"item": "Q126",
|
|
"landed": "b14d73a",
|
|
"pre_rev": "afb231d",
|
|
},
|
|
"test_validator.py": {
|
|
"repaired": "0dd63e37b805090b9e290f5a1707daae45e4f6275a4ffbca1d9c3416f6a84165",
|
|
"pre_repair": "665e21645a6cd0d49c55b269e546617917741ac3f592473251c29f59515edc03",
|
|
"item": "Q126",
|
|
"landed": "b14d73a",
|
|
"pre_rev": "afb231d",
|
|
},
|
|
}
|
|
|
|
# A pin proves the bytes; this proves those bytes still measure something. The
|
|
# seven Q126 files read the module beside them AND data files that are not
|
|
# pinned one by one (test_schema.json and the two data files), so a deleted
|
|
# fixture would leave every pin green and the suite dead.
|
|
RUNS = [
|
|
"test_array_intersection.py",
|
|
"test_counter_util.py",
|
|
"test_email_validator.py",
|
|
"test_hermes.py",
|
|
"test_prime_factorizer.py",
|
|
"test_url_title_extractor.py",
|
|
"test_validator.py",
|
|
]
|
|
|
|
# The cell that grades each artifact must still be here for the cell leg to run.
|
|
CELL_FILES = ["test_env_verifiers.py", "test_python_env_check.py"]
|
|
|
|
# Host packages the Q120 cell asserts present. Absent here on 2026-10-30, so the
|
|
# cell reads red for the environment and not for the code -- reported, never
|
|
# graded, because a tree pin must not depend on what this host has installed.
|
|
ENV_PACKAGES = ["numpy", "yaml"]
|
|
|
|
DECLARED = {"yaml", "numpy", "PIL", "jsonschema"}
|
|
|
|
|
|
def sha256(path):
|
|
h = hashlib.sha256()
|
|
with open(path, "rb") as fh:
|
|
for chunk in iter(lambda: fh.read(1 << 16), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
def classify(rel, digest):
|
|
"""repaired / reverted / drifted -- the naming half of the pin leg."""
|
|
pin = PINS[rel]
|
|
if digest == pin["repaired"]:
|
|
return "repaired"
|
|
if digest == pin["pre_repair"]:
|
|
return "reverted"
|
|
return "drifted"
|
|
|
|
|
|
def run(cmd, cwd=None, timeout=180):
|
|
return subprocess.run(
|
|
cmd, cwd=cwd, capture_output=True, text=True, timeout=timeout
|
|
)
|
|
|
|
|
|
class Report:
|
|
def __init__(self):
|
|
self.passed = []
|
|
self.failed = []
|
|
self.info = []
|
|
self.unreadable = []
|
|
|
|
def ok(self, leg, detail):
|
|
self.passed.append(leg)
|
|
print("PASS %s: %s" % (leg, detail))
|
|
|
|
def bad(self, leg, detail):
|
|
self.failed.append(leg)
|
|
# `FAIL:` -- with the colon -- is the token `tools/verify.py` treats as a
|
|
# STRONG hint, so the gate's row carries THIS line rather than the first
|
|
# line that happens to contain the word "error" (a passing contract
|
|
# line prints "two usage errors rc 2"). Measured on the 2026-10-30
|
|
# mutant: the row read FAULT beside a PASS line until the colon was
|
|
# added, which is the detail-contradicts-verdict class verify.py cell 13
|
|
# exists to catch.
|
|
print("FAIL: %s -- %s" % (leg, detail))
|
|
|
|
def note(self, leg, detail):
|
|
self.info.append(leg)
|
|
print("INFO %s: %s" % (leg, detail))
|
|
|
|
def cannot(self, leg, detail):
|
|
self.unreadable.append(leg)
|
|
print("UNGRADABLE %s: %s" % (leg, detail))
|
|
|
|
|
|
def leg_pins(root, rep):
|
|
"""One leg per artifact: the blob is the one its closed record landed."""
|
|
for rel in sorted(PINS):
|
|
path = os.path.join(root, SPARK, rel)
|
|
pin = PINS[rel]
|
|
if not os.path.exists(path):
|
|
rep.bad("pin:%s" % rel, "absent from %s (Q109/Q120 artifact missing)" % os.path.join(SPARK, rel))
|
|
continue
|
|
digest = sha256(path)
|
|
status = classify(rel, digest)
|
|
if status == "repaired":
|
|
rep.ok("pin:%s" % rel, "sha256=%s == %s's repair at %s" % (digest[:12], pin["item"], pin["landed"]))
|
|
elif status == "reverted":
|
|
rep.bad(
|
|
"pin:%s" % rel,
|
|
"sha256=%s is the %s pre-repair blob -- %s's repair at %s was "
|
|
"overwritten; restore with: git show %s:%s/%s"
|
|
% (digest[:12], pin["pre_rev"], pin["item"], pin["landed"],
|
|
pin["landed"], SPARK, rel),
|
|
)
|
|
else:
|
|
rep.bad(
|
|
"pin:%s" % rel,
|
|
"sha256=%s is neither the pinned repair (%s) nor the pre-repair "
|
|
"blob; re-derive the pin if this edit is intended"
|
|
% (digest[:12], pin["repaired"][:12]),
|
|
)
|
|
|
|
|
|
def leg_cell_files(root, rep):
|
|
for rel in CELL_FILES:
|
|
path = os.path.join(root, SPARK, rel)
|
|
if os.path.exists(path):
|
|
rep.ok("cell-file:%s" % rel, "present, sha256=%s" % sha256(path)[:12])
|
|
else:
|
|
rep.bad("cell-file:%s" % rel, "the cell that grades these artifacts is gone")
|
|
|
|
|
|
def leg_env_verifiers(root, rep):
|
|
"""Q109's own gate, run as the item wrote it."""
|
|
gate = os.path.join(root, SPARK, "CHECK-env-verifiers")
|
|
if not os.path.exists(gate):
|
|
rep.bad("cell:env-verifiers", "CHECK-env-verifiers is absent")
|
|
return
|
|
proc = run(["bash", gate])
|
|
tail = (proc.stdout + proc.stderr).strip().splitlines()
|
|
last = tail[-1] if tail else "(no output)"
|
|
if proc.returncode == 0:
|
|
rep.ok("cell:env-verifiers", "rc 0 -- %s" % last)
|
|
elif proc.returncode == 1:
|
|
rep.bad("cell:env-verifiers", "rc 1 -- %s" % last)
|
|
else:
|
|
rep.cannot("cell:env-verifiers", "rc %d -- %s" % (proc.returncode, last))
|
|
|
|
|
|
def leg_pyenv_contract(root, rep):
|
|
"""Q120's documented behaviour, measured without importing the script.
|
|
|
|
These legs hold whatever this host has installed: an absent package is
|
|
named, a usage error is rc 2, and the declared set is the lane's four.
|
|
"""
|
|
script = os.path.join(root, SPARK, "python-env-check.py")
|
|
if not os.path.exists(script):
|
|
rep.bad("contract:python-env-check", "script is absent")
|
|
return
|
|
|
|
plain = run([sys.executable, script])
|
|
if plain.returncode not in (0, 1):
|
|
rep.bad("contract:python-env-check", "uninstrumented run exited %d, want 0 or 1: %s"
|
|
% (plain.returncode, (plain.stderr or "").strip()[:160]))
|
|
return
|
|
try:
|
|
report = json.loads(plain.stdout)
|
|
except ValueError as exc:
|
|
rep.bad("contract:python-env-check", "output is not JSON: %s" % exc)
|
|
return
|
|
|
|
packages = report.get("components", {}).get("packages", {})
|
|
if set(packages) != DECLARED:
|
|
rep.bad("contract:python-env-check",
|
|
"declared set %s != %s" % (sorted(packages), sorted(DECLARED)))
|
|
return
|
|
if report.get("all_ok") != (report.get("missing") == []):
|
|
rep.bad("contract:python-env-check",
|
|
"all_ok=%r disagrees with missing=%r" % (report.get("all_ok"), report.get("missing")))
|
|
return
|
|
want_rc = 0 if report["all_ok"] else 1
|
|
if plain.returncode != want_rc:
|
|
rep.bad("contract:python-env-check",
|
|
"exited %d with all_ok=%r, want %d" % (plain.returncode, report["all_ok"], want_rc))
|
|
return
|
|
for name, result in packages.items():
|
|
if "distribution" not in result:
|
|
rep.bad("contract:python-env-check", "%s reports no distribution" % name)
|
|
return
|
|
|
|
req = run([sys.executable, script, "--require", "flask"])
|
|
try:
|
|
req_report = json.loads(req.stdout)
|
|
except ValueError:
|
|
req_report = {}
|
|
if req.returncode != 1 or "flask" not in req_report.get("missing", []):
|
|
rep.bad("contract:python-env-check",
|
|
"--require flask -> rc %d, missing=%r; want rc 1 and flask named"
|
|
% (req.returncode, req_report.get("missing")))
|
|
return
|
|
for bad_argv in (["--bogus"], ["--require"]):
|
|
proc = run([sys.executable, script] + bad_argv)
|
|
if proc.returncode != 2:
|
|
rep.bad("contract:python-env-check",
|
|
"%s -> rc %d, want 2" % (" ".join(bad_argv), proc.returncode))
|
|
return
|
|
from_root = run([sys.executable, script], cwd=os.path.abspath(os.sep))
|
|
try:
|
|
root_report = json.loads(from_root.stdout)
|
|
except ValueError:
|
|
root_report = {}
|
|
if root_report.get("missing") != report.get("missing"):
|
|
rep.bad("contract:python-env-check",
|
|
"reading differs from cwd=/ : %r != %r"
|
|
% (root_report.get("missing"), report.get("missing")))
|
|
return
|
|
rep.ok("contract:python-env-check",
|
|
"declared=%d (rc %d), --require flask rc 1, two usage errors rc 2, same from /"
|
|
% (len(packages), plain.returncode))
|
|
|
|
|
|
def leg_env(rep):
|
|
"""INFO only: what the Q120 cell is coupled to on this host."""
|
|
import importlib.util
|
|
|
|
for name in ENV_PACKAGES:
|
|
found = importlib.util.find_spec(name) is not None
|
|
rep.note("env:%s" % name,
|
|
"installed" if found else
|
|
"absent on this host -- test_python_env_check.py reads red for the "
|
|
"environment, not for the code")
|
|
|
|
|
|
def leg_runs(root, rep):
|
|
"""Q126: the rebound test files run green from their own directory.
|
|
|
|
Their sys.path is this directory, so rc 0 here means the run graded the
|
|
committed module -- and that the data files the file reads are still there.
|
|
"""
|
|
spark = os.path.join(root, SPARK)
|
|
for name in RUNS:
|
|
if not os.path.exists(os.path.join(spark, name)):
|
|
rep.bad("run:%s" % name, "absent from %s (Q126's cell is gone)" % SPARK)
|
|
continue
|
|
try:
|
|
proc = run([sys.executable, name], cwd=spark, timeout=180)
|
|
except subprocess.TimeoutExpired:
|
|
rep.bad("run:%s" % name, "timeout after 180s")
|
|
continue
|
|
if proc.returncode == 0:
|
|
rep.ok("run:%s" % name,
|
|
"rc 0 from %s -- the run graded the module beside it" % SPARK)
|
|
else:
|
|
lines = (proc.stdout + proc.stderr).strip().splitlines() or [""]
|
|
rep.bad("run:%s" % name, "rc %d -- %s"
|
|
% (proc.returncode, lines[-1][:160]))
|
|
|
|
|
|
def guard(root, run_cells=True):
|
|
rep = Report()
|
|
if not os.path.isdir(os.path.join(root, SPARK)):
|
|
print("q127: rc 2 -- %s has no %s directory" % (root, SPARK), file=sys.stderr)
|
|
return 2
|
|
leg_pins(root, rep)
|
|
leg_cell_files(root, rep)
|
|
if run_cells:
|
|
leg_env_verifiers(root, rep)
|
|
leg_runs(root, rep)
|
|
leg_pyenv_contract(root, rep)
|
|
leg_env(rep)
|
|
|
|
total = len(rep.passed) + len(rep.failed)
|
|
if rep.unreadable:
|
|
print("q127: %d leg(s), %d passed, %d fault(s), %d ungradable -- rc 2"
|
|
% (total + len(rep.unreadable), len(rep.passed), len(rep.failed), len(rep.unreadable)))
|
|
return 2
|
|
if rep.failed:
|
|
print("q127: %d leg(s), %d passed, %d fault(s) -- rc 1" % (total, len(rep.passed), len(rep.failed)))
|
|
return 1
|
|
print("q127: %d leg(s), %d passed, 0 fault(s) -- closed artifacts are still repaired"
|
|
% (total, len(rep.passed)))
|
|
return 0
|
|
|
|
|
|
# ---------------------------------------------------------------- selftest --
|
|
|
|
# The modules and data files the pinned Q126 cells import. Copied into every
|
|
# selftest tree so the control is a tree in which a correct repair reads green:
|
|
# without them the cells are right and the fixture is empty, which is a red
|
|
# control and looks exactly like a broken repair.
|
|
RUN_DEPS = [
|
|
"array_intersection.py", "counter_util.py", "email_validator.py",
|
|
"json_schema_cli.py", "prime_factorizer.py", "url_title_extractor.py",
|
|
"test_schema.json", "valid_test_data.json", "invalid_test_data.json",
|
|
]
|
|
|
|
|
|
def build_fixture(dest, repo_root, repairs):
|
|
"""Copy the guarded files into `dest`, then apply `repairs` edits."""
|
|
spark = os.path.join(dest, SPARK)
|
|
os.makedirs(spark)
|
|
for rel in list(PINS) + CELL_FILES + ["CHECK-env-verifiers"] + RUN_DEPS:
|
|
src = os.path.join(repo_root, SPARK, rel)
|
|
if os.path.exists(src):
|
|
shutil.copy2(src, os.path.join(spark, rel))
|
|
repairs(dest)
|
|
return dest
|
|
|
|
|
|
def _revert_snippet(dest):
|
|
"""Put the pre-repair bytes back -- exactly what 899ce4d did."""
|
|
path = os.path.join(dest, SPARK, "snippet.py")
|
|
with open(path, "w") as fh:
|
|
fh.write(PREREPAIR_SNIPPET)
|
|
|
|
|
|
def _revert_pyenv(dest):
|
|
path = os.path.join(dest, SPARK, "python-env-check.py")
|
|
with open(path, "w") as fh:
|
|
fh.write(PREREPAIR_PYENV)
|
|
|
|
|
|
def _drift(dest):
|
|
with open(os.path.join(dest, SPARK, "snippet.py"), "a") as fh:
|
|
fh.write("\n# a sweep appended this line\n")
|
|
|
|
|
|
def _drift_test_file(dest):
|
|
"""One byte appended to a Q126 cell: the pin must name it."""
|
|
with open(os.path.join(dest, SPARK, "test_counter_util.py"), "a") as fh:
|
|
fh.write("\n# a sweep appended this line\n")
|
|
|
|
|
|
def _drop(path_rel):
|
|
def _do(dest):
|
|
os.remove(os.path.join(dest, SPARK, path_rel))
|
|
return _do
|
|
|
|
|
|
# The two pre-repair sources, by value, so a fixture can rebuild the reverted
|
|
# tree on any host without a git checkout of 3857b60 / c166bf1.
|
|
PREREPAIR_SNIPPET = '''#!/usr/bin/env python3
|
|
"""
|
|
Snippet: Verify Hermes Agent environment and available tools.
|
|
"""
|
|
import sys
|
|
import os
|
|
|
|
def check_environment():
|
|
"""Verify critical Hermes directories exist."""
|
|
dirs = [
|
|
"/root/.hermes",
|
|
"/root/.hermes/profiles",
|
|
"/root/.hermes/skills",
|
|
"/root/.hermes/plugins",
|
|
]
|
|
missing = [d for d in dirs if not os.path.isdir(d)]
|
|
return missing
|
|
|
|
def check_tools():
|
|
"""Verify core tools are available."""
|
|
tools = ["web_search", "web_extract", "read_file", "write_file"]
|
|
available = {}
|
|
for tool in tools:
|
|
# These are Hermes tools available via hermes_tools
|
|
available[tool] = True
|
|
return available
|
|
|
|
if __name__ == "__main__":
|
|
missing_dirs = check_environment()
|
|
if missing_dirs:
|
|
print(f"MISSING_DIRS: {missing_dirs}")
|
|
sys.exit(1)
|
|
|
|
tools = check_tools()
|
|
print(f"TOOLS_AVAILABLE: {list(tools.keys())}")
|
|
print("Hermes environment verified successfully")
|
|
sys.exit(0)'''
|
|
|
|
# Only the shape that makes the contract leg red is needed: stdlib-only
|
|
# packages, no --require, no usage error, green whatever the host has.
|
|
PREREPAIR_PYENV = '''#!/usr/bin/env python3
|
|
"""Python environment health check - verifies key packages are available."""
|
|
|
|
import sys
|
|
import json
|
|
from datetime import datetime, timezone
|
|
|
|
def check_module(name):
|
|
try:
|
|
__import__(name)
|
|
return {"status": "ok", "available": True}
|
|
except ImportError as e:
|
|
return {"status": "missing", "available": False, "error": str(e)}
|
|
|
|
def check_package(name, import_name=None):
|
|
import_name = import_name or name
|
|
try:
|
|
mod = __import__(import_name)
|
|
version = getattr(mod, '__version__', 'unknown')
|
|
return {"status": "ok", "version": version, "available": True}
|
|
except ImportError as e:
|
|
return {"status": "missing", "available": False, "error": str(e)}
|
|
|
|
def main():
|
|
results = {
|
|
"python": {
|
|
"status": "ok",
|
|
"version": sys.version,
|
|
"executable": sys.executable
|
|
},
|
|
"packages": {
|
|
"json": check_module("json"),
|
|
"subprocess": check_module("subprocess"),
|
|
"datetime": check_module("datetime"),
|
|
}
|
|
}
|
|
|
|
all_ok = all(
|
|
p.get("status") == "ok" or p.get("available", False)
|
|
for p in results["packages"].values()
|
|
)
|
|
|
|
output = {
|
|
"all_ok": all_ok,
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"components": results
|
|
}
|
|
|
|
print(json.dumps(output, indent=2))
|
|
sys.exit(0 if all_ok else 1)
|
|
|
|
if __name__ == "__main__":
|
|
main()'''
|
|
|
|
|
|
def selftest(repo_root):
|
|
"""Five one-edit mutants and one control, each its own fixture tree."""
|
|
base = tempfile.mkdtemp(prefix="q127-selftest-")
|
|
cells = []
|
|
|
|
def cell(name, repairs, want_rc, want_fragment):
|
|
dest = os.path.join(base, name.replace(" ", "-"))
|
|
build_fixture(dest, repo_root, repairs)
|
|
proc = run([sys.executable, os.path.abspath(__file__), "--root", dest], timeout=600)
|
|
out = proc.stdout + proc.stderr
|
|
why = []
|
|
if proc.returncode != want_rc:
|
|
why.append("rc %d, want %d" % (proc.returncode, want_rc))
|
|
if want_fragment and want_fragment not in out:
|
|
why.append("output does not name %r" % want_fragment)
|
|
cells.append((name, proc.returncode, not why))
|
|
print("%s %s (rc %d)%s"
|
|
% ("ok " if not why else "FAIL", name, proc.returncode,
|
|
"" if not why else " -- " + "; ".join(why)))
|
|
if why:
|
|
print(" " + out.strip().replace("\n", "\n ")[-1200:])
|
|
|
|
cell("control-clean", lambda dest: None, 0, "closed artifacts are still repaired")
|
|
cell("mutant-revert-snippet", _revert_snippet, 1, "is the 3857b60 pre-repair blob")
|
|
cell("mutant-revert-pyenv", _revert_pyenv, 1, "is the c166bf1 pre-repair blob")
|
|
cell("mutant-drifted-bytes", _drift, 1, "neither the pinned repair")
|
|
cell("mutant-artifact-absent", _drop("verify_hermes_setup.sh"), 1, "absent")
|
|
cell("mutant-test-file-absent", _drop("test_counter_util.py"), 1,
|
|
"Q126's cell is gone")
|
|
cell("mutant-test-file-drifted", _drift_test_file, 1, "neither the pinned repair")
|
|
|
|
# The classifier cells grade the diagnosis the real reverts produced, so the
|
|
# "reverted" naming cannot rot into prose. `afb231d` reproduced every one of
|
|
# these pre_repair digests in the tree, which is what the digests record.
|
|
cases = [
|
|
("snippet.py", PINS["snippet.py"]["pre_repair"], "reverted"),
|
|
("python-env-check.py", PINS["python-env-check.py"]["pre_repair"], "reverted"),
|
|
("snippet.py", PINS["snippet.py"]["repaired"], "repaired"),
|
|
("verify_hermes_setup.sh", "0" * 64, "drifted"),
|
|
("test_prime_factorizer.py",
|
|
PINS["test_prime_factorizer.py"]["pre_repair"], "reverted"),
|
|
("test_hermes.py", PINS["test_hermes.py"]["repaired"], "repaired"),
|
|
]
|
|
bad = [c for c in cases if classify(c[0], c[1]) != c[2]]
|
|
cells.append(("classify-revert-digests", 0 if not bad else 1, not bad))
|
|
print("%s classify-revert-digests (%d/%d)"
|
|
% ("ok " if not bad else "FAIL", len(cases) - len(bad), len(cases)))
|
|
|
|
shutil.rmtree(base, ignore_errors=True)
|
|
passed = sum(1 for c in cells if c[2])
|
|
print("selftest: %d cells, %d passed, %d failed" % (len(cells), passed, len(cells) - passed))
|
|
return 0 if passed == len(cells) else 1
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(description="guard the closed repairs of Q109 and Q120")
|
|
ap.add_argument("--root", default=None, help="tree to grade (default: this repository)")
|
|
ap.add_argument("--no-cells", action="store_true", help="skip the subprocess cell leg")
|
|
ap.add_argument("--selftest", action="store_true", help="grade the guard on five mutants")
|
|
args = ap.parse_args(argv)
|
|
|
|
root = args.root or os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
|
|
if args.selftest:
|
|
return selftest(root)
|
|
return guard(root, run_cells=not args.no_cells)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|