7bc26f80d5
All three found by the adversarial review of 019cbc1, each by injection.
1. every_model_command() is NOT compiler-maintained, and both this PR's prose
and reduce.rs's own doc comment said it was. variant_name's exhaustive match
forces an arm in variant_name, not an entry in the value list -- deleting
ModelCommand::TogglePruneEmpty from it failed the new completeness check and
NOTHING ELSE in the workspace. So the diff is two independent hand-written
lists: strictly better than one list against its own length, and not the
derived enumeration improvise-5ubs got for the two enums. Doc comment
corrected; the prose in context/code/command.md and the evidence entry now
say what it is. Closing it properly wants improvise_protocol::variants made
pub so declared::<ModelCommand>() can bound the list -- another wave's crate,
reported rather than taken.
The assertion message was also ONE-DIRECTIONAL ("a name only on the right
..."), which is the shape that injection produces and which invites the wrong
repair: delete the case on the left, degrading both sides together. It now
names both directions and says which is which.
2. SELF_TEST_DUPLICATES case 3 was named for the ambiguity guard and never
reached it: both its headings were module-qualified, so canonical() returned
on the exact-match arm before touching by_leaf. Dropping the guard
(`if matches` for `if len(matches) == 1`) left --self-test at exit 0. The
fixture is now one BARE heading over an ambiguous leaf plus BOTH qualified
spellings, so the injection collides with one of them whichever way
next(iter(set)) falls -- deterministic across five runs, verified.
3. The evidence entry said five pre-existing records-mode tests fail under the
settle-before-rebuild injection; measured, it is six. And "three hundred
lines above" is four hundred.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FC2jVn8Au7E9JPkw6bwXXH
1189 lines
43 KiB
Python
Executable File
1189 lines
43 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Fail if any .rs file carries more than two consecutive comment lines.
|
|
|
|
The rule (user, 2026-09-20): reasoning lives in context/ prose, not in code.
|
|
A comment run is at most two lines, optionally naming a context/ file for the
|
|
rest. See context/code/ for where the prose went, and improvise-9c8c for why.
|
|
|
|
Counts `///`, `//!` and `//` alike. Attributes are not comments and are
|
|
ignored.
|
|
|
|
A ``` fence SUSPENDS the count rather than ending the run: the fenced body is
|
|
exempt (a doctest is a running test, not prose) but the prose on either side
|
|
belongs to ONE run and is summed. Two lines above a fence and two below is a
|
|
run of four, which is over the limit -- and that is deliberate, because a
|
|
reader meets it as one uninterrupted comment block. See --list-doctests.
|
|
|
|
Note rustdoc executes a bare ``` or ```rust fence and does NOT execute
|
|
```text, ```ignore or ```no_run. Only a runnable fence earns the exemption;
|
|
the rest is prose wearing a fence and the cap applies to it.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
LIMIT = 2
|
|
|
|
|
|
_RAW_STR = re.compile(r'(?:b|c)?r(#*)"')
|
|
_CHAR_LIT = re.compile(r"'(?:\\.|[^'\\])'")
|
|
_DOC_ATTR = re.compile(r"#!?\[\s*doc\s*=")
|
|
|
|
|
|
def scan(lines):
|
|
"""Classify each line, respecting Rust strings and NESTED block comments.
|
|
|
|
Returns per line: has_code, has_comment, the `//` body, the CODE-ONLY text
|
|
(no string or comment content), whether comment content leads the line, and
|
|
whether the line began inside a block comment. See context/code/tooling.md.
|
|
"""
|
|
out = []
|
|
in_block = 0
|
|
in_raw = None
|
|
in_str = False
|
|
for raw in lines:
|
|
i, n = 0, len(raw)
|
|
started_in_block = in_block > 0
|
|
has_code = False
|
|
has_comment = started_in_block
|
|
body = None
|
|
code = []
|
|
leads = None
|
|
while i < n:
|
|
if in_block:
|
|
has_comment = True
|
|
leads = True if leads is None else leads
|
|
if raw.startswith("/*", i):
|
|
in_block += 1
|
|
i += 2
|
|
elif raw.startswith("*/", i):
|
|
in_block -= 1
|
|
i += 2
|
|
else:
|
|
i += 1
|
|
continue
|
|
if in_raw is not None:
|
|
closer = '"' + "#" * in_raw
|
|
if raw.startswith(closer, i):
|
|
in_raw = None
|
|
i += len(closer)
|
|
else:
|
|
i += 1
|
|
has_code = True
|
|
leads = False if leads is None else leads
|
|
continue
|
|
if in_str:
|
|
has_code = True
|
|
leads = False if leads is None else leads
|
|
if raw[i] == "\\":
|
|
i += 2
|
|
continue
|
|
if raw[i] == '"':
|
|
in_str = False
|
|
i += 1
|
|
continue
|
|
if raw[i].isspace():
|
|
i += 1
|
|
continue
|
|
if raw.startswith("//", i):
|
|
has_comment = True
|
|
leads = True if leads is None else leads
|
|
if not has_code:
|
|
body = raw[i:]
|
|
break
|
|
if raw.startswith("/*", i):
|
|
has_comment = True
|
|
leads = True if leads is None else leads
|
|
in_block = 1
|
|
i += 2
|
|
continue
|
|
m = _RAW_STR.match(raw, i)
|
|
if m:
|
|
in_raw = len(m.group(1))
|
|
has_code = True
|
|
leads = False if leads is None else leads
|
|
code.append('"')
|
|
i = m.end()
|
|
continue
|
|
m = _CHAR_LIT.match(raw, i)
|
|
if m:
|
|
has_code = True
|
|
leads = False if leads is None else leads
|
|
i = m.end()
|
|
continue
|
|
if raw[i] == '"':
|
|
in_str = True
|
|
has_code = True
|
|
leads = False if leads is None else leads
|
|
i += 1
|
|
continue
|
|
has_code = True
|
|
leads = False if leads is None else leads
|
|
code.append(raw[i])
|
|
i += 1
|
|
out.append((has_code, has_comment, body, "".join(code), bool(leads), started_in_block))
|
|
return out
|
|
|
|
|
|
def _kind(body):
|
|
if body is None:
|
|
return "//"
|
|
if body.startswith("//!"):
|
|
return "//!"
|
|
return "///" if body.startswith("///") else "//"
|
|
|
|
|
|
def comment_runs(lines):
|
|
"""Yield (start_line_number, length) for each run, skipping runnable doctests.
|
|
|
|
Every question here is asked of the SCANNER, never of the raw line: an
|
|
attribute's bracket depth is counted over code only, because `#[cfg(feature
|
|
= "[")]` otherwise exempted the whole file. See context/code/tooling.md.
|
|
"""
|
|
scanned = scan(lines)
|
|
|
|
run_start, run_len, run_kind = None, 0, None
|
|
in_fence = fence_exempt = False
|
|
attr_depth = 0
|
|
attr_inner = False
|
|
inner_pending = False
|
|
|
|
def add(n, kind):
|
|
nonlocal run_start, run_len, run_kind
|
|
if run_start is None:
|
|
run_start, run_len, run_kind = n, 0, kind
|
|
run_len += 1
|
|
|
|
for n, (has_code, has_comment, body, code, leads, in_block_at_start) in enumerate(scanned, 1):
|
|
# A line that CLOSES a block comment carries comment text even when code
|
|
# follows it; a line that opens one after code does not.
|
|
is_comment = has_comment and (not has_code or (in_block_at_start and leads))
|
|
code_s = code.strip()
|
|
|
|
if attr_depth > 0:
|
|
attr_depth += code.count("[") - code.count("]")
|
|
if is_comment:
|
|
add(n, _kind(body))
|
|
if attr_depth <= 0:
|
|
attr_depth = 0
|
|
inner_pending = inner_pending or attr_inner
|
|
continue
|
|
|
|
if not is_comment and (code_s.startswith("#[") or code_s.startswith("#![")):
|
|
# `#[doc = "..."]` and `#![doc = "..."]` ARE comments; `#[doc(hidden)]`
|
|
# is not, which an unanchored `#[doc` prefix could not tell apart.
|
|
if _DOC_ATTR.match(code_s):
|
|
add(n, "//!" if code_s.startswith("#![") else "///")
|
|
continue
|
|
inner = code_s.startswith("#![")
|
|
depth = code.count("[") - code.count("]")
|
|
if depth > 0:
|
|
attr_depth, attr_inner = depth, inner
|
|
continue
|
|
inner_pending = inner_pending or inner
|
|
continue
|
|
|
|
if is_comment:
|
|
kind = _kind(body)
|
|
# An inner attribute ends a run only where the DOC KIND changes: a
|
|
# crate doc and an item doc are two blocks, `//!` either side is one.
|
|
if inner_pending:
|
|
inner_pending = False
|
|
if run_kind is not None and kind != run_kind and run_start is not None and run_len > 0:
|
|
yield run_start, run_len
|
|
run_start, run_len, run_kind = None, 0, None
|
|
doc = kind in ("///", "//!")
|
|
text = body.lstrip("/!").strip() if body is not None else ""
|
|
if doc and text.startswith("```"):
|
|
if in_fence:
|
|
exempt_here = fence_exempt
|
|
in_fence = fence_exempt = False
|
|
else:
|
|
info = text[3:].strip().lower()
|
|
in_fence = True
|
|
fence_exempt = info in ("", "rust") or info.startswith("rust,")
|
|
exempt_here = fence_exempt
|
|
if exempt_here:
|
|
continue
|
|
add(n, kind)
|
|
continue
|
|
if doc and in_fence and fence_exempt:
|
|
continue
|
|
add(n, kind)
|
|
else:
|
|
in_fence = fence_exempt = False
|
|
inner_pending = False
|
|
if run_start is not None and run_len > 0:
|
|
yield run_start, run_len
|
|
run_start, run_len, run_kind = None, 0, None
|
|
if run_start is not None and run_len > 0:
|
|
yield run_start, run_len
|
|
|
|
|
|
def rust_files(roots):
|
|
for root in roots:
|
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
dirnames[:] = [d for d in dirnames if d != "target"]
|
|
for f in filenames:
|
|
if f.endswith(".rs"):
|
|
yield os.path.join(dirpath, f)
|
|
|
|
|
|
_TEST_ATTRS = ("test", "tokio::test", "async_std::test", "wasm_bindgen_test")
|
|
|
|
|
|
def _is_test_attribute(line):
|
|
"""True only for an attribute that actually marks a test.
|
|
|
|
Matched by exact path, never by substring: `#[cfg(test)]` contains "test"
|
|
and marks a compilation condition, not a test.
|
|
"""
|
|
body = line.strip()
|
|
if not body.startswith("#["):
|
|
return False
|
|
body = body[2:].rstrip()
|
|
body = body[:-1] if body.endswith("]") else body
|
|
return body.strip().strip(":") in _TEST_ATTRS
|
|
|
|
|
|
EVIDENCE_DIR = os.path.join("context", "test-evidence")
|
|
EVIDENCE_CITE = "test-evidence"
|
|
JS_ROOTS = [os.path.join("harness", "tests"), os.path.join("web", "tests")]
|
|
|
|
_RUST_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*$")
|
|
_QUOTED_NAME = re.compile(r'^"(.+)"$')
|
|
# Only a RUNNABLE LEAF test. `.skip`, `.todo`, `.describe` and `.only` are all
|
|
# excluded: the first three name no executed assertion, and `.only` disables
|
|
# every OTHER test in its file, so a heading satisfied by one is a heading
|
|
# satisfied by a file that stopped running (review, PR #69).
|
|
_JS_TEST = re.compile(
|
|
r"""(?:^|[^\w.])(?:test|it)\s*\(\s*(['"`])(.+?)\1"""
|
|
)
|
|
|
|
|
|
def evidence_claims(text):
|
|
"""Yield ("rust"|"js"|"prose", name, lineno) for every heading in a file.
|
|
|
|
`### ` and `#### ` alike, because the browser entries have to use `####`
|
|
and were invisible to a check that collected only `### ` (improvise-5ray).
|
|
A heading is a CLAIM when it is shaped like a test name -- a bare
|
|
identifier (Rust) or a quoted string (JS). Anything else is prose about
|
|
the section: `Helper - x`, `Note - y`, a file's own scope. That shape rule
|
|
is the whole discrimination, and rewriting a claim as prose retires it with
|
|
nothing checking the prose is honest (improvise-9za9). The mechanism is the
|
|
diff a reviewer reads, not the printed prose count, which nothing stores.
|
|
See context/code/tooling.md.
|
|
"""
|
|
for n, raw in enumerate(text.split("\n"), 1):
|
|
s = raw.strip()
|
|
if not (s.startswith("### ") or s.startswith("#### ")):
|
|
continue
|
|
body = s.lstrip("#").strip().strip("`").strip()
|
|
m = _QUOTED_NAME.match(body)
|
|
if m:
|
|
yield "js", m.group(1), n
|
|
elif _RUST_NAME.match(body):
|
|
yield "rust", body, n
|
|
else:
|
|
yield "prose", body, n
|
|
|
|
|
|
_CARGO_LISTED = re.compile(r"^(\S+): test$")
|
|
|
|
|
|
def cargo_test_names(listing, ignored_listing=""):
|
|
"""The FULL PATHS cargo says it will run, from `cargo test -- --list`.
|
|
|
|
Paths and not leaves: six leaf names collide in this workspace, so a leaf
|
|
is not an identity. See context/code/tooling.md and improvise-rm4s.
|
|
"""
|
|
def paths(text):
|
|
return {
|
|
m.group(1)
|
|
for m in (_CARGO_LISTED.match(line.strip()) for line in text.split("\n"))
|
|
if m
|
|
}
|
|
|
|
return paths(listing) - paths(ignored_listing)
|
|
|
|
|
|
def _leaf(path):
|
|
return path.split("::")[-1]
|
|
|
|
|
|
def _cargo_listing(extra):
|
|
cmd = ["cargo", "test", "--workspace", "--", "--list", *extra]
|
|
try:
|
|
run = subprocess.run(cmd, capture_output=True, text=True)
|
|
except OSError as e:
|
|
raise SystemExit(f"error: cannot run cargo ({e}).\n"
|
|
"--check-evidence needs a toolchain, or --rust-tests FILE "
|
|
"holding a saved `cargo test --workspace -- --list`.")
|
|
if run.returncode != 0:
|
|
raise SystemExit(f"error: {' '.join(cmd)} failed:\n{run.stderr}")
|
|
return run.stdout
|
|
|
|
|
|
def rust_citations(text):
|
|
"""The tests here whose doc comment cites the evidence dir.
|
|
|
|
A citation obliges only a TEST. Accepting any `fn` let a `###` naming a
|
|
fixture or a helper pass, so an evidence file could report itself satisfied
|
|
while naming nothing that runs (found on PR #39).
|
|
"""
|
|
citing = set()
|
|
annotated = False
|
|
doc = []
|
|
# A `proptest!` block accepts its bodies as tests -- but only INSIDE the
|
|
# block. A bare flag was never cleared, so the first proptest! in a file
|
|
# made every later fn, helpers included, count as a test: a silent
|
|
# success in the gate itself (found by review on PR #40). Track the
|
|
# block's braces and leave when they balance.
|
|
proptest_depth = None
|
|
for raw in text.split("\n"):
|
|
s = raw.strip()
|
|
if proptest_depth is not None:
|
|
proptest_depth += raw.count("{") - raw.count("}")
|
|
if proptest_depth <= 0:
|
|
proptest_depth = None
|
|
elif s.startswith("proptest!"):
|
|
proptest_depth = raw.count("{") - raw.count("}")
|
|
if proptest_depth <= 0:
|
|
proptest_depth = None if "{" in raw else 0
|
|
in_proptest = proptest_depth is not None
|
|
if s.startswith("//"):
|
|
doc.append(s)
|
|
continue
|
|
# ANCHOR the attribute. `"test" in s` also matched `#[cfg(test)]`,
|
|
# so every cfg-gated fixture cleared a gate written to reject
|
|
# exactly those -- the third instance of the unanchored-substring
|
|
# class in this campaign, after the bd audit's exclusion list.
|
|
if s.startswith("#[") and _is_test_attribute(s):
|
|
annotated = True
|
|
continue
|
|
if s.startswith("fn ") or s.startswith("pub fn ") or s.startswith("async fn "):
|
|
if annotated or in_proptest:
|
|
name = s.split("fn ", 1)[1].split("(")[0].split("<")[0].strip()
|
|
if any(EVIDENCE_CITE in d for d in doc):
|
|
citing.add(name)
|
|
annotated = False
|
|
doc = []
|
|
elif s and not s.startswith("#["):
|
|
annotated = False
|
|
doc = []
|
|
return citing
|
|
|
|
|
|
def js_tests(text):
|
|
"""(tests defined here, tests whose preceding comment cites the evidence dir).
|
|
|
|
The JS half exists because `defined` held only Rust `fn` names, so a
|
|
`####` naming a Playwright or node test could never have resolved even if
|
|
the heading had been collected -- the convention that kept the checker
|
|
quiet was the same one that made it blind (improvise-5ray).
|
|
"""
|
|
defined, citing = set(), set()
|
|
doc = []
|
|
for raw in text.split("\n"):
|
|
s = raw.strip()
|
|
m = _JS_TEST.search(raw)
|
|
if m:
|
|
defined.add(m.group(2))
|
|
if any(EVIDENCE_CITE in d for d in doc):
|
|
citing.add(m.group(2))
|
|
doc = []
|
|
continue
|
|
if s.startswith("//") or s.startswith("*") or s.startswith("/*"):
|
|
doc.append(s)
|
|
elif s:
|
|
doc = []
|
|
return defined, citing
|
|
|
|
|
|
def js_files(roots):
|
|
for root in roots:
|
|
for dirpath, dirnames, filenames in os.walk(root):
|
|
dirnames[:] = [d for d in dirnames if d != "node_modules"]
|
|
for f in filenames:
|
|
if f.endswith(".spec.js") or f.endswith(".test.js"):
|
|
yield os.path.join(dirpath, f)
|
|
|
|
|
|
def _duplicate_claims(claims, by_leaf, defined):
|
|
"""Two headings naming ONE test, keyed by the test they resolve to.
|
|
|
|
Resolved and not spelled, so `### f` beside `### m::tests::f` is caught
|
|
too: the heading is the key a reader looks a test up by, and two entries
|
|
under one key means the first one found is taken as current. See
|
|
context/code/tooling.md.
|
|
"""
|
|
def canonical(kind, name):
|
|
if kind == "js" or name in defined["rust"]:
|
|
return name
|
|
matches = by_leaf.get(name, set())
|
|
return next(iter(matches)) if len(matches) == 1 else name
|
|
|
|
grouped = {}
|
|
for (kind, name), where in claims.items():
|
|
grouped.setdefault((kind, canonical(kind, name)), []).extend(where)
|
|
def in_file_order(loc):
|
|
path, _, n = loc.rpartition(":")
|
|
return (path, int(n) if n.isdigit() else 0)
|
|
|
|
return {key: sorted(where, key=in_file_order)
|
|
for key, where in grouped.items() if len(where) > 1}
|
|
|
|
|
|
def evidence_verdict(claims, defined, citing):
|
|
"""(orphans, unstated, ambiguous, duplicates), keyed by ("rust"|"js", name).
|
|
|
|
`orphans` is evidence naming a test nobody can find. `unstated` is the
|
|
reverse obligation (improvise-lyyc): a test that POINTS AT the evidence
|
|
file must appear in it, or a reader following the pointer arrives nowhere.
|
|
`ambiguous` is a bare leaf that two tests answer to -- a refusal and not a
|
|
warning, because a heading satisfied by a test in another module answering
|
|
another question is a pass while the gate does nothing.
|
|
`duplicates` is the mirror image: two headings answered by one test, where
|
|
every copy passes and the reader takes whichever comes first as current
|
|
(improvise-hsoe). Also a refusal, on the improvise-rm4s precedent.
|
|
Only that narrower reverse obligation, because most tests have no evidence
|
|
and should not be forced to. What none of the four can see: an entry can
|
|
exist and still describe its subject backwards.
|
|
"""
|
|
by_leaf = {}
|
|
for path in defined["rust"]:
|
|
by_leaf.setdefault(_leaf(path), set()).add(path)
|
|
|
|
orphans, ambiguous = {}, {}
|
|
for key, where in claims.items():
|
|
kind, name = key
|
|
if kind == "js":
|
|
if name not in defined["js"]:
|
|
orphans[key] = where
|
|
continue
|
|
if name in defined["rust"]:
|
|
continue
|
|
matches = by_leaf.get(name, set())
|
|
if len(matches) == 1:
|
|
continue
|
|
(ambiguous if matches else orphans)[key] = where
|
|
|
|
# A citation is a bare `fn` name, so it is satisfied by a QUALIFIED heading
|
|
# naming that leaf -- qualifying a heading must not orphan its own test.
|
|
claimed = {(kind, _leaf(name)) for kind, name in claims}
|
|
unstated = {}
|
|
for kind, names in citing.items():
|
|
for name in names:
|
|
if (kind, name) not in claimed:
|
|
unstated.setdefault((kind, name), []).extend(sorted(names[name]))
|
|
return orphans, unstated, ambiguous, _duplicate_claims(claims, by_leaf, defined)
|
|
|
|
|
|
def check_evidence(roots, rust_listing=None, rust_ignored=None):
|
|
"""Both directions over context/test-evidence/. See evidence_verdict.
|
|
|
|
A MISSING evidence directory is not an early return: it used to be, and it
|
|
exited 0 while 170 citations pointed at nothing (review of PR #83).
|
|
"""
|
|
claims, prose = {}, 0
|
|
for dirpath, _, filenames in os.walk(EVIDENCE_DIR):
|
|
for f in sorted(filenames):
|
|
if not f.endswith(".md"):
|
|
continue
|
|
path = os.path.join(dirpath, f)
|
|
text = open(path, encoding="utf8", errors="replace").read()
|
|
for kind, name, n in evidence_claims(text):
|
|
if kind == "prose":
|
|
prose += 1
|
|
else:
|
|
claims.setdefault((kind, name), []).append(f"{path}:{n}")
|
|
|
|
if rust_listing is None:
|
|
source = "cargo"
|
|
rust_names = cargo_test_names(_cargo_listing([]), _cargo_listing(["--ignored"]))
|
|
else:
|
|
source = f"{rust_listing} + {rust_ignored}"
|
|
rust_names = cargo_test_names(open(rust_listing, encoding="utf8").read(),
|
|
open(rust_ignored, encoding="utf8").read())
|
|
|
|
defined = {"rust": rust_names, "js": set()}
|
|
citing = {"rust": {}, "js": {}}
|
|
for path in rust_files(roots):
|
|
for name in rust_citations(open(path, encoding="utf8", errors="replace").read()):
|
|
citing["rust"].setdefault(name, []).append(path)
|
|
for path in js_files(JS_ROOTS):
|
|
d, c = js_tests(open(path, encoding="utf8", errors="replace").read())
|
|
defined["js"] |= d
|
|
for name in c:
|
|
citing["js"].setdefault(name, []).append(path)
|
|
|
|
orphans, unstated, ambiguous, duplicates = evidence_verdict(claims, defined, citing)
|
|
if not orphans and not unstated and not ambiguous and not duplicates:
|
|
print(f"test-evidence: {len(claims)} entries, all name a test that runs "
|
|
f"and no test is named twice (names from {source}); "
|
|
f"{sum(len(v) for v in citing.values())} tests cite the file and all have one; "
|
|
f"{prose} prose headings claim nothing")
|
|
return 0
|
|
if orphans:
|
|
print(f"{len(orphans)} test-evidence entries name no test:\n", file=sys.stderr)
|
|
for (kind, name), where in sorted(orphans.items()):
|
|
print(f" [{kind}] {name} ({', '.join(where)})", file=sys.stderr)
|
|
if ambiguous:
|
|
by_leaf = {}
|
|
for path in defined["rust"]:
|
|
by_leaf.setdefault(_leaf(path), set()).add(path)
|
|
print(f"\n{len(ambiguous)} test-evidence entries name a leaf that is not unique; "
|
|
"write the module path:\n", file=sys.stderr)
|
|
for (kind, name), where in sorted(ambiguous.items()):
|
|
print(f" [{kind}] {name} ({', '.join(where)})", file=sys.stderr)
|
|
for path in sorted(by_leaf[name]):
|
|
print(f" -> {path}", file=sys.stderr)
|
|
if duplicates:
|
|
print(f"\n{len(duplicates)} tests are named by more than one test-evidence "
|
|
"heading; merge them or give each its own test:\n", file=sys.stderr)
|
|
for (kind, name), where in sorted(duplicates.items()):
|
|
print(f" [{kind}] {name} ({', '.join(where)})", file=sys.stderr)
|
|
if unstated:
|
|
print(f"\n{len(unstated)} tests cite {EVIDENCE_DIR} and have no entry there:\n",
|
|
file=sys.stderr)
|
|
for (kind, name), where in sorted(unstated.items()):
|
|
print(f" [{kind}] {name} ({', '.join(where)})", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
def check_cap(roots, limit):
|
|
"""The comment-run cap. Every invocation that can exit 0 runs this.
|
|
|
|
Why no flag selects it away: context/code/tooling.md §2, "A flag adds".
|
|
"""
|
|
violations = []
|
|
for path in sorted(rust_files(roots)):
|
|
lines = open(path, encoding="utf8", errors="replace").read().split("\n")
|
|
for start, length in comment_runs(lines):
|
|
if length > limit:
|
|
violations.append((path, start, length))
|
|
|
|
if not violations:
|
|
print(f"comment runs: none longer than {limit} lines")
|
|
return 0
|
|
|
|
worst = sorted(violations, key=lambda v: -v[2])
|
|
print(f"{len(violations)} comment runs longer than {limit} lines:\n", file=sys.stderr)
|
|
for path, start, length in worst:
|
|
print(f" {path}:{start} {length} lines", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
SELF_TEST_CASES = [
|
|
("a plain run of three", 3, """
|
|
// one
|
|
// two
|
|
// three
|
|
fn f() {}
|
|
"""),
|
|
("a block comment of six", 6, """
|
|
/* one
|
|
two
|
|
three
|
|
four
|
|
five
|
|
*/
|
|
fn f() {}
|
|
"""),
|
|
("a rustdoc block comment of six", 6, """
|
|
/** one
|
|
* two
|
|
* three
|
|
* four
|
|
* five
|
|
*/
|
|
fn f() {}
|
|
"""),
|
|
("an unclosed fence must not exempt the rest of the file", 6, """
|
|
/// opens a fence and never closes it
|
|
/// ```
|
|
/// let x = 1;
|
|
fn a() {}
|
|
|
|
/// one
|
|
/// two
|
|
/// three
|
|
/// four
|
|
/// five
|
|
/// six
|
|
fn b() {}
|
|
"""),
|
|
("an attribute must not split a run", 4, """
|
|
/// one
|
|
/// two
|
|
#[inline]
|
|
/// three
|
|
/// four
|
|
fn c() {}
|
|
"""),
|
|
("a non-runnable fence is prose and counts", 4, """
|
|
/// one
|
|
/// ```text
|
|
/// two
|
|
/// ```
|
|
fn d() {}
|
|
"""),
|
|
("a fence in a PLAIN // comment earns no exemption", 7, """
|
|
// ```
|
|
// one
|
|
// two
|
|
// three
|
|
// four
|
|
// five
|
|
// ```
|
|
fn f() {}
|
|
"""),
|
|
("a block comment opened after code on the same line", 5, """
|
|
fn f() {} /* one
|
|
two
|
|
three
|
|
four
|
|
five
|
|
*/
|
|
"""),
|
|
("nested block comments do not close early", 7, """
|
|
/* /* */
|
|
one
|
|
two
|
|
three
|
|
four
|
|
five
|
|
*/
|
|
fn f() {}
|
|
"""),
|
|
("a multi-line attribute does not split a run", 4, """
|
|
/// one
|
|
/// two
|
|
#[cfg(all(
|
|
unix,
|
|
target_pointer_width = "64"
|
|
))]
|
|
/// three
|
|
/// four
|
|
fn f() {}
|
|
"""),
|
|
("an attribute inside a fence does not reopen it", 5, """
|
|
/// one
|
|
/// ```
|
|
/// let x = 1;
|
|
#[inline]
|
|
/// ```
|
|
/// hidden A
|
|
/// hidden B
|
|
/// hidden C
|
|
/// hidden D
|
|
fn g() {}
|
|
"""),
|
|
("#[doc = ...] is the one attribute that is a comment", 3, """
|
|
#[doc = "one"]
|
|
#[doc = "two"]
|
|
#[doc = "three"]
|
|
fn f() {}
|
|
"""),
|
|
("#![doc = ...] is the inner spelling of the same thing", 5, """
|
|
#![doc = "one"]
|
|
#![doc = "two"]
|
|
#![doc = "three"]
|
|
#![doc = "four"]
|
|
#![doc = "five"]
|
|
fn f() {}
|
|
"""),
|
|
("an attribute holding a bracket in a string exempts nothing", 6, """
|
|
#[cfg(feature = "[")]
|
|
fn g() {}
|
|
// a
|
|
// b
|
|
// c
|
|
// d
|
|
// e
|
|
// f
|
|
fn f() {}
|
|
"""),
|
|
("an inner attribute between two //! blocks does not split them", 4, """
|
|
//! one
|
|
//! two
|
|
#![allow(clippy::needless_return)]
|
|
//! three
|
|
//! four
|
|
fn f() {}
|
|
"""),
|
|
("a block comment closing on a line with code still counts", 3, """
|
|
/* one
|
|
two
|
|
three */ fn f() {}
|
|
"""),
|
|
("a char literal holding a quote does not open a string", 3, """
|
|
pub const C: char = '"';
|
|
// one
|
|
// two
|
|
// three
|
|
fn f() {}
|
|
"""),
|
|
]
|
|
|
|
SELF_TEST_CLEAN = [
|
|
("two lines is the limit", """
|
|
// one
|
|
// two
|
|
fn f() {}
|
|
"""),
|
|
("a runnable doctest is exempt", """
|
|
/// one
|
|
/// ```
|
|
/// let x = 1;
|
|
/// assert_eq!(x, 1);
|
|
/// ```
|
|
/// two
|
|
fn f() {}
|
|
"""),
|
|
("a closed fence does not leak into a later comment", """
|
|
/// one
|
|
/// ```
|
|
/// let x = 1;
|
|
/// ```
|
|
fn a() {}
|
|
|
|
// two
|
|
// three
|
|
fn b() {}
|
|
"""),
|
|
("a block comment inside a raw string is not a comment", """
|
|
pub const S: &str = r#"x"y /* not a comment
|
|
one
|
|
two
|
|
three
|
|
four
|
|
"#;
|
|
fn a() {}
|
|
"""),
|
|
("// inside a raw string is not a comment", """
|
|
pub const S: &str = r#"x"y // not a comment
|
|
// nor this
|
|
// nor this
|
|
// nor this
|
|
"#;
|
|
fn a() {}
|
|
"""),
|
|
("a crate doc and an item doc are separated by the inner attribute", """
|
|
//! crate one
|
|
//! crate two
|
|
#![allow(clippy::needless_return)]
|
|
/// item one
|
|
/// item two
|
|
pub fn go() {}
|
|
"""),
|
|
("// inside a multi-line plain string is not a comment", """
|
|
pub const S: &str = "open \\
|
|
// one
|
|
// two
|
|
// three
|
|
close";
|
|
fn a() {}
|
|
"""),
|
|
("#[doc(hidden)] is an attribute, not a comment", """
|
|
/// one
|
|
/// two
|
|
#[doc(hidden)]
|
|
pub fn f() {}
|
|
"""),
|
|
("an attribute's brackets close, so later code still ends a run", """
|
|
/// one
|
|
/// two
|
|
#[cfg(all(
|
|
unix,
|
|
))]
|
|
fn f() {}
|
|
/// three
|
|
/// four
|
|
fn g() {}
|
|
"""),
|
|
]
|
|
|
|
|
|
# (name, listing, ignored listing, the paths that RUN).
|
|
#
|
|
# Three source shapes defeated the old scrape -- a test in a string literal, one
|
|
# behind a cfg feature, one behind cfg(target_arch = "wasm32") -- and an ignored
|
|
# test defeats a listing read without its `--ignored` twin (improvise-rm4s).
|
|
SELF_TEST_LISTING = [
|
|
("the module path is kept, because a leaf is not an identity",
|
|
"help::tests::a_real_test: test\n", "", ["help::tests::a_real_test"]),
|
|
|
|
("an ignored test is one that does not run",
|
|
"a::runs: test\na::skipped: test\n", "a::skipped: test\n", ["a::runs"]),
|
|
|
|
("cargo's prose is not a test name",
|
|
" Running unittests src/lib.rs (target/debug/deps/x-1)\n"
|
|
"2 tests, 0 benchmarks\n", "", []),
|
|
]
|
|
|
|
|
|
# (name, evidence markdown, rust source, js source, cargo listing, orphans, unstated).
|
|
#
|
|
# The evidence checker had no self-test at all, which is how both halves of
|
|
# improvise-5ray and improvise-lyyc survived: the shape that silenced the
|
|
# checker was the shape it could not see. Each case below is an injection.
|
|
SELF_TEST_EVIDENCE = [
|
|
("a #### heading naming no test is an orphan",
|
|
'#### "a test that does not exist anywhere"\n',
|
|
"", "", "", ["a test that does not exist anywhere"], []),
|
|
|
|
("a #### heading naming a JS test that exists is satisfied",
|
|
'#### "a peer that cannot be reached is named"\n',
|
|
"", 'test("a peer that cannot be reached is named", async () => {});\n',
|
|
"", [], []),
|
|
|
|
("a quoted heading can never be satisfied by a Rust fn of that name",
|
|
'#### "a_rust_name"\n',
|
|
"#[test]\nfn a_rust_name() {}\n", "", "m::a_rust_name: test\n",
|
|
["a_rust_name"], []),
|
|
|
|
("a prose #### heading claims nothing",
|
|
"#### Helper — `addressed`\n#### Note — the basis_check module\n"
|
|
"#### harness/tests/shell.spec.js — the file's own scope\n",
|
|
"", "", "", [], []),
|
|
|
|
("a ### naming a helper rather than a test is still an orphan",
|
|
"### a_helper\n", "fn a_helper() {}\n", "", "", ["a_helper"], []),
|
|
|
|
("a test that cites the evidence file and has no entry is caught",
|
|
"", "/// See `context/test-evidence/clients.md`.\n#[test]\nfn a_cited_test() {}\n",
|
|
"", "m::a_cited_test: test\n", [], ["a_cited_test"]),
|
|
|
|
("the same test with an entry is satisfied",
|
|
"### a_cited_test\n",
|
|
"/// See `context/test-evidence/clients.md`.\n#[test]\nfn a_cited_test() {}\n",
|
|
"", "m::a_cited_test: test\n", [], []),
|
|
|
|
("a NON-test fn citing the file is under no obligation",
|
|
"", "/// See `context/test-evidence/clients.md`.\nfn a_helper() {}\n", "",
|
|
"", [], []),
|
|
|
|
("a citation belonging to the fn ABOVE does not attach to the next one",
|
|
"### cited\n",
|
|
"/// See `context/test-evidence/clients.md`.\n#[test]\nfn cited() {}\n"
|
|
"\n#[test]\nfn uncited() {}\n",
|
|
"", "m::cited: test\nm::uncited: test\n", [], []),
|
|
|
|
("a heading cannot be satisfied by a skipped or todo test",
|
|
'#### "a skipped one"\n#### "a todo one"\n', "",
|
|
'test.skip("a skipped one", () => {});\ntest.todo("a todo one");\n',
|
|
"", ["a skipped one", "a todo one"], []),
|
|
|
|
("a heading cannot be satisfied by a test.only, which disables its file",
|
|
'#### "the only one left running"\n', "",
|
|
'test.only("the only one left running", () => {});\n',
|
|
"", ["the only one left running"], []),
|
|
|
|
("a heading cannot be satisfied by an empty suite's name",
|
|
'#### "a suite with nothing in it"\n', "",
|
|
'test.describe("a suite with nothing in it", () => {});\n',
|
|
"", ["a suite with nothing in it"], []),
|
|
|
|
("a test nested in a suite still satisfies its heading",
|
|
'#### "a nested test"\n', "",
|
|
'test.describe("a group", () => {\n test("a nested test", () => {});\n});\n',
|
|
"", [], []),
|
|
|
|
("a JS test that cites the evidence file and has no entry is caught",
|
|
"", "",
|
|
'// See context/test-evidence/clients.md.\ntest("a cited spec", async () => {});\n',
|
|
"", [], ["a cited spec"]),
|
|
]
|
|
|
|
|
|
# (name, evidence markdown, the kind of each heading in order).
|
|
#
|
|
# Prose is a third KIND rather than a heading the collector drops, so retiring a
|
|
# claim by rewriting it as prose moves the reported count (improvise-9za9).
|
|
SELF_TEST_HEADINGS = [
|
|
("a heading is classified three ways, prose included",
|
|
'#### Helper — `addressed`\n### a_rust_name\n#### "a js name"\n',
|
|
["prose", "rust", "js"]),
|
|
|
|
("a module-qualified heading is a Rust claim, not prose",
|
|
"### ui::project::tests::a_load_that_fails_is_not_a_rebase\n", ["rust"]),
|
|
]
|
|
|
|
|
|
# (name, evidence markdown, cargo listing, orphans, ambiguous).
|
|
#
|
|
# Six leaf names collide in this workspace and two were cited, so a heading was
|
|
# satisfiable by a test in another module answering another question -- renaming
|
|
# the one an entry describes left the checker at exit 0 (review of PR #83).
|
|
SELF_TEST_AMBIGUITY = [
|
|
("a leaf two tests answer to is refused, not silently resolved",
|
|
"### a_load_that_fails_is_not_a_rebase\n",
|
|
"ui::host::tests::a_load_that_fails_is_not_a_rebase: test\n"
|
|
"ui::project::tests::a_load_that_fails_is_not_a_rebase: test\n",
|
|
[], ["a_load_that_fails_is_not_a_rebase"]),
|
|
|
|
("qualifying it resolves it, and to the one it names",
|
|
"### ui::project::tests::a_load_that_fails_is_not_a_rebase\n",
|
|
"ui::host::tests::a_load_that_fails_is_not_a_rebase: test\n"
|
|
"ui::project::tests::a_load_that_fails_is_not_a_rebase: test\n",
|
|
[], []),
|
|
|
|
("a qualified heading naming a module that does not have it is an orphan",
|
|
"### ui::nowhere::tests::a_load_that_fails_is_not_a_rebase\n",
|
|
"ui::host::tests::a_load_that_fails_is_not_a_rebase: test\n",
|
|
["ui::nowhere::tests::a_load_that_fails_is_not_a_rebase"], []),
|
|
|
|
("a unique leaf still resolves unqualified",
|
|
"### a_lonely_test\n", "m::tests::a_lonely_test: test\n", [], []),
|
|
]
|
|
|
|
|
|
# (name, evidence markdown, cargo listing, js source, the tests named twice).
|
|
#
|
|
# A heading that names a test that RUNS passes -- and a second heading naming
|
|
# the same one passes just as well, so an entry superseded by its successor
|
|
# reads as current to whoever meets it first (improvise-hsoe, found on PR #92).
|
|
# Every case here is an injection; the clean arms stop an always-fail passing.
|
|
SELF_TEST_DUPLICATES = [
|
|
("one heading twice in one file is refused",
|
|
"### a_real_test\n### a_real_test\n", "m::tests::a_real_test: test\n", "",
|
|
["m::tests::a_real_test"]),
|
|
|
|
("the qualified and bare spellings of one test are the same heading",
|
|
"### a_real_test\n### m::tests::a_real_test\n", "m::tests::a_real_test: test\n", "",
|
|
["m::tests::a_real_test"]),
|
|
|
|
# A BARE heading, because two qualified ones resolve by the exact-match arm
|
|
# and never reach the ambiguity guard this case is named for (review, PR #96).
|
|
("a bare heading over an ambiguous leaf is not a duplicate of a qualified one",
|
|
"### shared\n### m::tests::shared\n### n::tests::shared\n",
|
|
"m::tests::shared: test\nn::tests::shared: test\n", "", []),
|
|
|
|
("a JS heading twice is refused too",
|
|
'#### "a spec"\n#### "a spec"\n', "", 'test("a spec", () => {});\n', ["a spec"]),
|
|
|
|
("two headings naming two tests are not duplicates",
|
|
"### one\n### two\n", "m::one: test\nm::two: test\n", "", []),
|
|
|
|
("prose headings repeat freely -- they claim no test",
|
|
"### Helper - x\n### Helper - x\n", "", "", []),
|
|
]
|
|
|
|
|
|
def _duplicate_self_test():
|
|
failures = []
|
|
for name, md, listing, js, want in SELF_TEST_DUPLICATES:
|
|
claims = {}
|
|
for kind, cname, n in evidence_claims(md):
|
|
if kind != "prose":
|
|
claims.setdefault((kind, cname), []).append(f"fixture:{n}")
|
|
jd, _ = js_tests(js)
|
|
defined = {"rust": cargo_test_names(listing), "js": jd}
|
|
*_, duplicates = evidence_verdict(claims, defined, {"rust": {}, "js": {}})
|
|
got = sorted(k[1] for k in duplicates)
|
|
if got != sorted(want):
|
|
failures.append(f" DUPLICATES: {name}: expected {sorted(want)}, saw {got}")
|
|
return failures
|
|
|
|
|
|
def _ambiguity_self_test():
|
|
failures = _duplicate_self_test()
|
|
for name, md, listing, want_o, want_a in SELF_TEST_AMBIGUITY:
|
|
claims = {}
|
|
for kind, cname, n in evidence_claims(md):
|
|
if kind != "prose":
|
|
claims.setdefault((kind, cname), []).append(f"fixture:{n}")
|
|
defined = {"rust": cargo_test_names(listing), "js": set()}
|
|
orphans, _, ambiguous, _dupes = evidence_verdict(
|
|
claims, defined, {"rust": {}, "js": {}})
|
|
got_o = sorted(k[1] for k in orphans)
|
|
got_a = sorted(k[1] for k in ambiguous)
|
|
if got_o != sorted(want_o):
|
|
failures.append(f" ORPHANS: {name}: expected {sorted(want_o)}, saw {got_o}")
|
|
if got_a != sorted(want_a):
|
|
failures.append(f" AMBIGUOUS: {name}: expected {sorted(want_a)}, saw {got_a}")
|
|
return failures
|
|
|
|
|
|
def _listing_self_test():
|
|
failures = []
|
|
for name, listing, ignored, want in SELF_TEST_LISTING:
|
|
got = sorted(cargo_test_names(listing, ignored))
|
|
if got != sorted(want):
|
|
failures.append(f" LISTING: {name}: expected {sorted(want)}, saw {got}")
|
|
for name, md, want in SELF_TEST_HEADINGS:
|
|
got = [kind for kind, _, _ in evidence_claims(md)]
|
|
if got != want:
|
|
failures.append(f" HEADINGS: {name}: expected {want}, saw {got}")
|
|
return failures
|
|
|
|
|
|
def _evidence_self_test():
|
|
failures = _listing_self_test() + _ambiguity_self_test()
|
|
for name, md, rs, js, listing, want_orphans, want_unstated in SELF_TEST_EVIDENCE:
|
|
claims = {}
|
|
for kind, cname, n in evidence_claims(md):
|
|
if kind != "prose":
|
|
claims.setdefault((kind, cname), []).append(f"fixture:{n}")
|
|
rc = rust_citations(rs)
|
|
jd, jc = js_tests(js)
|
|
defined = {"rust": cargo_test_names(listing), "js": jd}
|
|
citing = {"rust": {k: ["fixture.rs"] for k in rc},
|
|
"js": {k: ["fixture.js"] for k in jc}}
|
|
orphans, unstated, ambiguous, duplicates = evidence_verdict(claims, defined, citing)
|
|
if duplicates:
|
|
failures.append(f" DUPLICATES: {name}: unexpected {sorted(k[1] for k in duplicates)}")
|
|
got_o = sorted(k[1] for k in orphans)
|
|
got_u = sorted(k[1] for k in unstated)
|
|
if got_o != sorted(want_orphans):
|
|
failures.append(f" ORPHANS: {name}: expected {sorted(want_orphans)}, saw {got_o}")
|
|
if got_u != sorted(want_unstated):
|
|
failures.append(f" UNSTATED: {name}: expected {sorted(want_unstated)}, saw {got_u}")
|
|
if ambiguous:
|
|
failures.append(f" AMBIGUOUS: {name}: unexpected {sorted(k[1] for k in ambiguous)}")
|
|
return failures
|
|
|
|
|
|
SELF_TEST_DISPATCH = [
|
|
("bare", []),
|
|
("--check-evidence", ["--check-evidence",
|
|
"--rust-tests", "listing.txt",
|
|
"--rust-tests-ignored", "ignored.txt"]),
|
|
]
|
|
|
|
_DISPATCH_LISTING = "\nrunning 1 test\na::b::c: test\n\n1 test, 0 benchmarks\n"
|
|
_DISPATCH_IGNORED = "\nrunning 0 tests\n\n0 tests, 0 benchmarks\n"
|
|
_DISPATCH_CAPPED = "// one\n// two\n// three\n// four\nfn x() {}\n"
|
|
_DISPATCH_CLEAN = "// one\n// two\nfn x() {}\n"
|
|
|
|
|
|
def _dispatch_case(tmp, body, extra):
|
|
os.makedirs(os.path.join(tmp, "src"), exist_ok=True)
|
|
open(os.path.join(tmp, "src", "f.rs"), "w", encoding="utf8").write(body)
|
|
open(os.path.join(tmp, "listing.txt"), "w", encoding="utf8").write(_DISPATCH_LISTING)
|
|
open(os.path.join(tmp, "ignored.txt"), "w", encoding="utf8").write(_DISPATCH_IGNORED)
|
|
cmd = [sys.executable, os.path.abspath(__file__), "src"] + extra
|
|
return subprocess.run(cmd, cwd=tmp, capture_output=True, text=True)
|
|
|
|
|
|
def _dispatch_self_test():
|
|
"""Every invocation that can exit 0 answers the cap (improvise-ijql).
|
|
|
|
The fault is a four-line run; the clean arm stops an always-fail passing.
|
|
See context/code/tooling.md §2, "A flag adds a question".
|
|
"""
|
|
failures = []
|
|
for name, extra in SELF_TEST_DISPATCH:
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
run = _dispatch_case(tmp, _DISPATCH_CAPPED, extra)
|
|
if run.returncode == 0:
|
|
failures.append(f" DISPATCH: {name}: exited 0 over a four-line comment run")
|
|
elif not ("comment runs longer than" in run.stderr and "f.rs" in run.stderr):
|
|
failures.append(f" DISPATCH: {name}: failed, but not about the cap: "
|
|
f"{run.stderr.strip()[:200]}")
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
run = _dispatch_case(tmp, _DISPATCH_CLEAN, extra)
|
|
if run.returncode != 0:
|
|
failures.append(f" DISPATCH: {name}: refused a clean tree "
|
|
f"({run.returncode}): {run.stderr.strip()[:200]}")
|
|
return failures
|
|
|
|
|
|
def self_test():
|
|
"""Check the checker against every bypass it has already been fixed for.
|
|
|
|
Ten silent successes have been found in this file, every one failing in the
|
|
direction that looks like passing. See context/code/tooling.md.
|
|
"""
|
|
failures = []
|
|
for name, expected, src in SELF_TEST_CASES:
|
|
runs = [n for n in comment_runs(src.split("\n")) if n[1] > LIMIT]
|
|
longest = max((n[1] for n in runs), default=0)
|
|
if longest != expected:
|
|
failures.append(f" MISSED: {name}: expected a run of {expected}, saw {longest}")
|
|
for name, src in SELF_TEST_CLEAN:
|
|
runs = [n for n in comment_runs(src.split("\n")) if n[1] > LIMIT]
|
|
if runs:
|
|
failures.append(f" FALSE POSITIVE: {name}: flagged {runs}")
|
|
failures += _evidence_self_test()
|
|
failures += _dispatch_self_test()
|
|
if failures:
|
|
print("checker self-test FAILED:", file=sys.stderr)
|
|
print("\n".join(failures), file=sys.stderr)
|
|
return 1
|
|
print(f"checker self-test: {len(SELF_TEST_CASES)} violations caught, "
|
|
f"{len(SELF_TEST_CLEAN)} legitimate shapes allowed, "
|
|
f"{len(SELF_TEST_EVIDENCE)} evidence verdicts, "
|
|
f"{len(SELF_TEST_LISTING) + len(SELF_TEST_HEADINGS)} listing readings, "
|
|
f"{len(SELF_TEST_AMBIGUITY)} name resolutions, "
|
|
f"{len(SELF_TEST_DUPLICATES)} duplicate-heading verdicts, "
|
|
f"{len(SELF_TEST_DISPATCH)} invocations answer the cap")
|
|
return 0
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("roots", nargs="*",
|
|
default=["src", "crates", "tests", "examples", "harness"])
|
|
ap.add_argument("--limit", type=int, default=LIMIT)
|
|
ap.add_argument("--list-doctests", action="store_true",
|
|
help="list doc comments containing a ``` fence and exit")
|
|
ap.add_argument("--check-evidence", action="store_true",
|
|
help="every context/test-evidence heading must name a test that exists")
|
|
ap.add_argument("--rust-tests", metavar="FILE",
|
|
help="a saved `cargo test --workspace -- --list`; the default runs it. "
|
|
"Requires --rust-tests-ignored: one file alone would skip the "
|
|
"ignored-test rule entirely")
|
|
ap.add_argument("--rust-tests-ignored", metavar="FILE",
|
|
help="the matching `cargo test --workspace -- --list --ignored`")
|
|
ap.add_argument("--self-test", action="store_true",
|
|
help="check the checker against the bypasses it has been fixed for")
|
|
args = ap.parse_args()
|
|
if args.self_test:
|
|
return self_test()
|
|
default_roots = ["src", "crates", "tests", "examples", "harness"]
|
|
requested = args.roots or default_roots
|
|
# A path that is not a directory must FAIL. Filtering it away made
|
|
# `check-comment-runs.py some/file.rs` walk nothing and print success --
|
|
# a filter that over-matches fails in the direction that looks like
|
|
# passing, which is the one direction a checker must not fail in.
|
|
roots = [r for r in requested if os.path.isdir(r)]
|
|
missing = [r for r in requested if not os.path.isdir(r)]
|
|
if missing and args.roots:
|
|
for r in missing:
|
|
kind = "not a directory" if os.path.exists(r) else "does not exist"
|
|
print(f"error: {r}: {kind} (pass directories, not files)", file=sys.stderr)
|
|
return 2
|
|
if not roots:
|
|
print("error: nothing to scan", file=sys.stderr)
|
|
return 2
|
|
|
|
if args.check_evidence:
|
|
if bool(args.rust_tests) != bool(args.rust_tests_ignored):
|
|
print("error: --rust-tests and --rust-tests-ignored come together; one alone "
|
|
"silently drops the ignored-test rule", file=sys.stderr)
|
|
return 2
|
|
# Additive, not selective: this flag used to early-return, so the
|
|
# expensive form a human runs alone exited 0 over a capped file.
|
|
capped = check_cap(roots, args.limit)
|
|
return max(capped, check_evidence(roots, args.rust_tests, args.rust_tests_ignored))
|
|
|
|
if args.list_doctests:
|
|
for path in sorted(rust_files(roots)):
|
|
for n, raw in enumerate(open(path, encoding="utf8", errors="replace"), 1):
|
|
s = raw.strip()
|
|
if s.startswith(("///", "//!")) and s.lstrip("/!").strip().startswith("```"):
|
|
print(f"{path}:{n}")
|
|
return 0
|
|
|
|
return check_cap(roots, args.limit)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|