mirror of
https://github.com/jmcorgan/fips.git
synced 2026-09-14 00:45:08 +00:00
Two unrelated harness defects with the same shape: the run's verdict names something other than what actually went wrong. The nostr publish/consume suite treats a dead relay as a product failure. strfry is a third-party container and it has segfaulted mid-run, twelve milliseconds after both nodes connected; everything below that was a correct report of a dead relay, and the run failed on the peer-count wait with the only evidence of the real cause sitting in one container-log line ninety lines above the summary. Add a relay verdict that states the relay's own condition — gone, not running, restarted, or faulted per its log — and print it at the head of every diagnostics dump, which is what every failure path already goes through. A relay whose state or log cannot be read is reported as unestablished rather than as healthy. The verdict does not decide the run: a relay that faulted while the assertions still passed is noted and left passing, since the suite proved what it set out to prove. The setup-bucket refill test raced its own precondition. It delivered exactly three forged setups against a 50/s refill and asserted one had been refused, which holds only if all three finish inside one 20 ms window; on a loaded runner the bucket refilled mid-loop, the third setup was admitted, and the precondition failed on arrangement rather than on behaviour. Under the CI retry policy that reports as green with a flaky count, so nothing surfaced it. Drain against a 2/s refill, which a delivery would have to take 500 ms to outrun, and deliver until a refusal is actually observed rather than assuming three is enough, with a cap that says what a runner slow enough to reach it means. The refill half then waits out a full burst from empty. (cherry picked from commit ec0fadfd136e0f6a64551fab61d042e486861842)
261 lines
10 KiB
Python
Executable File
261 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Verify that every daemon log string a test matches on still exists in src/.
|
|
|
|
A test that greps the daemon's log for a message the daemon no longer emits
|
|
does not fail — it quietly stops observing anything, and an assertion built on
|
|
it (especially one expecting a count of zero) passes for the wrong reason.
|
|
That class has produced several findings, so it is checked mechanically here
|
|
rather than re-discovered by reading.
|
|
|
|
The check extracts the string literals that test code matches against daemon
|
|
log lines, reduces each to its longest literal run (patterns carry regex
|
|
syntax), and requires that run to appear somewhere under src/. Anything that
|
|
legitimately does not originate in src/ — runtime panic text, tracing's own
|
|
level tokens — must be named in ALLOWED with a reason, so the exceptions are
|
|
reviewable instead of invisible.
|
|
|
|
Usage: testing/check-log-strings.py [--verbose]
|
|
Exit: 0 all matched strings are live, 1 otherwise.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
SRC = REPO / "src"
|
|
TESTING = REPO / "testing"
|
|
|
|
# Strings matched against log text that do not come from src/, each with the
|
|
# reason it is legitimately absent. Anything here is exempt from the src/
|
|
# existence requirement — keep the list short and the reasons specific.
|
|
ALLOWED = {
|
|
"panicked": "emitted by the Rust runtime's panic hook, not by our code",
|
|
"PANIC": "panic-adjacent marker matched defensively alongside 'panicked'",
|
|
"ERROR": "tracing's own level token, produced by the subscriber's formatter",
|
|
" ERROR ": "tracing's own level token, produced by the subscriber's formatter",
|
|
" WARN ": "tracing's own level token, produced by the subscriber's formatter",
|
|
"caught a signal": (
|
|
"read from the strfry relay container's log, not the fips daemon's — "
|
|
"strfry's own crash handler writes it"
|
|
),
|
|
"terminate called": (
|
|
"read from the strfry relay container's log, not the fips daemon's — "
|
|
"the C++ runtime writes it on an uncaught exception"
|
|
),
|
|
"Bootstrapped 100%": (
|
|
"read from the tor-daemon container's log, not the fips daemon's — "
|
|
"Tor's own bootstrap progress line"
|
|
),
|
|
"panicked at": "the Rust runtime's panic hook writes this, not our code",
|
|
"RUST_BACKTRACE": "the runtime's backtrace hint, printed alongside a panic",
|
|
"fatal runtime error": "emitted by the Rust runtime on an abort",
|
|
}
|
|
|
|
# Shell helpers whose first argument is a pattern matched against daemon logs.
|
|
SHELL_HELPERS = ("count_log_pattern", "assert_zero_count")
|
|
|
|
# A literal run shorter than this is too weak to search for meaningfully.
|
|
MIN_ANCHOR = 8
|
|
|
|
|
|
META = set("[](){}?*+.^$")
|
|
|
|
|
|
def literal_anchors(pattern: str) -> list[str]:
|
|
"""Longest literal run of each alternation branch of a grep pattern.
|
|
|
|
Walks the pattern rather than substituting, because escaping has to be
|
|
resolved in the same pass as the split: `\\.` is a literal dot and ends
|
|
nothing, while a bare `.` is a wildcard and ends the run. Unescaping first
|
|
and splitting after would conflate the two and treat `directory.mode` as
|
|
though it were literal text.
|
|
"""
|
|
branches, current, i = [], [], 0
|
|
while i < len(pattern):
|
|
ch = pattern[i]
|
|
if ch == "\\" and i + 1 < len(pattern):
|
|
nxt = pattern[i + 1]
|
|
if nxt == "|": # BRE alternation
|
|
branches.append("".join(current))
|
|
current = []
|
|
elif nxt in "wsdbWSDB": # a character class, not a literal
|
|
current.append("\0")
|
|
else:
|
|
current.append(nxt)
|
|
i += 2
|
|
continue
|
|
if ch == "|": # ERE alternation
|
|
branches.append("".join(current))
|
|
current = []
|
|
elif ch in META:
|
|
current.append("\0")
|
|
else:
|
|
current.append(ch)
|
|
i += 1
|
|
branches.append("".join(current))
|
|
|
|
anchors = []
|
|
for branch in branches:
|
|
runs = [r.strip() for r in branch.split("\0")]
|
|
runs = [r for r in runs if r]
|
|
if runs:
|
|
anchors.append(max(runs, key=len))
|
|
return anchors
|
|
|
|
|
|
def python_candidates() -> list[tuple[Path, int, str]]:
|
|
"""`"literal" in line` tests in testing/ python."""
|
|
found = []
|
|
for path in sorted(TESTING.rglob("*.py")):
|
|
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
for m in re.finditer(r'"([^"]+)"\s+in\s+line\b', line):
|
|
found.append((path, n, m.group(1)))
|
|
return found
|
|
|
|
|
|
def shell_candidates() -> list[tuple[Path, int, str]]:
|
|
"""Literal first argument to a log-matching shell helper."""
|
|
helpers = "|".join(SHELL_HELPERS)
|
|
# Quoted literal only; a variable argument is resolved elsewhere and is
|
|
# reported as unscannable rather than silently skipped.
|
|
literal = re.compile(rf"\b(?:{helpers})\s+(\"[^\"$]+\"|'[^']+')")
|
|
variable = re.compile(rf"\b(?:{helpers})\s+[\"']?\$")
|
|
found = []
|
|
for path in sorted(TESTING.rglob("*.sh")):
|
|
for n, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
|
|
if line.lstrip().startswith("#"):
|
|
continue
|
|
for m in literal.finditer(line):
|
|
found.append((path, n, m.group(1)[1:-1]))
|
|
if variable.search(line):
|
|
found.append((path, n, None))
|
|
return found
|
|
|
|
|
|
def pattern_table_candidates() -> list[tuple[Path, int, str]]:
|
|
"""Bash associative-array keys used as log patterns.
|
|
|
|
A suite that iterates a table of patterns into a log-matching helper hides
|
|
every string behind a variable, so the helper rule above sees only
|
|
`count_log_pattern "$pat"` and reports it unscannable. The keys are the
|
|
patterns; read them where they are written.
|
|
"""
|
|
key = re.compile(r'^\s*\[\s*"([^"$]+)"\s*\]=')
|
|
found = []
|
|
for path in sorted(TESTING.rglob("*.sh")):
|
|
text = path.read_text(encoding="utf-8")
|
|
if not any(h in text for h in SHELL_HELPERS):
|
|
continue
|
|
for n, line in enumerate(text.splitlines(), 1):
|
|
if line.lstrip().startswith("#"):
|
|
continue
|
|
m = key.match(line)
|
|
if m:
|
|
found.append((path, n, m.group(1)))
|
|
return found
|
|
|
|
|
|
def grep_candidates() -> list[tuple[Path, int, str]]:
|
|
"""Literal grep patterns whose input is daemon log text.
|
|
|
|
Scoped by what the grep READS, not by what the file mentions. A suite
|
|
greps several unrelated sources — its own analyzer output, fipsctl JSON,
|
|
Tor's log, ping output — and only the daemon's log has to correspond to a
|
|
string in src/. Two shapes qualify: a grep piped directly from
|
|
`docker logs`, and a grep fed a variable that was assigned from it.
|
|
"""
|
|
grep_lit = r"\bgrep\b[^|;]*?\s(\"[^\"$]+\"|'[^'$]+')"
|
|
assign = re.compile(r"(\w+)=\"?\$\(\s*docker logs\b")
|
|
found = []
|
|
for path in sorted(TESTING.rglob("*.sh")):
|
|
text = path.read_text(encoding="utf-8")
|
|
if "docker logs" not in text:
|
|
continue
|
|
log_vars = set(assign.findall(text))
|
|
# A grep reading one of those variables, by herestring or by pipe.
|
|
var_alt = "|".join(re.escape(v) for v in log_vars) or r"\0"
|
|
reads_var = re.compile(rf"[\"']?\$\{{?(?:{var_alt})\b")
|
|
for n, line in enumerate(text.splitlines(), 1):
|
|
if line.lstrip().startswith("#"):
|
|
continue
|
|
if "docker logs" not in line and not reads_var.search(line):
|
|
continue
|
|
for m in re.finditer(grep_lit, line):
|
|
found.append((path, n, m.group(1)[1:-1]))
|
|
return found
|
|
|
|
|
|
def main() -> int:
|
|
verbose = "--verbose" in sys.argv
|
|
src_text = "\n".join(
|
|
p.read_text(encoding="utf-8", errors="replace")
|
|
for p in SRC.rglob("*.rs")
|
|
)
|
|
|
|
candidates = (
|
|
python_candidates()
|
|
+ shell_candidates()
|
|
+ pattern_table_candidates()
|
|
+ grep_candidates()
|
|
)
|
|
dead, checked, exempt, unscannable = [], 0, 0, 0
|
|
|
|
for path, lineno, raw in candidates:
|
|
rel = path.relative_to(REPO)
|
|
if raw is None:
|
|
unscannable += 1
|
|
if verbose:
|
|
print(f" skip {rel}:{lineno}: pattern comes from a variable")
|
|
continue
|
|
if raw in ALLOWED:
|
|
exempt += 1
|
|
if verbose:
|
|
print(f" allow {rel}:{lineno}: {raw!r} ({ALLOWED[raw]})")
|
|
continue
|
|
anchors = literal_anchors(raw)
|
|
# An alternation may mix daemon strings with runtime ones, so the
|
|
# allowlist applies per branch and not only to the whole pattern.
|
|
if any(a in ALLOWED for a in anchors):
|
|
exempt += 1
|
|
if verbose:
|
|
print(f" allow {rel}:{lineno}: {raw!r} (branch in ALLOWED)")
|
|
anchors = [a for a in anchors if a not in ALLOWED]
|
|
usable = [a for a in anchors if len(a) >= MIN_ANCHOR]
|
|
if not usable:
|
|
unscannable += 1
|
|
if verbose:
|
|
print(f" skip {rel}:{lineno}: {raw!r} has no literal run >= {MIN_ANCHOR}")
|
|
continue
|
|
checked += 1
|
|
# Every alternation branch must be live: one dead branch is a matcher
|
|
# that has silently narrowed.
|
|
for anchor in usable:
|
|
if anchor not in src_text:
|
|
dead.append((rel, lineno, raw, anchor))
|
|
elif verbose:
|
|
print(f" ok {rel}:{lineno}: {anchor!r}")
|
|
|
|
print(
|
|
f"log-string check: {checked} matched, {exempt} allowed, "
|
|
f"{unscannable} unscannable, {len(dead)} dead"
|
|
)
|
|
if dead:
|
|
print("\nStrings matched against daemon logs that src/ never emits:\n")
|
|
for rel, lineno, raw, anchor in dead:
|
|
print(f" {rel}:{lineno}")
|
|
print(f" pattern: {raw!r}")
|
|
print(f" missing: {anchor!r}")
|
|
print(
|
|
"\nEither correct the string to what the daemon emits, or add it to "
|
|
"ALLOWED\nin this script with the reason it does not come from src/."
|
|
)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|