From e094c913f2676c817306080397bb1afaa176495b Mon Sep 17 00:00:00 2001 From: ledger Date: Fri, 30 Oct 2026 23:42:34 +0000 Subject: [PATCH] volt: Q132 repair -- tool_map reads a LEGS table whose field is not a literal Q130's closed-artifacts leg added "path": os.path.join(...) to verify.py's LEGS, so ast.literal_eval refused the whole table and every gate leg vanished from docs/TOOLS.md while --check compared against a document rendered before the field existed: the gate-leg column went blank, not red. partial_literal() keeps every element/field that parses and names what it cannot read (legs_note, probes_note) in the document instead of rendering an empty column; _count_by_tool refuses a row whose name is not a string rather than subscripting it into the dict. Selftest 20 -> 23 cells, pin moved with it, docs/TOOLS.md re-rendered (23 tools, 10 gate legs). --- docs/TOOLS.md | 3 +- tools/selftest_baseline.tsv | 2 +- tools/tool_map.py | 162 +++++++++++++++++++++++--- wizard-work/volt/CHECK-q132 | 20 ++++ wizard-work/volt/q132_verify.py | 195 ++++++++++++++++++++++++++++++++ 5 files changed, 363 insertions(+), 19 deletions(-) create mode 100755 wizard-work/volt/CHECK-q132 create mode 100644 wizard-work/volt/q132_verify.py diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 69e72e1..5146d2b 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -32,7 +32,7 @@ module docstring, flags from the tool's own `--help`, probe rows from | `selftest_sweep.py` | every instrument proves itself, in one command, and the number it advertises is the number it prints. | `--repo` `--tools-dir` `--timeout` `--json` `--baseline` `--no-baseline` `--write-baseline` `--quiet` `--verbose` `--selftest` | 1 / 0 / 1 | `selftest-sweep` | | `served_drift_check.py` | does the host actually serve the tree at this revision? | `--root` `--rev` `--base-url` `--prefix` `--exclude` `--max-bytes` `--timeout` `--json` `--quiet` `--selftest` | 1 / 0 / 1 | - | | `tool_map.py` | the instruments get a map, and the map is generated from the tree. | `--repo` `--rev` `--write` `--check` `--json` `--selftest` | 1 / 0 / 1 | - | -| `verify.py` | the HOME the index gate did not have: one command, nine legs, one exit code, and a row per leg so a partial run cannot look like a clean one. | `--repo` `--queue` `--index` `--checkin-day` `--fast` `--timeout` `--json` `--quiet` `--selftest` | 1 / 0 / 1 | - | +| `verify.py` | the HOME the index gate did not have: one command, ten legs, one exit code, and a row per leg so a partial run cannot look like a clean one. | `--repo` `--queue` `--index` `--checkin-day` `--fast` `--timeout` `--json` `--quiet` `--selftest` | 1 / 0 / 1 | - | | `write_probe.py` | Measure the WRITE end of every tool in tools/: `--json PATH` at a target the host cannot write. | `--repo` `--only` `--timeout` `--json` `--selftest` `--verbose` | 1 / 0 / 1 | `write-axis` | ## The gate @@ -52,6 +52,7 @@ dropped by `--fast`. | `selftest-sweep` | `selftest_sweep.py` | yes | every tool's --selftest and both read-refusal axes | | `write-axis` | `write_probe.py` | no | a --json target this host cannot write: rc 2, one line naming it, never a traceback -- and the same rule for every caller-path write flag (--write, --save-vocab) | | `import-resolve` | `import_resolve_check.py` | no | every committed test file binds the module it imports to the repo, not to a host path (static sys.path scan plus --import-probe) | +| `closed-artifacts` | `closed_artifact_guard.py` | no | every repair a closed item landed is still the bytes that landed (sha256 pins for Q109/Q120 and the seven Q126 cells) and its cell still runs green | A tool that appears in no leg is still graded by `selftest-sweep`, which runs every tracked tool's `--selftest` and both read-refusal axes -- so the diff --git a/tools/selftest_baseline.tsv b/tools/selftest_baseline.tsv index ac32e0d..2553ccd 100644 --- a/tools/selftest_baseline.tsv +++ b/tools/selftest_baseline.tsv @@ -29,6 +29,6 @@ queue_index.py 80 cells reply_check.py 49 cells selftest_sweep.py 49 cells served_drift_check.py 25 cells -tool_map.py 20 cells +tool_map.py 23 cells verify.py 59 cells write_probe.py 15 cells diff --git a/tools/tool_map.py b/tools/tool_map.py index a465bad..f849349 100644 --- a/tools/tool_map.py +++ b/tools/tool_map.py @@ -7,7 +7,7 @@ WHY THIS EXISTS queue block, in a reply -- and nothing states the whole set: which tool grades what, which flags each one actually accepts, which of them the two probe axes cover, and which leg of the operator's gate a failure shows up in. The closest - thing is `verify.py`'s `LEGS` table, which names six of them. + thing is `verify.py`'s `LEGS` table, which names ten of them. A hand-written map would be a fourth notation of the defect this suite keeps grading: prose about the tree, one step from the thing it describes, with no @@ -106,7 +106,7 @@ USAGE python3 tool_map.py --repo . --check docs/TOOLS.md # grade drift, rc 1 on drift python3 tool_map.py --repo . --rev WORKTREE --check docs/TOOLS.md # ...as it will be committed python3 tool_map.py --repo . --json out.json - python3 tool_map.py --selftest # 20 cells + python3 tool_map.py --selftest # 23 cells """ from __future__ import annotations @@ -171,8 +171,51 @@ def tracked_tools(repo, rev): return sorted(names) +_UNREADABLE = object() + + +def partial_literal(node): + """`ast.literal_eval`, but a container survives a field that is not literal. + + `literal_eval` refuses the WHOLE table when one value is a call, and the + caller used to drop that table silently. `verify.py`'s `LEGS` gained + `"path": os.path.join(...)` and every gate leg vanished from the map while + `--check` still read rc 0 against a document rendered before the field + existed. A dict here keeps every key whose value parses, a tuple or list + keeps every element that survives, and `_UNREADABLE` is returned only when + the node itself is not a container of literals -- the caller decides + whether that is fatal, and `collect()` says so in the document. + """ + try: + return ast.literal_eval(node) + except (ValueError, SyntaxError): + pass + if isinstance(node, (ast.Tuple, ast.List)): + out = [v for v in (partial_literal(e) for e in node.elts) + if v is not _UNREADABLE] + if node.elts and not out: + return _UNREADABLE # nothing survived: not an empty container + return out + if isinstance(node, ast.Dict): + out = {} + for k, v in zip(node.keys, node.values): + if k is None: # {**mapping} + continue + kv, vv = partial_literal(k), partial_literal(v) + if kv is _UNREADABLE or vv is _UNREADABLE: + continue + out[kv] = vv + return out + return _UNREADABLE + + def literal_assignments(repo, rev, path, names): - """{name: value} for the module-level assignments whose value is a literal.""" + """{name: value} for the module-level assignments whose value is a literal. + + A value that is a container of literals is read even when one field of it + is not (see `partial_literal`); a value that cannot be read at all is + absent from the result, and `collect()` names that absence in the render. + """ src = read_blob(repo, rev, path) if src is None: return {} @@ -187,10 +230,9 @@ def literal_assignments(repo, rev, path, names): tgt = node.targets[0] if not isinstance(tgt, ast.Name) or tgt.id not in names: continue - try: - found[tgt.id] = ast.literal_eval(node.value) - except (ValueError, SyntaxError): - continue + val = partial_literal(node.value) + if val is not _UNREADABLE: + found[tgt.id] = val return found @@ -284,19 +326,25 @@ def flags_of(repo, rev, name): def _count_by_tool(entries): - """Count probes per tool name (first element of each entry tuple). + """Count probes per tool name: ({tool: rows}, rows that were not readable). All three probe tables share the same shape for tallying: each entry's first element is the tool name, and the rest is payload the tally ignores. READ_PROBES / NOT_UTF8_PROBES entries are (tool, argv); - MALFORMED_PROBES entries are (tool, label, ...). This helper replaces the - three identical loops that previously counted each table by hand. + MALFORMED_PROBES entries are (tool, label, ...). A row whose first element + is not a string -- an entry built from a name the AST could not resolve -- + is counted as dropped instead of raising out of the renderer, and + `collect()` names the drop in the document so a short count is never read + as a table that has no more rows. """ - counts = {} + counts, dropped = {}, 0 for entry in entries: - name = entry[0] + name = entry[0] if isinstance(entry, (list, tuple)) and entry else None + if not isinstance(name, str): + dropped += 1 + continue counts[name] = counts.get(name, 0) + 1 - return counts + return counts, dropped def collect(repo, rev): @@ -307,10 +355,27 @@ def collect(repo, rev): sweep = literal_assignments(repo, rev, "tools/selftest_sweep.py", {"READ_PROBES", "NOT_UTF8_PROBES", "MALFORMED_PROBES"}) - read_n = _count_by_tool(sweep.get("READ_PROBES", ())) - utf8_n = _count_by_tool(sweep.get("NOT_UTF8_PROBES", ())) - mal_n = _count_by_tool(sweep.get("MALFORMED_PROBES", ())) + read_n, read_d = _count_by_tool(sweep.get("READ_PROBES", ())) + utf8_n, utf8_d = _count_by_tool(sweep.get("NOT_UTF8_PROBES", ())) + mal_n, mal_d = _count_by_tool(sweep.get("MALFORMED_PROBES", ())) + probes_dropped = read_d + utf8_d + mal_d + probes_note = "" + if probes_dropped: + probes_note = ("%d probe row(s) in `tools/selftest_sweep.py` could not be " + "read as data -- the probe counts above are lower than the " + "tables that carry them" % probes_dropped) + leg_src = read_blob(repo, rev, "tools/verify.py") or "" legs = literal_assignments(repo, rev, "tools/verify.py", {"LEGS"}).get("LEGS", ()) + legs_note = "" + if not legs and re.search(r"^LEGS\s*=", leg_src, re.M): + # An empty column and an unread table render identically, so say which + # one this is in the document itself. A `LEGS` that is a call, an + # import, or a comprehension cannot be read as data; dropping it + # silently is how the gate column went blank while `--check` stayed + # green against a document rendered before the table changed. + legs_note = ("the `LEGS` table in `tools/verify.py` could not be read as " + "data -- the gate column above is empty because the map " + "failed to read it, not because no tool has a leg") leg_of = {} for leg in legs: if isinstance(leg, dict) and "tool" in leg and "key" in leg: @@ -330,7 +395,8 @@ def collect(repo, rev): "malformed": mal_n.get(name, 0), "legs": leg_of.get(name, []), }) - return {"rev": rev, "rows": rows, + return {"rev": rev, "rows": rows, "legs_note": legs_note, + "probes_note": probes_note, "legs": [{"key": l.get("key"), "tool": l.get("tool"), "slow": bool(l.get("slow")), "what": l.get("what", "")} for l in legs if isinstance(l, dict)]} @@ -367,6 +433,8 @@ def render(data): if (r["read"] or r["not_utf8"] or r["malformed"]) else "-") gate = ", ".join(f"`{k}`" for k in r["legs"]) or "-" lines.append(f"| `{r['tool']}` | {purpose} | {flags} | {probes} | {gate} |") + if data.get("probes_note"): + lines += ["", "*" + data["probes_note"] + "*"] lines += [ "", "## The gate", @@ -381,6 +449,8 @@ def render(data): for leg in legs: lines.append(f"| `{leg['key']}` | `{leg['tool']}` | " f"{'yes' if leg['slow'] else 'no'} | {leg['what']} |") + if data.get("legs_note"): + lines += ["", "*" + data["legs_note"] + "*"] lines += [ "", "A tool that appears in no leg is still graded by `selftest-sweep`, which runs", @@ -528,6 +598,64 @@ def selftest(repo): cell("WORKTREE outside a git work tree: rc 2, not a crash", rc_wt_norepo == 2, f"rc {rc_wt_norepo}") + # -- a leg field that is not a literal must not erase the leg -------- + # Q130 gave the closed-artifacts leg `"path": os.path.join(...)`. That + # one call made `ast.literal_eval` refuse the whole LEGS table, so every + # leg vanished from the map while `--check` stayed green against a + # document rendered before the field existed. Both directions are + # pinned here: a non-literal FIELD keeps its leg, and a LEGS that is not + # data at all is named in the rendered document instead of rendering an + # empty column that reads as "no tool has a leg". + vpath = os.path.join(fixture, "tools", "verify.py") + with open(vpath, "w") as fh: + fh.write('import os\n' + 'LEGS = ({"key": "fake", "tool": "fake_tool.py",\n' + ' "slow": False,\n' + ' "path": os.path.join("tools", "fake_tool.py"),\n' + ' "what": "grade the fake contract"},)\n') + commit("non-literal leg field") + d2 = collect(fixture, "HEAD") + by2 = {r["tool"]: r for r in d2["rows"]} + cell("collect: a leg field that is not a literal does not erase the leg", + by2["fake_tool.py"]["legs"] == ["fake"] + and [l["key"] for l in d2["legs"]] == ["fake"] + and d2["legs_note"] == "", + str((by2["fake_tool.py"]["legs"], d2["legs"], d2["legs_note"]))) + + with open(vpath, "w") as fh: + fh.write('LEGS = build_legs()\n') + commit("unreadable LEGS") + d3 = collect(fixture, "HEAD") + txt3 = render(d3) + cell("render: an unreadable LEGS table is named, not silently empty", + d3["legs"] == [] and d3["legs_note"] != "" + and "could not be read as data" in txt3 + and {r["tool"]: r["legs"] for r in d3["rows"]}["fake_tool.py"] == [], + str((d3["legs"], d3["legs_note"]))) + + # A probe row whose element is not a literal must not become an empty + # element the counter then subscripts: `_count_by_tool` reads entry[0], + # so a dropped name raised IndexError out of the renderer. + with open(os.path.join(fixture, "tools", "verify.py"), "w") as fh: + fh.write('LEGS = ({"key": "fake", "tool": "fake_tool.py",\n' + ' "slow": False, "what": "grade the fake contract"},)\n') + with open(os.path.join(fixture, "tools", "selftest_sweep.py"), "w") as fh: + fh.write('READ_PROBES = ((TOOL_NAME, ["--repo", "{nope}"]),)\n' + 'NOT_UTF8_PROBES = ()\nMALFORMED_PROBES = ()\n') + commit("non-literal probe element") + try: + d4 = collect(fixture, "HEAD") + crash = "" + read_n4 = {r["tool"]: r["read"] for r in d4["rows"]}["fake_tool.py"] + note4 = d4["probes_note"] + txt4 = render(d4) + except Exception as exc: # noqa: BLE001 - graded + crash, read_n4, note4, txt4 = "%s: %s" % (type(exc).__name__, exc), None, "", "" + cell("collect: a probe row that is not a literal is dropped, not indexed", + crash == "" and read_n4 == 0 and "could not be read as data" in note4 + and note4 in txt4, + crash or f"read={read_n4} note={note4!r}") + # -- the currency cell: the reason this document stays true ---------- here = os.path.join(repo, "docs", "TOOLS.md") try: diff --git a/wizard-work/volt/CHECK-q132 b/wizard-work/volt/CHECK-q132 new file mode 100755 index 0000000..094c35c --- /dev/null +++ b/wizard-work/volt/CHECK-q132 @@ -0,0 +1,20 @@ +#!/bin/sh +# Q132 CHECK -- tool_map.py must read a LEGS table whose field is not a +# literal, must name a table it cannot read, and must stay the render of +# this tree. +# +# rc 0 = every leg as expected; 1 = a graded fault; 2 = could not grade. +# The work is done by wizard-work/volt/q132_verify.py beside this file; +# every fixture leg runs in a throwaway git repo, so the graded tree is +# only ever read. Run it from any checkout. +set -u +REPO="$(cd "$(dirname "$0")/../.." && pwd)" || exit 2 +if ! cd "$REPO"; then + echo "q132: could not grade -- cannot cd to $REPO" + exit 2 +fi +if ! command -v python3 >/dev/null 2>&1; then + echo "q132: could not grade -- no python3 on PATH" + exit 2 +fi +exec python3 wizard-work/volt/q132_verify.py diff --git a/wizard-work/volt/q132_verify.py b/wizard-work/volt/q132_verify.py new file mode 100644 index 0000000..9058033 --- /dev/null +++ b/wizard-work/volt/q132_verify.py @@ -0,0 +1,195 @@ +#!/usr/bin/env python3 +"""Q132 instrument: `tool_map` must read a `LEGS` table whose field is not a literal. + +The fault, measured at `d0ba06e` before any edit: `tools/verify.py`'s `LEGS` +gained `"path": os.path.join("wizard-work", "spark", +"closed_artifact_guard.py")` when Q130 added the `closed-artifacts` leg. That +one call made `ast.literal_eval` refuse the whole table, so +`literal_assignments()` returned nothing for `LEGS`, every row's gate column +rendered `-`, and the render no longer matched `docs/TOOLS.md`. The gate-leg +column did not go red; it went BLANK. + +Legs + 1. control, fixed parser a fixture whose `LEGS` carries a call-valued + field renders the leg by name + 2. mutant, pre-fix parser the same fixture and document rendered by + `ast.literal_eval`-only: the leg is gone, the + document drifts, and `--check` reads rc 1 + 3. unreadable table a `LEGS` that is not data at all is named in + the rendered document, not rendered as `-` + 4. real tree is current `--check docs/TOOLS.md` rc 0 at HEAD and at + WORKTREE, and the render names 10 gate legs + 5. seller's own cells `--selftest` rc 0 and 22/22 cells printed + +Every fixture leg runs in its own throwaway git repo under a temp directory; +the graded tree is only read. Exit codes follow the repo contract: 0 every +leg as expected, 1 a leg disagreed, 2 could not grade. +""" + +import os +import re +import shutil +import subprocess +import sys +import tempfile + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.dirname(os.path.dirname(HERE)) +TOOL_MAP = os.path.join(REPO, "tools", "tool_map.py") + +FAIL = [] +COUNT = [0] + +MUT_OLD = """ val = partial_literal(node.value) + if val is not _UNREADABLE: + found[tgt.id] = val +""" +MUT_NEW = """ try: + found[tgt.id] = ast.literal_eval(node.value) + except (ValueError, SyntaxError): + continue +""" + +FAKE_TOOL = ('#!/usr/bin/env python3\n' + '"""fake_tool.py -- grades the fake contract, which nothing measured."""\n' + 'import sys\nprint("ran")\n') +SWEEP = ('READ_PROBES = ()\nNOT_UTF8_PROBES = ()\nMALFORMED_PROBES = ()\n') +VERIFY_LITERAL_FIELD = ( + 'import os\n' + 'LEGS = ({"key": "fake", "tool": "fake_tool.py", "slow": False,\n' + ' "path": os.path.join("tools", "fake_tool.py"),\n' + ' "what": "grade the fake contract"},)\n') + + +def leg(name, ok, detail=""): + COUNT[0] += 1 + print("%s %-26s %s" % ("PASS" if ok else "FAIL", name, detail)) + if not ok: + FAIL.append(name) + + +def run(argv, cwd=None): + p = subprocess.run(argv, cwd=cwd, capture_output=True, text=True) + return p.returncode, p.stdout, p.stderr + + +def git(repo, *args): + env = dict(os.environ, GIT_AUTHOR_NAME="t", GIT_AUTHOR_EMAIL="t@e", + GIT_COMMITTER_NAME="t", GIT_COMMITTER_EMAIL="t@e") + return run(["git", "-C", repo] + list(args))[0], env + + +def build_fixture(root, verify_src=VERIFY_LITERAL_FIELD): + """A three-tool git work tree whose LEGS table is the grading surface.""" + os.makedirs(os.path.join(root, "tools")) + os.makedirs(os.path.join(root, "docs")) + for name, body in (("fake_tool.py", FAKE_TOOL), + ("selftest_sweep.py", SWEEP), + ("verify.py", verify_src)): + with open(os.path.join(root, "tools", name), "w") as fh: + fh.write(body) + env = dict(os.environ, GIT_AUTHOR_NAME="t", GIT_AUTHOR_EMAIL="t@e", + GIT_COMMITTER_NAME="t", GIT_COMMITTER_EMAIL="t@e") + run(["git", "init", "-q", root], cwd=None) + run(["git", "-C", root, "add", "-A"]) + run(["git", "-C", root, "-c", "user.name=t", "-c", "user.email=t@e", + "commit", "-qm", "fixture"]) + return env + + +def gate_cell(doc_text, tool="fake_tool.py"): + """The last table cell of the row for `tool`, or None.""" + for line in doc_text.splitlines(): + if line.startswith("| `%s`" % tool): + return line.rstrip().rstrip("|").split("|")[-1].strip() + return None + + +def main(): + if not os.path.isfile(TOOL_MAP): + print("q132: could not grade -- no tools/tool_map.py at %s" % REPO) + return 2 + + td = tempfile.mkdtemp(prefix="q132.") + fixed = os.path.join(td, "tool_map_fixed.py") + shutil.copyfile(TOOL_MAP, fixed) + mutant = os.path.join(td, "tool_map_mutant.py") + with open(TOOL_MAP, encoding="utf-8") as fh: + src = fh.read() + if src.count(MUT_OLD) != 1: + print("q132: could not grade -- the mutant block appears %d time(s)" + % src.count(MUT_OLD)) + return 2 + with open(mutant, "w", encoding="utf-8") as fh: + fh.write(src.replace(MUT_OLD, MUT_NEW)) + + # -- 1. control: the fixed parser reads a call-valued field -------------- + fx = os.path.join(td, "fx") + build_fixture(fx) + doc = os.path.join(fx, "docs", "TOOLS.md") + rc, out, err = run([sys.executable, fixed, "--repo", fx, "--write", doc]) + with open(doc, encoding="utf-8") as fh: + text = fh.read() + cell = gate_cell(text) + leg("control-render", rc == 0 and cell == "`fake`", + "rc=%d gate=%r" % (rc, cell)) + + # -- 2. mutant: the pre-fix parser loses the leg and drifts ------------- + mdoc = os.path.join(fx, "docs", "MUTANT.md") + rc_m, out_m, err_m = run([sys.executable, mutant, "--repo", fx, + "--write", mdoc]) + with open(mdoc, encoding="utf-8") as fh: + mtext = fh.read() + mcell = gate_cell(mtext) + rc_c, out_c, err_c = run([sys.executable, mutant, "--repo", fx, + "--check", doc]) + leg("mutant-loses-leg", rc_m == 0 and mcell == "-", + "rc=%d gate=%r" % (rc_m, mcell)) + leg("mutant-doc-drifts", rc_c == 1 and "DRIFT" in out_c, + "rc=%d first=%r" % (rc_c, (out_c or err_c).splitlines()[:1])) + + # -- 3. a LEGS that is not data is named, not rendered as `-` ----------- + fx2 = os.path.join(td, "fx2") + build_fixture(fx2, verify_src="LEGS = build_legs()\n") + doc2 = os.path.join(fx2, "docs", "TOOLS.md") + rc2, out2, err2 = run([sys.executable, fixed, "--repo", fx2, + "--write", doc2]) + with open(doc2, encoding="utf-8") as fh: + text2 = fh.read() + named = "could not be read as data" in text2 + rc2c, out2c, _ = run([sys.executable, fixed, "--repo", fx2, + "--check", doc2]) + leg("unreadable-named", rc2 == 0 and named and gate_cell(text2) == "-", + "rc=%d named=%s gate=%r" % (rc2, named, gate_cell(text2))) + leg("unreadable-still-checks", rc2c == 0, + "rc=%d (the document states the failure, so it is current)" % rc2c) + + # -- 4. the real tree: the column is populated and the map is current --- + rdoc = os.path.join(td, "TOOLS.real.md") + rc3, out3, err3 = run([sys.executable, TOOL_MAP, "--repo", REPO, + "--rev", "WORKTREE", "--write", rdoc]) + told = re.search(r"(\d+) gate legs", out3 or "") + legs_n = int(told.group(1)) if told else -1 + rc_h, out_h, err_h = run([sys.executable, TOOL_MAP, "--repo", REPO, + "--check", os.path.join(REPO, "docs", "TOOLS.md")]) + rc_w, out_w, err_w = run([sys.executable, TOOL_MAP, "--repo", REPO, + "--rev", "WORKTREE", "--check", + os.path.join(REPO, "docs", "TOOLS.md")]) + leg("real-tree-legs", rc3 == 0 and legs_n == 10, "rc=%d legs=%d" % (rc3, legs_n)) + leg("real-tree-current", rc_h == 0 and rc_w == 0, + "HEAD rc=%d WORKTREE rc=%d %s" % (rc_h, rc_w, (out_h or err_h).strip()[:80])) + + # -- 5. the tool's own cells ------------------------------------------- + rc4, out4, err4 = run([sys.executable, TOOL_MAP, "--repo", REPO, "--selftest"]) + m = re.search(r"selftest: (\d+) cells, (\d+) passed, (\d+) failed", out4 or "") + ok4 = bool(m) and m.group(1) == m.group(2) == "23" and m.group(3) == "0" + leg("tool-selftest", rc4 == 0 and ok4, + "rc=%d %s" % (rc4, m.group(0) if m else (err4 or out4).strip()[:90])) + + print("q132: %d leg(s), %d fault -- %s" + % (COUNT[0], len(FAIL), "OK" if not FAIL else "FAILED")) + return 0 if not FAIL else 1 + + +if __name__ == "__main__": + sys.exit(main())