139 lines
5.5 KiB
Python
139 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""null verification for the Wizard Troupe standup.
|
|
|
|
Exit code is the verdict: 0 = everything holds, 1 = something broke.
|
|
|
|
Run: python3 /root/troupe-standup/wizard-work/null/check.py
|
|
|
|
Self-run guard: the recursive probe runs this same file with
|
|
NULL_CHECK_SELF=1, which makes the probe inert (report success without
|
|
re-executing). Without the guard the probe would re-arm the variable and
|
|
recurse forever.
|
|
"""
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
|
|
MYDIR = os.path.dirname(os.path.abspath(__file__))
|
|
REPO = os.path.dirname(os.path.dirname(MYDIR))
|
|
PASSED, FAILED = [], []
|
|
|
|
|
|
def check(name, ok, detail=""):
|
|
(PASSED if ok else FAILED).append(name)
|
|
print(("PASS " if ok else "FAIL ") + name + (" -> " + detail if detail else ""))
|
|
|
|
|
|
def main():
|
|
# --- 1. artifact presence (all three files) ---
|
|
for fn in ("wizard.log.md", "wizard.py", "check.py"):
|
|
check("artifact present: " + fn, os.path.isfile(os.path.join(MYDIR, fn)))
|
|
|
|
# --- 2. wizard.py compiles, exposes the expected API ---
|
|
src = open(os.path.join(MYDIR, "wizard.py")).read()
|
|
check("wizard.py compiles", _compiles(src))
|
|
ns = {"__name__": "wizard_module", "__file__": os.path.join(MYDIR, "wizard.py")}
|
|
if _compiles(src):
|
|
exec(compile(src, "wizard.py", "exec"), ns)
|
|
check("API: craft exists", callable(ns.get("craft")))
|
|
check("API: VERDICT/CHECK markers exist", "VERDICT:" in src and "CHECK:" in src)
|
|
|
|
# --- 3. craft() unit test: deterministic, non-empty, format correct ---
|
|
try:
|
|
verdict, chk, summary = ns["craft"](
|
|
"fix the build", "staging", "Codeforge", "d7a1f", "alex"
|
|
)
|
|
ok = bool(verdict) and chk.startswith("CHECK: ") and isinstance(summary, str) and summary
|
|
check("craft() returns VERDICT/CHECK/summary", ok,
|
|
repr((verdict, chk[:40] + "...", summary[:24])))
|
|
except Exception as e: # noqa: BLE001
|
|
check("craft() returns VERDICT/CHECK/summary", False, "%s: %s" % (type(e).__name__, e))
|
|
|
|
# --- 4. report parses: exactly one VERDICT: and one CHECK: ---
|
|
log = open(os.path.join(MYDIR, "wizard.log.md")).read()
|
|
nv = len(re.findall(r"(?m)^VERDICT:", log))
|
|
nc = len(re.findall(r"(?m)^CHECK:", log))
|
|
check("wizard.log.md has exactly one VERDICT:", nv == 1, "found %d" % nv)
|
|
check("wizard.log.md has exactly one CHECK:", nc == 1, "found %d" % nc)
|
|
|
|
# --- 5. self-verification: run check.py and require exit 0 ---
|
|
check("check.py self-run exits 0", _self_run(), "")
|
|
|
|
# --- 6. git hygiene: commit exists and pushed HEAD is a descendant of it ---
|
|
ok, detail = _git_ok()
|
|
check("commit pushed to origin/main", ok, detail)
|
|
|
|
print("\n%d passed, %d failed" % (len(PASSED), len(FAILED)))
|
|
if FAILED:
|
|
print("FAILED: " + ", ".join(FAILED))
|
|
return 1
|
|
print("ALL CHECKS GREEN")
|
|
return 0
|
|
|
|
|
|
def _self_run():
|
|
"""Re-execute check.py under an inert marker env var; True iff it exits 0.
|
|
|
|
The child sees NULL_CHECK_SELF=1, so its own _self_run() reports True
|
|
without re-executing. That bounds the recursion depth to exactly one
|
|
child process instead of an unbounded chain.
|
|
"""
|
|
if os.environ.get("NULL_CHECK_SELF") == "1":
|
|
return True
|
|
env = dict(os.environ, NULL_CHECK_SELF="1")
|
|
try:
|
|
r = subprocess.run([sys.executable, os.path.join(MYDIR, "check.py")],
|
|
capture_output=True, text=True, env=env, timeout=120)
|
|
return r.returncode == 0
|
|
except Exception: # noqa: BLE001
|
|
return False
|
|
|
|
|
|
def _compiles(src):
|
|
try:
|
|
compile(src, "wizard.py", "exec")
|
|
return True
|
|
except SyntaxError:
|
|
return False
|
|
|
|
|
|
def _git_ok():
|
|
"""True iff this dir's artifact is committed by null and pushed to origin/main.
|
|
|
|
Robust to other agents landing commits on main: we look for ANY null commit
|
|
touching this dir and confirm it is an ancestor of origin/main (i.e. pushed),
|
|
rather than assuming it is HEAD.
|
|
"""
|
|
try:
|
|
commits = subprocess.run(
|
|
["git", "-C", REPO, "log", "--format=%H %an %s",
|
|
"--", "wizard-work/null"],
|
|
capture_output=True, text=True).stdout.splitlines()
|
|
mine = [c for c in commits if c.split(" ", 2)[1] == "null"]
|
|
if not mine:
|
|
return False, "no commit by null touching wizard-work/null"
|
|
commit, subject = mine[0].split(" ", 1)[0], mine[0].split(" ", 2)[2]
|
|
if not subject.startswith("null:"):
|
|
return False, "subject %r lacks 'null:' prefix" % subject
|
|
files = subprocess.run(
|
|
["git", "-C", REPO, "show", "--name-only", "--format=", commit],
|
|
capture_output=True, text=True).stdout.split()
|
|
need = {"wizard-work/null/wizard.log.md", "wizard-work/null/wizard.py",
|
|
"wizard-work/null/check.py"}
|
|
if not need.issubset(set(files)):
|
|
return False, "commit missing files: %s" % sorted(need - set(files))
|
|
origin = subprocess.run(["git", "-C", REPO, "rev-parse", "origin/main"],
|
|
capture_output=True, text=True).stdout.strip()
|
|
anc = subprocess.run(
|
|
["git", "-C", REPO, "merge-base", "--is-ancestor", commit, origin])
|
|
if anc.returncode != 0:
|
|
return False, "commit %s not pushed to origin/main" % commit[:8]
|
|
return True, "commit %s by null is an ancestor of origin/main" % commit[:8]
|
|
except Exception as e: # noqa: BLE001
|
|
return False, "%s: %s" % (type(e).__name__, e)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|