gate_blame (volt): --help must answer from a lone copy -- the module-level import of verify.py made tool_map's oracle say 'no --help oracle' at 2c2184c; import deferred to gate(), queue/index defaults read from verify at run time, unknown leg is a named rc 2; selftest 20 -> 21 cells (the lone-copy cell is the regression)

This commit is contained in:
volt committed 2026-10-20 19:11:35 +00:00
1 parent 2c2184cbba
commit ad20c75b3c
1 file changed
+64 -15
+64 -15
View File
@@ -51,7 +51,7 @@ USAGE:
python3 gate_blame.py --repo . --leg checkin --range 10
python3 gate_blame.py --repo /root/troupe-standup --leg index-current --range 5
python3 gate_blame.py --repo . --leg checkin --range 24 --json /tmp/blame.json
python3 gate_blame.py --repo . --selftest # fixture corpus, 20 cells
python3 gate_blame.py --repo . --selftest # fixture corpus, 21 cells
"""
from __future__ import annotations
@@ -67,10 +67,14 @@ import sys
import tempfile
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import verify # noqa: E402 -- the gate's own runner and leg table, reused on purpose
LEG_KEYS = tuple(leg["key"] for leg in verify.LEGS)
PASS, FAULT, UNGRADABLE = verify.PASS, verify.FAULT, verify.UNGRADABLE
# These three strings mirror verify.py's verdict vocabulary. They are written as
# literals ON PURPOSE: this module must import with no `verify.py` beside it.
# `tool_map.py` builds a tool's `--help` oracle from a temporary copy of that one
# file, so a module-level import of a sibling turns this tool into "no --help
# oracle" -- measured on this file at 2c2184c, where the first render said
# `- *(no --help oracle)*` for gate_blame.py while its own --help worked in-tree.
PASS, FAULT, UNGRADABLE = "PASS", "FAULT", "UNGRADABLE"
# The selftest size this docstring claims. Graded against the count that ran, the
# same way `verify.py` grades its own: a docstring count with no reader is how the
@@ -83,6 +87,23 @@ class CannotGrade(Exception):
"""rc 2: a reason this host cannot produce a timeline."""
def gate():
"""verify.py -- the gate's own leg table and runner, imported on demand.
Deferred rather than module-level so `--help` answers from a lone copy of this
file (see the PASS/FAULT/UNGRADABLE note above). A missing verify.py is a
refusal this host can name, never a traceback.
"""
here = os.path.dirname(os.path.abspath(__file__))
if here not in sys.path:
sys.path.insert(0, here)
try:
import verify # noqa: PLC0415 -- deferred on purpose, see docstring
except ImportError as exc:
raise CannotGrade("verify.py is not beside this tool: %s" % exc)
return verify
def _git(repo, *args, timeout=120):
return subprocess.run(["git", "-C", repo] + list(args),
capture_output=True, text=True, timeout=timeout)
@@ -134,7 +155,7 @@ def stamps(root, revs):
return out
def grade_rev(root, rev, leg, queue, index, timeout, scratch):
def grade_rev(root, rev, leg, queue, index, timeout, scratch, gate_mod):
"""One revision -> one row, graded by its own tree via verify.run_leg."""
wt = os.path.join(scratch, rev[:12])
added = _git(root, "worktree", "add", "--detach", "--quiet", wt, rev)
@@ -143,7 +164,7 @@ def grade_rev(root, rev, leg, queue, index, timeout, scratch):
"detail": "worktree add refused: %s"
% (_first_line(added.stderr) or "rc %d" % added.returncode)}
try:
row = verify.run_leg(leg, wt, queue, index, timeout)
row = gate_mod.run_leg(leg, wt, queue, index, timeout)
finally:
_git(root, "worktree", "remove", "--force", wt)
return {"rev": rev, "verdict": row["verdict"], "rc": row["rc"],
@@ -195,12 +216,16 @@ def render(rows, events, leg, ref, queue):
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
ap.add_argument("--repo", default=".")
ap.add_argument("--leg", default="checkin", choices=LEG_KEYS)
ap.add_argument("--leg", default="checkin", metavar="LEG",
help="gate leg to attribute, named from this checkout's "
"verify.LEGS (default checkin)")
ap.add_argument("--range", type=int, default=10,
help="first-parent commits to walk back from --ref (default 10)")
ap.add_argument("--ref", default="HEAD")
ap.add_argument("--queue", default=verify.QUEUE_DEFAULT)
ap.add_argument("--index", default=verify.INDEX_DEFAULT)
ap.add_argument("--queue", default=None,
help="repo-relative queue path (default: verify.py's own)")
ap.add_argument("--index", default=None,
help="repo-relative index document (default: verify.py's own)")
ap.add_argument("--timeout", type=int, default=300, help="per-revision leg timeout")
ap.add_argument("--json", dest="json_path", default=None)
ap.add_argument("--quiet", action="store_true")
@@ -210,10 +235,22 @@ def main(argv=None):
if args.selftest:
return selftest()
leg = next((l for l in verify.LEGS if l["key"] == args.leg), None)
if leg is None: # argparse choices makes this unreachable; kept explicit.
print("gate_blame: unknown leg %r (known: %s)" % (args.leg, ", ".join(LEG_KEYS)))
try:
gate_mod = gate()
except CannotGrade as exc:
print("gate_blame: cannot grade: %s" % exc)
return 2
leg_keys = tuple(l["key"] for l in gate_mod.LEGS)
leg = next((l for l in gate_mod.LEGS if l["key"] == args.leg), None)
if leg is None:
print("gate_blame: unknown leg %r (known: %s)"
% (args.leg, ", ".join(leg_keys)))
return 2
# The queue/index defaults are verify.py's, not copies of them: a default
# that drifts here would grade every revision against the wrong document.
queue = args.queue if args.queue is not None else gate_mod.QUEUE_DEFAULT
index = args.index if args.index is not None else gate_mod.INDEX_DEFAULT
try:
root = work_tree_root(args.repo)
revs = window(root, args.ref, args.range)
@@ -226,7 +263,7 @@ def main(argv=None):
try:
dates = stamps(root, revs)
for rev in revs:
row = grade_rev(root, rev, leg, args.queue, args.index, args.timeout, scratch)
row = grade_rev(root, rev, leg, queue, index, args.timeout, scratch, gate_mod)
row["date"] = dates.get(rev, "")
rows.append(row)
finally:
@@ -428,8 +465,20 @@ def selftest():
rc == 2 and "not a git work tree" in out, "rc=%d %s" % (rc, out.strip()))
proc = subprocess.run([sys.executable, os.path.join(here, "gate_blame.py"),
"--repo", a, "--leg", "nope"], capture_output=True, text=True)
cell("refusal: unknown leg rejected by argparse (rc 2)",
proc.returncode == 2 and "invalid choice" in proc.stderr, proc.stderr[:120])
cell("refusal: unknown leg is rc 2 and names the known legs",
proc.returncode == 2 and "unknown leg" in proc.stdout
and "checkin" in proc.stdout, (proc.stdout or proc.stderr)[:140])
# -- the portability defect tool_map.py caught at 2c2184c: --help must
# answer from a LONE copy of this file, with no verify.py beside it.
lone = os.path.join(base, "lone")
os.makedirs(lone, exist_ok=True)
shutil.copy(os.path.join(here, "gate_blame.py"),
os.path.join(lone, "gate_blame.py"))
proc = subprocess.run([sys.executable, os.path.join(lone, "gate_blame.py"), "--help"],
capture_output=True, text=True, cwd=lone)
cell("--help works from a lone copy (no verify.py beside it)",
proc.returncode == 0 and "--leg" in proc.stdout, (proc.stdout or proc.stderr)[:140])
# -- JSON shape --------------------------------------------------------
jpath = os.path.join(base, "blame.json")