diff --git a/tools/verify.py b/tools/verify.py index 94b044e..6cd42c4 100644 --- a/tools/verify.py +++ b/tools/verify.py @@ -58,7 +58,7 @@ TOOLS ARE RESOLVED FROM --repo, NOT FROM THIS FILE'S DIRECTORY docs/queue-index.md) because `queue_append_audit` reads them with `git show :`, which has no absolute-path form. -`--selftest` fixture corpus, 24 cells +`--selftest` fixture corpus, 26 cells """ import argparse @@ -92,11 +92,23 @@ HINTS = ("STALE:", "FAIL", "FAILED", "fatal", "cannot", "TIMEOUT", "NO_", def pick_detail(text, limit=150): - """First line that names a reason, else the last line that says anything.""" + """First line that names a reason, else the last line that says anything. + + A summary line that reports ZERO faults is not a reason: `selftest_sweep` + prints `sweep: 15 tools, 15 OK, 0 fatal` even on the run where its input + axes went red, and that line was being carried onto a FAULT row -- the row + said FAULT beside the words "0 fatal". Zero-count summaries are therefore + only reachable on the second pass (a clean run still gets its sweep line), + and any line that names a real reason outranks them. + """ lines = [ln.strip() for ln in text.splitlines() if ln.strip()] - for ln in lines: - if any(h in ln for h in HINTS): - return ln[:limit] + hinted = [ln for ln in lines if any(h in ln for h in HINTS)] + reasons = [ln for ln in hinted + if " 0 fatal" not in ln and " 0 fault" not in ln] + if reasons: + return reasons[0][:limit] + if hinted: + return hinted[0][:limit] return lines[-1][:limit] if lines else "" @@ -219,7 +231,7 @@ def main(argv=None): help="per-leg timeout in seconds (default 300)") ap.add_argument("--json", metavar="PATH", help="write the rows here") ap.add_argument("--quiet", action="store_true") - ap.add_argument("--selftest", action="store_true", help="fixture corpus, 24 cells") + ap.add_argument("--selftest", action="store_true", help="fixture corpus, 26 cells") a = ap.parse_args(argv) if a.selftest: @@ -414,6 +426,18 @@ def selftest(): check("12b and the skipped leg is not counted as a pass", obj.get("counts", {}).get(PASS), 2) + # 13. the detail line must not contradict the verdict: a row graded + # FAULT beside "0 fatal" is the SILENT failure this gate exists to catch. + check("13 a zero-fault summary is not a reason line", + pick_detail("sweep: 15 tools, 15 OK, 0 fatal\n" + "read-refusal: 20 probes, 19 ok, 1 fault (NO_PROBE)\n" + "malformed-input: 18 probes, 17 ok, 1 fault (NO_PROBE)\n"), + "read-refusal: 20 probes, 19 ok, 1 fault (NO_PROBE)") + check("13b a clean run still gets its sweep line", + pick_detail("sweep: 14 tools, 14 OK, 0 fatal\n" + "read-refusal: 19 probes, 19 ok, 0 fault\n"), + "sweep: 14 tools, 14 OK, 0 fatal") + print("selftest: %d cells, %d passed, %d failed" % (cells, passed, len(failures))) for f in failures: print(" FAIL %s" % f)