test: sort sample_path() calls into real source order in the tier guard - #510
Conversation
`sample_path_calls()` documented "in source order" but collected via `ast.walk`, which is breadth-first: a call nested inside a method near the top of a file is yielded *after* one at module level near the bottom. Measured on a four-call module, an unsorted walk reports lines 14, 11, 7, 7 -- the file read backwards. - sort by `(lineno, col_offset)`, so the docstring's promise is kept by the code rather than by luck; `col_offset` breaks ties for two calls on one line - add a regression test that builds a module whose walk order and source order differ, so the sort cannot silently regress - route the last hand-built capture path in `test_pcapng_unit.py` through `sample_path()`, and rewrite the `_tiers` module docstring that cited it as the outstanding example `audit_module()` emits one finding per call in this order, so breadth-first order made the same set of violations list differently between runs and a reader could not tell a reordering from a new finding. tests/test_tier_guard.py 25 passed / 19 subtests; tests/protocols/misc/test_pcapng_unit.py 37 passed / 139 subtests.
Review verdict: CHANGES NEEDEDThe fix is correct and its regression test genuinely fails without it — reverting only the sort gives One finding blocks, and it is the same defect class this PR exists to fix — a docstring asserting a property that measurement disproves:
Non-blocking:
Verified correct and worth recording: the sort matches independent ground truth (character offsets in raw source) across all four probed shapes; the ordering is genuinely load-bearing end-to-end via And a silent hole this closes that the PR does not claim: run from Not verified: Python 3.10/3.11 (measured on 3.14.7 only), a git-less checkout, and the fixture-dependent tier. |
…rose claims Addresses review feedback on #510. - tests/test_tier_guard.py: the synthetic module's line-7 pair was a tuple, and ast.Tuple keeps its elements in one field list, so those calls already walk left to right -- col_offset was pinned by nothing and the suite stayed green with it dropped. Replaced with a conditional expression: ast.IfExp stores test, body, orelse while source writes body if test else orelse, so the walk reaches them reversed and only col_offset puts them back. The findings assertion now matches capture names as well as line numbers, since the two line-7 calls are indistinguishable by lineno. - tests/_tiers.py: document which AST shapes need col_offset (ast.Dict, whose keys are all walked before any values, and ast.IfExp) and which do not (ast.Tuple). Re-measure the false-positive figure with its unit: 16 distinct literal values in unit-tier modules, of which the only two that are sample_path arguments are already legal. - tests/protocols/misc/test_pcapng_unit.py: correct which exception fires when a fixture is absent, and record that the relative path this replaced never resolved outside the repository root -- a silent skip on a tree that had every fixture. Both halves of the sort key now fail independently when reverted.
Review feedback addressed — pushed
|
| sort key | result |
|---|---|
(node.lineno, node.col_offset) — as shipped |
25 passed, 19 subtests, exit 0 |
node.lineno only |
1 failed — test_calls_and_findings_come_out_in_source_order |
node.col_offset only |
1 failed |
The lineno-only failure reports (7, 'http6.cap') ahead of (7, 'test.pcap'): a stable sort leaves a tie in walk order, so only col_offset restores it. The findings assertion now matches capture names as well as lines, because the two line-7 calls are indistinguishable by lineno — that is what pins the diagnostic order a reader of a failed collection actually sees.
Three corrections to figures in the review and in this PR's description
- False positives now carry a unit: a rule as broad as "any string literal ending in
.pcap" matches 16 distinct literal values in the unit-tier modules (18 matched in total, 85 occurrences, 199 across all oftests/). Of the 18, only two aresample_patharguments —in.pcap, which is committed, andtest.pcap, read inside the sanctionedtry/except— and both are already legal. So the broad rule would catch nothing the narrow one misses and invent 16. The earlier "about fifteen" was this figure read loosely. - The file count in the review is wrong:
find tests -name '*.py' | wc -lgives 133, not 176. Identical in the worktree and the reference checkout. It affects no docstring, since no count of modules is quoted, but it should not be repeated. - Fixture count is 20, not 19 — the 18
examples/generators/make_samples.pywrites, plus committeddhcp.pcapngandin.pcap.
ast.Dict is named in both docstrings as the sibling shape that also mis-walks a single line, since all of its keys are visited before any of its values. ast.IfExp was used in the test because it reverses with only two calls and fits the file's line-length norm; a reversing Dict needs four calls on one line.
Suite unchanged either side of the change: 938 passed, 5 skipped, 1988 subtests, exit 0, run as CI does. Zero TierGuardWarning in both logs, so the guard genuinely ran.
Re-review of
|
| sort key | result |
|---|---|
(node.lineno, node.col_offset) as shipped |
25 passed, 19 subtests, exit 0 |
node.lineno only |
1 failed — tests/test_tier_guard.py:344, first differing element (7, 'http6.cap') against expected (7, 'test.pcap') |
node.col_offset only |
1 failed, 24 passed — same test, 14 sorting first |
It also reproduced the original finding directly: parsing the pre-fix fixture's line 7 with ast.walk unsorted already yields test.pcap (col 11) before http6.cap (col 37) — so ast.Tuple never needed the tie-break, which is exactly why the old tuple-shaped fixture could not have caught a dropped col_offset.
It went well past the claim: 36 AST shapes probed, no counterexample
Asked to hunt for a shape the fixed key still mis-orders, it confirmed ast.Dict behaves as the docstrings say (lineno-only gives a, c, b, d; the shipped key gives a, b, c, d) and then probed 36 further single-line shapes where field-visit order could disagree with text order: Set, nested IfExp, 2- and 3-way BoolOp, Compare, BinOp, call args / kwargs / *args / **kwargs, Subscript, 3-part Slice, list/dict/generator comprehensions, f-strings including format specs and a multi-line expression, walrus, lambda body and lambda default, starred tuple, UnaryOp, assert, with, chained and multi-target assignment, AnnAssign, backslash continuation, triple-quoted multi-line string argument, semicolon-joined statements, decorator plus default on adjacent lines, and match/case guards.
All 36 came out in true source order. And the reason it generalises is worth stating: col_offset is a direct positional marker, independent of the order ast.walk happens to visit a node's fields, so sorting on position rather than on traversal is robust across shapes.
Secondary claims confirmed independently
The false-positive census re-derived with its own AST scan using the real _tiers.is_unit_tier: 18 distinct .pcap-suffixed literals in unit-tier modules across 85 occurrences, 199 occurrences across all of tests/, and exactly in.pcap and test.pcap appearing as sample_path arguments — both already legal. find tests -name '*.py' | wc -l = 133. Fixture count = 20.
One nuance it found, and it corrects something I wrote
I said the names-in-findings assertion "pins the diagnostic order a reader of a failed collection sees." That overstates its role here. audit_module (tests/_tiers.py:589-593) iterates sample_path_calls(...) with no re-sort, so it is order-identical to the calls list asserted two lines earlier — and in both ablations the AssertionError fired at that earlier calls-level assertEqual (line 344), before audit_module was called at all. So the findings-level regex is genuine additional coverage, of a different bug class (name/line desync during message construction), but it is not the mechanism that closes the originally-flagged gap. That gap is closed by the calls-level expectation now carrying names, together with the fixture using IfExp rather than Tuple. Non-blocking, but the narrative should not claim otherwise.
Also verified
The corrected comment in tests/protocols/misc/test_pcapng_unit.py is accurate: check_unit_tier_read returns None for a call on a handled_lines-recognised line before sample_path can raise GeneratedFixtureInUnitTierError, so the plain FileNotFoundError from the existence check is what fires when a capture is genuinely absent — matching that exception's own docstring. And the SAMPLE_ROOT line references at tests/protocols/test_option_coverage_runtime.py:39,80 are exact.
Description corrected
It flagged that this PR's description still carried the stale claims even though the in-code docstrings were fixed — the [6, 5, 4]/[4, 5, 6] probe, "about fifteen false positives", "all 19 fixture captures", and the tuple example given as needing the tie-break, which it measured as precisely the shape that does not. All four are now corrected above.
Not verified
Python 3.10/3.11 (measured on 3.14.7 only), a git-less checkout, and the full-suite figure — the last deliberately, under host-care constraints after an earlier OOM incident on this machine.
|
✅ GOOD TO MERGE — cross-reviewed twice, both halves of the sort key now provably pinned, 36 further AST shapes probed with no counterexample found. |
The defect
tests/_tiers.py'ssample_path_calls()documents that it returns everysample_path(...)call "in source order", but collects them withast.walk, which is breadth-first. A call written near the top of a file but nested inside a method is therefore yielded after one written at the bottom at module level.Measured independently of the fix:
audit_module()emits one finding per call in this order, and the guard's whole value is a diagnostic a reader can act on. Breadth-first order is not the order anything appears in the file, and it shifts when unrelated code moves between nesting levels — so the same set of violations lists differently from one edit to the next, and a reader comparing two runs cannot distinguish a reordering from a new finding.The fix
Sort by
(lineno, col_offset).col_offsetbreaks the tie for two calls on one line — though not for a tuple:ast.Tuplekeeps its elements in one field list, so(sample_path('a.pcap'), sample_path('b.pcap'))already walks left to right and needs no tie-break. That was the original review's finding, and it is why the regression fixture now uses a conditional expression instead:ast.IfExpstorestest, body, orelsewhile source writesbody if test else orelse, sosample_path('a.pcap') if sample_path('b.pcap') else Noneis walked b, a.ast.Dictis the other shape that mis-orders a single line, since all of itskeysare walked before anyvalues.Sorted rather than replaced with a hand-written lexical-order visitor deliberately: the sort is the whole fix in one expression and needs nothing kept in step with it, whereas a second traversal is another thing that can disagree with
ast.walkabout where calls live.Also in this change
tests/protocols/misc/test_pcapng_unit.pynow goes throughsample_path(), inside thetry/except FileNotFoundErroridiomexplain()recommends. The_tiersmodule docstring cited that line as the outstanding example of a capture read the guard cannot see, so the docstring is rewritten to match — and it now records the measurement behind not generalising the rule: a check as broad as "any string literal ending in.pcap" matched 16 distinct literal values in the unit-tier modules — 18 matched in total across 85 occurrences, and 199 occurrences across all oftests/— of which only two aresample_patharguments at all and both are already legal, so the broad rule would catch nothing this one misses, and a false positive here aborts collection for everybody.Verification
Test-only change; no library code touched.
tests/test_tier_guard.pytests/protocols/misc/test_pcapng_unit.pyBoth exit 0, run against a tree with all 20 fixture captures present — the 18
examples/generators/make_samples.pywrites, plus the committeddhcp.pcapngandin.pcap.