263 lines
11 KiB
Python
263 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""check_bindings.py -- this lane's own gate for where its tests bind.
|
|
|
|
WHY THIS EXISTS
|
|
`tools/import_resolve_check.py` grades the whole tree and is a leg of the
|
|
repo gate, so a beam test that binds a module to a host path reddens the
|
|
repository *after* it lands. That has now happened four times in this lane:
|
|
`test_canonical_url.py` and `test_median.py` (Q98), `test_damerau_levenshtein.py`
|
|
and `test_expr.py` (Q133), and `test_next_greater.py` -- opened 2026-11-02 as
|
|
Q153, committed at `096ba9b` with `sys.path.insert(0,
|
|
"/root/.hermes/wizard-outputs/beam")`. Each one read green when run, because
|
|
the host copy and the tree copy were byte-identical; each one graded bytes
|
|
the repository does not control, and on any other host it could not grade at
|
|
all (`ModuleNotFoundError`).
|
|
|
|
The repo tool is right and stays the authority. What the lane lacked is a
|
|
reading it can run before committing: `wizard-work/beam/CHECK` collected all
|
|
681 cells and read rc 0 while `test_next_greater.py` was bound outside the
|
|
tree. This wraps the authority, scopes its finding rows to this lane, and
|
|
turns them into the lane gate's rc.
|
|
|
|
WHAT IT MEASURES
|
|
One subprocess: `tools/import_resolve_check.py --repo <root> --rev <rev>
|
|
--json <tmp> --quiet`, which parses every tracked test file with `ast` and
|
|
names the ones binding an absolute constant outside the repo (OUTSIDE_REPO)
|
|
or at a fixed in-tree path (ABS_PATH_IN_REPO). Both classes are faults here.
|
|
The finding rows are then filtered to paths under this file's own directory,
|
|
so another lane's in-flight defect does not redden this lane's gate -- the
|
|
repo gate already grades that, for everybody, once.
|
|
|
|
THE EXIT CONTRACT -- the house three codes
|
|
0 the tool graded the tree and no file in THIS lane binds outside it
|
|
1 at least one lane file binds outside the repo (each row printed)
|
|
2 could not grade: the repo tool is absent, the tool itself returned 2
|
|
(its inventory or a file was unreadable), or its JSON could not be read
|
|
|
|
rc 2 is not a pass and not a fault: a lane gate that could not read the tree
|
|
has not earned a claim about it.
|
|
|
|
NOT CLAIMED
|
|
That the graded files test their modules well -- this grades the binding, no
|
|
assertion. That a green lane means the repo is green: this lane's rows are
|
|
the only ones read here, and `tools/verify.py` is the whole-tree gate. That
|
|
a computed path (`os.path.join(os.environ["HOME"], ...)`) is caught: the
|
|
underlying tool's `--import-probe` is what sees those, and it is not run here
|
|
because it executes test code.
|
|
|
|
USAGE
|
|
python3 check_bindings.py # this lane, the work tree
|
|
python3 check_bindings.py --rev HEAD # the committed blobs
|
|
python3 check_bindings.py --repo /path --lane wizard-work/beam
|
|
python3 check_bindings.py --selftest # 6 cells
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import contextlib
|
|
import glob
|
|
import io
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
LANE = os.path.dirname(os.path.abspath(__file__))
|
|
DEFAULT_REPO = os.path.dirname(os.path.dirname(LANE))
|
|
TOOL_REL = os.path.join("tools", "import_resolve_check.py")
|
|
|
|
|
|
class CannotGrade(Exception):
|
|
"""The binding reading could not be taken."""
|
|
|
|
|
|
def lane_prefix(repo, lane):
|
|
"""The lane's path relative to the repo root, with a trailing slash."""
|
|
rel = os.path.relpath(os.path.abspath(lane), os.path.abspath(repo))
|
|
if rel.startswith(".."):
|
|
raise CannotGrade("lane %s is outside repo %s" % (lane, repo))
|
|
return rel.rstrip("/") + "/"
|
|
|
|
|
|
def grade(repo, lane, rev):
|
|
"""(rc, faults, graded, lane_files) for one reading."""
|
|
repo = os.path.abspath(repo)
|
|
tool = os.path.join(repo, TOOL_REL)
|
|
if not os.path.isfile(tool):
|
|
raise CannotGrade("no %s under %s" % (TOOL_REL, repo))
|
|
prefix = lane_prefix(repo, lane)
|
|
lane_files = sorted(os.path.basename(p) for p in
|
|
glob.glob(os.path.join(repo, prefix, "test_*.py")))
|
|
with tempfile.TemporaryDirectory(prefix="bindings-") as td:
|
|
out = os.path.join(td, "rows.json")
|
|
try:
|
|
proc = subprocess.run(
|
|
[sys.executable, tool, "--repo", repo, "--rev", rev,
|
|
"--json", out, "--quiet"],
|
|
capture_output=True, text=True, timeout=600)
|
|
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
raise CannotGrade("could not run %s: %s" % (TOOL_REL, exc))
|
|
if proc.returncode == 2:
|
|
why = (proc.stderr or proc.stdout or "").strip().splitlines()
|
|
raise CannotGrade("%s could not grade: %s"
|
|
% (TOOL_REL, why[-1] if why else "rc 2"))
|
|
if proc.returncode not in (0, 1):
|
|
raise CannotGrade("%s exited %d: %s"
|
|
% (TOOL_REL, proc.returncode,
|
|
(proc.stderr or "").strip()[-200:]))
|
|
try:
|
|
with open(out) as fh:
|
|
row = json.load(fh)
|
|
except (OSError, ValueError) as exc:
|
|
raise CannotGrade("could not read %s's --json: %s" % (TOOL_REL, exc))
|
|
faults = [f for f in row.get("faults", []) if str(f[0]).startswith(prefix)]
|
|
return (1 if faults else 0), faults, row.get("graded", 0), lane_files
|
|
|
|
|
|
def run(repo, lane, rev, quiet=False):
|
|
try:
|
|
rc, faults, graded, lane_files = grade(repo, lane, rev)
|
|
except CannotGrade as exc:
|
|
print("bindings: cannot grade -- %s" % exc, file=sys.stderr)
|
|
return 2
|
|
prefix = lane_prefix(repo, lane)
|
|
if not quiet:
|
|
for path, line, cls, how, raw, is_prefix in faults:
|
|
print("FAIL bindings: %s:%s %s -> %s%s"
|
|
% (path, line, how, raw,
|
|
" (f-string prefix)" if is_prefix else ""))
|
|
print("bindings: %d file(s) graded repo-wide at %s, %d test file(s) in "
|
|
"%s" % (graded, rev, len(lane_files), prefix))
|
|
if faults:
|
|
print("bindings: %d lane file(s) bind a module outside the repo"
|
|
% len(faults))
|
|
return 1
|
|
if not quiet:
|
|
print("bindings: PASS -- no test file in %s binds outside the repo"
|
|
% prefix)
|
|
return 0
|
|
|
|
|
|
# -------------------------------------------------------------------- selftest
|
|
|
|
def _write(path, text):
|
|
os.makedirs(os.path.dirname(path) or ".", exist_ok=True)
|
|
with open(path, "w") as fh:
|
|
fh.write(text)
|
|
|
|
|
|
def _fixture(td, name):
|
|
"""A throwaway git repo shaped like this one: tools/ + wizard-work/<lane>/."""
|
|
repo = os.path.join(td, name)
|
|
os.makedirs(repo)
|
|
src = os.path.join(DEFAULT_REPO, TOOL_REL)
|
|
with open(src) as fh:
|
|
_write(os.path.join(repo, TOOL_REL), fh.read())
|
|
lane = os.path.join(repo, "wizard-work", "beam")
|
|
_write(os.path.join(lane, "target.py"), "thing = 1\n")
|
|
_write(os.path.join(lane, "test_ok.py"),
|
|
"import pathlib, sys\n"
|
|
"HERE = pathlib.Path(__file__).resolve().parent\n"
|
|
"sys.path.insert(0, str(HERE))\n"
|
|
"from target import thing\n")
|
|
subprocess.run(["git", "init", "-q", repo], check=True,
|
|
capture_output=True, text=True)
|
|
subprocess.run(["git", "-C", repo, "add", "-A"], check=True,
|
|
capture_output=True, text=True)
|
|
return repo, lane
|
|
|
|
|
|
def selftest():
|
|
cells, passed, failures = 0, 0, []
|
|
|
|
def check(label, got, want):
|
|
nonlocal cells, passed
|
|
cells += 1
|
|
if got == want:
|
|
passed += 1
|
|
else:
|
|
failures.append("%s\n got: %r\n want: %r" % (label, got, want))
|
|
|
|
def call(*args, **kw):
|
|
"""run() with its report swallowed; a refusal is its own rc (2)."""
|
|
with contextlib.redirect_stdout(io.StringIO()), \
|
|
contextlib.redirect_stderr(io.StringIO()):
|
|
return run(*args, **kw)
|
|
|
|
with tempfile.TemporaryDirectory(prefix="bindings-st-") as td:
|
|
# 1. a __file__ binding in the lane is clean
|
|
repo, lane = _fixture(td, "c1")
|
|
check("1 a lane file bound from __file__ -> rc 0",
|
|
call(repo, lane, "WORKTREE"), 0)
|
|
|
|
# 2. the defect Q153 opened: a lane file bound to the host copy
|
|
_write(os.path.join(lane, "test_bad.py"),
|
|
'import sys\nsys.path.insert(0, "/root/.hermes/wizard-outputs/beam")\n')
|
|
subprocess.run(["git", "-C", repo, "add", "-A"], check=True,
|
|
capture_output=True, text=True)
|
|
rc, faults, graded, files = grade(repo, lane, "WORKTREE")
|
|
check("2 a host binding in the lane -> rc 1", rc, 1)
|
|
check("2b and the row names the file",
|
|
(faults[0][0], faults[0][2]), ("wizard-work/beam/test_bad.py",
|
|
"OUTSIDE_REPO"))
|
|
os.remove(os.path.join(lane, "test_bad.py"))
|
|
subprocess.run(["git", "-C", repo, "add", "-A"], check=True,
|
|
capture_output=True, text=True)
|
|
|
|
# 3. another lane's defect is NOT this lane's fault: scope, not silence
|
|
other = os.path.join(repo, "wizard-work", "spark")
|
|
_write(os.path.join(other, "test_spark.py"),
|
|
'import sys\nsys.path.insert(0, "/root/.hermes/skills")\n')
|
|
subprocess.run(["git", "-C", repo, "add", "-A"], check=True,
|
|
capture_output=True, text=True)
|
|
check("3 another lane's host binding leaves this lane rc 0",
|
|
call(repo, lane, "WORKTREE"), 0)
|
|
|
|
# 4. the authority missing is rc 2, not a green lane
|
|
os.remove(os.path.join(repo, TOOL_REL))
|
|
check("4 a missing authority -> rc 2", call(repo, lane, "WORKTREE"), 2)
|
|
|
|
# 5. and the whole-tree reading still sees what this lane scoped out
|
|
repo2, lane2 = _fixture(td, "c5")
|
|
_write(os.path.join(repo2, "wizard-work", "spark", "test_spark.py"),
|
|
'import sys\nsys.path.insert(0, "/root/.hermes/skills")\n')
|
|
subprocess.run(["git", "-C", repo2, "add", "-A"], check=True,
|
|
capture_output=True, text=True)
|
|
proc = subprocess.run(
|
|
[sys.executable, os.path.join(repo2, TOOL_REL), "--repo", repo2,
|
|
"--rev", "WORKTREE", "--quiet"],
|
|
capture_output=True, text=True)
|
|
check("5 the authority still reds the tree (rc 1)", proc.returncode, 1)
|
|
|
|
print("selftest: %d cells, %d passed, %d failed"
|
|
% (cells, passed, len(failures)))
|
|
for f in failures:
|
|
print(" FAIL %s" % f)
|
|
return 0 if passed == cells else 1
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(
|
|
description="Grade where THIS lane's test files bind the modules they "
|
|
"import, by scoping tools/import_resolve_check.py to the "
|
|
"lane directory.")
|
|
ap.add_argument("--repo", default=DEFAULT_REPO,
|
|
help="git work tree root (default: the checkout holding "
|
|
"this file)")
|
|
ap.add_argument("--lane", default=LANE,
|
|
help="lane directory to scope to (default: this file's)")
|
|
ap.add_argument("--rev", default="WORKTREE", metavar="REV",
|
|
help="WORKTREE (the index, default) or a committed revision")
|
|
ap.add_argument("--quiet", action="store_true")
|
|
ap.add_argument("--selftest", action="store_true")
|
|
a = ap.parse_args(argv)
|
|
if a.selftest:
|
|
return selftest()
|
|
return run(a.repo, a.lane, a.rev, a.quiet)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|