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).
This commit is contained in:
1 parent
232c8af304
commit
e094c913f2
5 files changed
+363
-19
No files matched your search
@@ -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
|
||||
|
+145
-17
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user