Skip extracted test modules in the hazard scanner by how they load, not by name - #443
lamemustafa wants to merge 2 commits into
Conversation
…ot by name #440, #441 and #442 moved inline `#[cfg(test)] mod tests { ... }` blocks into sibling files loaded by `#[cfg(test)] #[path = "..."] mod tests;`. check-tally-request-builder-hazards.mjs skipped only the inline form, so it began reading those files as production code. Results did not change, but a test that builds a hazard-shaped string as an expectation would have raised a false alarm. Skipping any file named *_tests.rs was rejected: a safety scanner must not exempt production code for its file name. Instead a file is skipped only when it is actually loaded as a test module: - Edges: a brace-depth-0 out-of-line `mod name;` whose attribute group has a cfg implying test (cfg(test), or test as an argument of cfg(all(...))), with #[path] or bare in a crate root or mod.rs; plus any such declaration made by a file already test-only, iterated to a fixed point. - Vetoes, from every .rs file in the repository and matched by basename case-insensitively: any other `mod name;` at any depth (including macro bodies and inline modules), any `path = "..."` or include!-family string not part of an edge (covering cfg_attr), and every Cargo crate root. - Files whose braces underflow or do not end balanced contribute no edges. - Everything unhandled -- cfg(any(...)), #[path] inside an inline module, r#name -- leaves the file scanned, so an error is a false alarm, never a missed hazard. The design was critiqued before implementation. The critique found four ways the first draft could skip a production file -- a non-test declaration it did not recognise, a Cargo target, a declaration outside the scan roots, case variants -- and that it would still have scanned 24 of the 74 extracted files: 22 declared by other test files, one under cfg(all(test, ...)), one bare in a mod.rs. The veto set, the fixed point and those two cfg/bare shapes are the response. Checked, not asserted: on this repository the skipped set is exactly the 74 files the rustc-validated module graph from #436 marks test-only, path for path; the 10 pinned violations are unchanged. The skipped count is now pinned too, so a lexer regression that quarantines too much fails the gate. Main now runs under `import.meta.main` so the functions can be imported; a test runs the real script as a child process so a main that silently does nothing cannot pass. 15 fixture tests; 15 mutations of the scanner (trust file names, accept any cfg mentioning test, drop each veto, trust unbalanced files, accept nested declarations, drop propagation, drop the crate-root veto, case-sensitive veto, #[path] under the stem, main never running, no bare-mod detection, strings or comments not skipped, only the first attribute, no raw-string path) each fail a named test. One of those first survived because a fixture's production declaration masked the lexer; the fixture was fixed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Adversarial review of the previous commit found it could skip a production file. The current tree is unaffected, but 21 of 23 attack inputs -- all valid Rust -- made the scanner skip a file holding a hazard: modules declared through macro metavariables, include! written any way but the one form the lexer read, comments inside declarations and path attributes, a c-string or a non-BMP char literal mis-lexing the rest of a file, a comment or raw string inside cfg(all(...)), and Cargo roots in inline tables, single quotes or `build =`. The lesson is that proving "only test code loads this file" by lexing Rust cannot be made complete, and every gap is a missed hazard. So: - Pin the exact skipped set, not its count. A lexer mistake can no longer add a skip silently: the gate fails naming the file, and a person adds it only after confirming it is loaded solely under #[cfg(test)]. Pinning a count would also have let one wrong skip swap for one removed module. - Refuse, naming the file and line, when any .rs file contains a loader the lexer does not model: `mod $metavar`, `mod r#name`, a comment inside a module declaration or path attribute, a backslash path, include! without a literal argument, or include! of a non-.rs file. Matched on raw source so it errs loud. None occurs in the repository. - Fix the demonstrated lexing gaps: c"/cr#" strings, non-BMP char literals, arbitrary whitespace before a path/include string, and pruning only a crate's own target/ rather than any directory with that name. - A comment or raw string inside a cfg is not parsed and does not imply test. Cargo roots are read from inline tables, single quotes and `build =`. - `import.meta.main` falls back to a real-path comparison: the job that runs this gate does not pin Node, and an older Node would otherwise make the gate a silent no-op. Measured, not asserted: the reviewer's attack harness now reports every input as scanned or refused -- none skipped -- where the previous commit skipped 21. On the repository the pinned set is unchanged at the 74 files the rustc-validated module graph marks test-only; the 10 violations are unchanged. 22 tests. 30 mutations -- the original 15, the reviewer's surviving ones, and one per fix above -- each fail a named test; two first survived because their fixtures did not reach the gap (an escape instead of the literal character, and a path string in an attribute instead of a loose include_str!), and the fixtures were corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Closing without merging. We chose the status quo. What this tried to fix. After #440, #441 and #442 extracted test modules into their own files, the request-builder hazard scanner reads those files as production code. That is a latent false-alarm risk only. Results are unchanged today: the same 10 pinned violations and no false alarm. Why not merge. Every precise fix trades that latent, loud false alarm for a possible silent missed hazard in a safety gate, and master has no missed-hazard risk.
The lesson: proving "only test code loads this file" by lexing Rust can't be made complete, and in a safety scanner every gap in that proof is a silent miss. A future false alarm from an extracted test file will be loud, and we can handle it when it appears. Kept for reference: the design critique, both reviews' findings, and the attack harnesses (23 and 35 cases, with rustc confirmation) stay available if this is revisited. The branch is retained.
|
docs/proposed-rust-module-conventions.md item 2, continuing #440-#442, for the last large inline test block among pinned files: the `tests` module of tools/bridge-tally-compatibility/src/lib.rs -- the tool that validates the compatibility surface -- moves to lib_tests.rs beside it via #[path]. lib.rs goes from 2,853 to 1,686 lines. No production code followed the block. Checked, not asserted: - lib.rs rebuilds byte-identical to HEAD from the new parent plus the re-indented child. - rustfmt's only non-whitespace change is one trailing comma; all 169 string and char literals are identical. - Test lists are identical before and after: 24 lib tests by default, 30 with bills-native-outstandings-probe-receipt. The full crate passes under both, with no warnings. - Before resealing, real_tree_has_complete_migration_and_report_surface_coverage failed with surface_file_changed -- the expected result of changing a pinned file -- and passes after; reseal --verify exits 0 and exactly one pinned hash changed. - clippy -D warnings is clean for CI's tools-workspace step and its probe-receipt feature step. - Text readers: check-tally-live-read-boundary.mjs allowlists this lib.rs for probe identifiers; the moved tests contain none, and it passes. The request-builder hazard scanner now reads lib_tests.rs as production (see #443) and still reports exactly its 10 pinned violations. The other scanning scripts pass, and the frontend suite passes (239 node, 113 vitest, 10 Playwright) on a clean tree -- the merge-driver test refuses to run with uncommitted surface changes. Coverage, stated rather than assumed. lib_tests.rs is unpinned, following #416's rule and #440-#442: tests decide nothing about what Bridge posts or emits. For this file the loss is sharper than before, because real_tree_has_complete_migration_and_report_surface_coverage holds the only executable check of the reserve bound, `MAX_SURFACE_FILES - files.len() <= RESERVED_SURFACE_FILES`. The cap itself stays enforced in the pinned lib.rs. Editing or deleting that assertion no longer changes a pinned hash. The one #[cfg(windows)] test, normalise_surface_path_uses_forward_slashes, could not run on the macOS host that did this move; CI's windows-latest native job runs the tools workspace tests and covers it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#440,#441and#442moved inline#[cfg(test)] mod tests { … }blocks into sibling files loaded by#[cfg(test)] #[path = "…"] mod tests;.check-tally-request-builder-hazards.mjsskipped only the inline form, so it began reading those files as production code. That could raise false alarms. The fix below skips extracted test files without ever exempting a production file, because that would silently hide a real hazard.How it decides, and why a lexer mistake can't hide a hazard
1. The quarantine (
testOnlyModuleFiles) marks a file test-only when a trusted edge reaches it and no veto names it.modwith acfgthat impliestest, eithercfg(test)ortestinsidecfg(all(…)). Themodmust carry#[path], or be bare in a crate root ormod.rs. Any declaration made by an already test-only file also counts, iterated to a fixed point..rsfile in the repo and matched by basename, case-insensitively.mod name;at any depthpath = "…"orinclude*!string that isn't part of an edgebuild =mod $metavar,mod r#name, a comment inside a declaration or path attribute, a backslash path,include!without a literal argument, andinclude!of a non-.rsfile. None of these occur in the repo today.2. The pinned set (
EXPECTED_TEST_MODULE_FILES) lists the 74 skipped paths exactly, the same way the violations are pinned. This is the safety net. Lexing Rust can't see every way a file might be loaded, so no mistake may silently add a skip. Any change fails the gate, names the file, and needs a person to confirm the file is loaded only under#[cfg(test)]before adding it.How it got here
cfg(all(test, …))and bare-mod.rsshapes.include!forms, comments inside declarations,c-strings and emoji char literals mis-lexing a file, comments insidecfg, and Cargo root forms. The first implementation skipped the hazard file in 21 of its 23 cases. Its recommendation to pin the set rather than a count also closes a swap: one wrong skip replacing one removed module would leave a count unchanged.Measured
include_str!. The fixtures were corrected.import.meta.mainfalls back to comparing real paths. The job that runs this gate (tally-portable) doesn't pin Node, and an older Node would otherwise turn the gate into a silent no-op. A test runs the real script as a child process.check-ci-workflow-consistency.mjspasses. The scanner is itself pinned: resealing changed exactly its hash, and--verifyexits 0.Tradeoffs to know
EXPECTED_TEST_MODULE_FILES. The gate says so, by name.check-unbounded-reads.mjsstill treats*_tests?.rsas test code by name and is untouched. Say if you want it moved onto this rule.🤖 Generated with Claude Code