Files
MobileGL/scripts/ci/wire_declines_audit.py
T
swung0x48 266d6a32f8 [tools] (P7): the wire declines audit carries a self-test whose fixtures must red, and the CI step runs it before the tree audit
Fable review of the B3 rework, should-fix: the audit had no negative
control, so a green run could not say it would have been red. It now takes
--self-test, which writes each inline fixture as a one-file renderer tree in
a temp dir, runs the audit on it and checks both the rc and the line the
audit must print (so a red is red for the fixture's reason):

  comment-only site (//)                -> rc 1, "has NO SITE"
  comment-only site (/* */)             -> rc 1, "has NO SITE"
  string-literal site                   -> rc 1, "has NO SITE"
  other-branch log                      -> rc 1, "with no MGLOG_W/E in its own block"
  nested-branch log                     -> rc 1, same
  log only in a /* */ comment above     -> rc 1, same
  nearest log is MGLOG_D                -> rc 1, same
  good sites (AT form, bare Count under
    its own MGLOG_E_ONCE, def row with a
    trailing comment)                   -> rc 0, "0 unlogged, 0 unknown"

8/8, rc 0. Mutants: with the strip disabled 5 of 8 fail (the four
comment/string reds go green and the good fixture's commented row is
lost); with the old 8-line window in place of the block walk 2 of 8 fail
(other-branch, nested-branch). Against the audit as it stood at aab01275,
six of the seven red fixtures passed and the good one failed.

.github/workflows/test.yml: the "Wire declines audit (B3)" step runs
--self-test and then the tree audit, the way the next step runs
trace_cases.py --self-test-transport-matrices before it computes matrices.
2026-09-22 20:49:45 -04:00

292 lines
12 KiB
Python

#!/usr/bin/env python3
"""P7 wave 2 package B3: WireDeclines.def rows and their sites must agree.
CONTRACT-P7 §0 rule I admits two shapes for a wire-arm refusal, a decline and a named Fatal.
The decline half is only real if:
(1) every row in WireDeclines.def is actually counted somewhere - a row with no site is a
name that can never move, which reads in a report as "this cannot happen here" when it
only means "nobody wired it up"; and
(2) every site emits a line - a counter with no log puts the next reader back on a bisect,
which is the thing the package exists to remove.
Rows are APPEND-ONLY (the lanes assert specific counters), so (1) cannot be fixed later by
deleting a row once a release has shipped it. This check is why it cannot rot.
Sites reached through MGL_WIRE_DECLINE_AT satisfy both at once. A bare
WireDeclineTally::Count(WireDeclineSite::X) is allowed ONLY when an MGLOG_W/E statement stands
just above it IN THE SAME BLOCK: at the Count line's own indentation, with no line of lesser
indentation (an enclosing `if (...) {`, a `} else {`) between them. A log in a sibling branch
or a nested one is not this site's line.
Everything is matched on the text with comments and string/character literals blanked out
(newlines kept, so every report cites the real line): a site or a log that is only a comment,
or only a string, is not one.
usage: wire_declines_audit.py [repo-root]
wire_declines_audit.py --self-test (negative controls on inline fixtures)
"""
import contextlib
import io
import os
import re
import sys
import tempfile
RENDERER = "MobileGL/MG_Backend/DirectVulkan/Renderer"
DEF = os.path.join(RENDERER, "WireDeclines.def")
SOURCES = (".cpp", ".inc", ".h")
# How far above a bare Count() an MGLOG_ may stand and still be "the line for this site" -
# a cap on top of the block walk, so a log far up the same block does not count either.
LOG_WINDOW = 8
# Matched on comment-stripped text, so a row with a trailing `// note` is still a row.
ROW = re.compile(r"^\s*MGL_WIRE_DECLINE\(\s*([A-Za-z][A-Za-z0-9]*)\s*\)\s*$")
AT_SITE = re.compile(r"MGL_WIRE_DECLINE_AT\(\s*([A-Za-z][A-Za-z0-9]*)\s*,")
COUNT_SITE = re.compile(r"WireDeclineTally::Count\(\s*WireDeclineSite::([A-Za-z][A-Za-z0-9]*)\s*\)")
# Warning or error only: an MGLOG_D / MGLOG_V is compiled away in the Release builds that ship,
# so it is not a line anybody can read off a device.
LOGGED = re.compile(r"MGLOG_[WE](_ONCE)?\s*\(")
RAW_PREFIX = re.compile(r"(?:^|[^A-Za-z0-9_])(?:u8|u|U|L)?R$")
def strip_code(text: str) -> str:
"""Blank out //, /* */ comments and string / character literals, keeping every newline.
What remains lines up with the source line for line, so a match's index is a real line
number. Raw strings R"d(...)d" and C++14 digit separators (1'000) are handled."""
out = list(text)
n = len(text)
i = 0
def blank(a: int, b: int) -> None:
for k in range(a, min(b, n)):
if out[k] != "\n":
out[k] = " "
while i < n:
c = text[i]
nxt = text[i + 1] if i + 1 < n else ""
if c == "/" and nxt == "/":
j = i
while j < n and text[j] != "\n":
# A backslash-newline continues a // comment onto the next line.
if text[j] == "\\" and j + 1 < n and text[j + 1] == "\n":
j += 2
continue
j += 1
blank(i, j)
i = j
elif c == "/" and nxt == "*":
j = text.find("*/", i + 2)
j = n if j < 0 else j + 2
blank(i, j)
i = j
elif c == '"' and RAW_PREFIX.search(text[max(0, i - 3):i]):
open_paren = text.find("(", i + 1)
if open_paren < 0:
blank(i, n)
break
delim = text[i + 1:open_paren]
close = text.find(")" + delim + '"', open_paren + 1)
j = n if close < 0 else close + len(delim) + 2
blank(i, j)
i = j
elif c == '"' or (c == "'" and not (i > 0 and (text[i - 1].isalnum() or text[i - 1] == "_"))):
j = i + 1
while j < n and text[j] != c and text[j] != "\n":
j += 2 if text[j] == "\\" else 1
j = min(j + 1, n)
blank(i, j)
i = j
else:
i += 1
return "".join(out)
def indent_of(line: str) -> int:
return len(line.expandtabs(4)) - len(line.expandtabs(4).lstrip())
def has_own_log(lines, i: int) -> bool:
"""Is there an MGLOG_W/E statement above line i in the same block, within LOG_WINDOW?
Walk upwards; blank lines and deeper lines (a log's continuation lines, a nested block)
are passed over, but only a line at exactly the Count's indentation can be its log. The
first line indented LESS than the Count is the block's opening (or a sibling branch's
`} else {`) and ends the walk."""
own = indent_of(lines[i])
for k in range(i - 1, max(-1, i - 1 - LOG_WINDOW), -1):
line = lines[k]
if not line.strip():
continue
ind = indent_of(line)
if ind < own:
return False
if ind == own and LOGGED.search(line):
return True
return False
def audit(root: str) -> int:
def_path = os.path.join(root, DEF)
if not os.path.isfile(def_path):
print("wire-declines: %s not found" % DEF, file=sys.stderr)
return 2
rows = []
with open(def_path, encoding="utf-8") as f:
def_text = strip_code(f.read())
for line in def_text.splitlines():
m = ROW.match(line)
if m:
rows.append(m.group(1))
if len(rows) != len(set(rows)):
dupes = sorted({r for r in rows if rows.count(r) > 1})
print("wire-declines: duplicate rows: %s" % ", ".join(dupes), file=sys.stderr)
return 1
counted = {} # name -> [(file, line)]
unlogged_counts = [] # (file, line, name)
src_dir = os.path.join(root, RENDERER)
for entry in sorted(os.listdir(src_dir)):
if not entry.endswith(SOURCES):
continue
# The header that DEFINES the macro and the enum names them all without being a site.
if entry == "WireDeclineTally.h":
continue
path = os.path.join(src_dir, entry)
with open(path, encoding="utf-8", errors="replace") as f:
lines = strip_code(f.read()).splitlines()
for i, line in enumerate(lines):
for m in AT_SITE.finditer(line):
counted.setdefault(m.group(1), []).append((entry, i + 1))
for m in COUNT_SITE.finditer(line):
name = m.group(1)
counted.setdefault(name, []).append((entry, i + 1))
if not has_own_log(lines, i):
unlogged_counts.append((entry, i + 1, name))
orphan_rows = [r for r in rows if r not in counted]
unknown_sites = sorted(set(counted) - set(rows))
for name in orphan_rows:
print("wire-declines: row %s has NO SITE - it can never be counted" % name, file=sys.stderr)
for name in unknown_sites:
for where in counted[name]:
print("wire-declines: %s:%d counts %s, which is not a row in WireDeclines.def"
% (where[0], where[1], name), file=sys.stderr)
for entry, line, name in unlogged_counts:
print("wire-declines: %s:%d counts %s with no MGLOG_W/E in its own block within %d lines "
"above it - a decline must be readable from the log, not only from a counter"
% (entry, line, name, LOG_WINDOW), file=sys.stderr)
bad = len(orphan_rows) + len(unknown_sites) + len(unlogged_counts)
print("wire-declines: %d rows, %d with sites, %d site(s) total, %d unlogged, %d unknown"
% (len(rows), len(rows) - len(orphan_rows), sum(len(v) for v in counted.values()),
len(unlogged_counts), len(unknown_sites)))
return 1 if bad else 0
# --self-test: each fixture is (name, def rows text, Fixture.cpp text, expected rc, text the
# audit must print - so a red is red for the reason the fixture is about). The red ones are
# the holes the B3 fix round closed; the green one proves the audit still accepts the two
# shapes the tree uses.
NO_SITE = "has NO SITE"
NO_LOG = "with no MGLOG_W/E in its own block"
_DEF_GHOST = "MGL_WIRE_DECLINE(GhostRow)\n"
SELF_TEST_FIXTURES = (
("comment-only site (//)", _DEF_GHOST,
"void F() {\n"
" // MGL_WIRE_DECLINE_AT(GhostRow, \"never counted\");\n"
"}\n", 1, NO_SITE),
("comment-only site (/* */)", _DEF_GHOST,
"void F() {\n"
" /* retired:\n"
" MGL_WIRE_DECLINE_AT(GhostRow, \"never counted\"); */\n"
"}\n", 1, NO_SITE),
("string-literal site", _DEF_GHOST,
"const char* kDoc = \"MGL_WIRE_DECLINE_AT(GhostRow, x);\";\n", 1, NO_SITE),
("other-branch log", _DEF_GHOST,
"void F(int x) {\n"
" if (x == 1) {\n"
" MGLOG_W(\"x is one\");\n"
" }\n"
" if (x == 2) {\n"
" WireDeclineTally::Count(WireDeclineSite::GhostRow);\n"
" }\n"
"}\n", 1, NO_LOG),
("nested-branch log", _DEF_GHOST,
"void F(int x) {\n"
" if (x) {\n"
" if (x == 1) {\n"
" MGLOG_W(\"x is one\");\n"
" }\n"
" WireDeclineTally::Count(WireDeclineSite::GhostRow);\n"
" }\n"
"}\n", 1, NO_LOG),
("log only in a /* */ comment above", _DEF_GHOST,
"void F(int x) {\n"
" if (x) {\n"
" /* MGLOG_W(\"x\"); */\n"
" WireDeclineTally::Count(WireDeclineSite::GhostRow);\n"
" }\n"
"}\n", 1, NO_LOG),
("nearest log is MGLOG_D", _DEF_GHOST,
"void F(int x) {\n"
" if (x) {\n"
" MGLOG_D(\"compiled away in Release\");\n"
" WireDeclineTally::Count(WireDeclineSite::GhostRow);\n"
" }\n"
"}\n", 1, NO_LOG),
("good sites (AT form, bare Count under its own W/E, def row with trailing comment)",
"MGL_WIRE_DECLINE(GoodAt) // the macro form\nMGL_WIRE_DECLINE(GoodBare)\n",
"bool F(int x) {\n"
" if (x == 1) {\n"
" MGL_WIRE_DECLINE_AT(GoodAt, \"x=%d\", x);\n"
" return false;\n"
" }\n"
" if (x == 2) {\n"
" MGLOG_E_ONCE(\"x is two: %d, \"\n"
" \"declined\", x);\n"
" WireDeclineTally::Count(WireDeclineSite::GoodBare);\n"
" return false;\n"
" }\n"
" return true;\n"
"}\n", 0, "0 unlogged, 0 unknown"),
)
def self_test() -> int:
failures = 0
for name, def_text, src_text, want, why in SELF_TEST_FIXTURES:
with tempfile.TemporaryDirectory(prefix="wire-declines-selftest-") as root:
src_dir = os.path.join(root, RENDERER)
os.makedirs(src_dir)
with open(os.path.join(src_dir, "WireDeclines.def"), "w", encoding="utf-8") as f:
f.write(def_text)
with open(os.path.join(src_dir, "Fixture.cpp"), "w", encoding="utf-8") as f:
f.write(src_text)
captured = io.StringIO()
with contextlib.redirect_stdout(captured), contextlib.redirect_stderr(captured):
got = audit(root)
ok = got == want and why in captured.getvalue()
failures += 0 if ok else 1
print("wire-declines self-test: %-4s %s -> rc %d (want %d, printing \"%s\")"
% ("ok" if ok else "FAIL", name, got, want, why))
if not ok:
sys.stdout.write("".join(" " + l + "\n" for l in captured.getvalue().splitlines()))
print("wire-declines self-test: %d/%d fixtures as expected"
% (len(SELF_TEST_FIXTURES) - failures, len(SELF_TEST_FIXTURES)))
return 1 if failures else 0
def main() -> int:
if len(sys.argv) > 1 and sys.argv[1] == "--self-test":
return self_test()
return audit(os.path.abspath(sys.argv[1] if len(sys.argv) > 1 else "."))
if __name__ == "__main__":
sys.exit(main())