diff --git a/tools/gate_blame.py b/tools/gate_blame.py index 0e17566..329585a 100644 --- a/tools/gate_blame.py +++ b/tools/gate_blame.py @@ -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")