test/pytest: a helper must not take a tree it ignores (#933) - #942
Conversation
linuxhikerpm
left a comment
There was a problem hiding this comment.
Reviewed exact head fa2a815b5df2. 13/13 CI is green on this SHA. The patch matches #933: honouring srcdir cannot work because the fixture trees do not contain test/lib.sh, so the unread parameter was noise and dropping it is the right fix. The AST scan's positive control, fixture/hook exclusions, and f-string/nested/comprehension cases are the right shape.
Blocking: mergeable=CONFLICTING against current main. GitHub will not merge this SHA. The CHANGELOG/TESTS.md insertion point is the same one #944 uses, and #944 still has _sh(srcdir, expr), so these two also conflict with each other. Please rebase onto current main (and onto #944, or land this first and have #944 rebase) so CI runs on the resolved tree.
Non-blocking: the scan is test/pytest/*.py only and ignores *args/**kwargs. That is enough for the class #933 named. A helper that hides an unused argument in *args is a different defect.
fa2a815 to
2eeb31f
Compare
|
Rebased onto Gate on pg18a at
|
linuxhikerpm
left a comment
There was a problem hiding this comment.
Reviewed exact head 2eeb31fada04. 13/13 CI is green on this SHA. The patch still matches #933: honouring srcdir cannot work because the fixture trees do not contain test/lib.sh, so dropping the unread parameter is the right fix. The AST scan's positive control, fixture/hook exclusions, and f-string/nested/comprehension cases are unchanged and still the right shape.
Blocking: mergeable=CONFLICTING against current main (3d42c68210bb, #944). GitHub will not merge this SHA. #944 landed after this rebase and touches the same CHANGELOG / TESTS.md / test_build_refusal.py insertion points. Please rebase onto current main so CI runs on the resolved tree.
I scanned origin/main's test/pytest/*.py with this PR's unread-parameter helper: the only hit is still _sh(srcdir) at test_build_refusal.py:478. #944's new helpers do not add unread parameters, so after rebase the corpus-wide scan should still go 1 → 0. Worth re-running that arm on the rebased tree rather than assuming it.
Non-blocking: the scan is test/pytest/*.py only and ignores *args/**kwargs. That is enough for the class #933 named.
) `_sh(srcdir, expr)` read as "evaluate one lib.sh expression against a tree" -- the docstring said so, nine call sites passed a fixture tree, and the body sourced the module-global SRCDIR, the real source tree, instead. Whatever those arms measured, it was not parameterised by the tree they were handed. WHICH READING WAS INTENDED IS A MEASUREMENT, not a judgement, and it decides the fix. Every caller passes a tree built by `_tree_with_module` or `_tree_with_source`, and none of those contains `test/lib.sh`: fixture tree holds: ['Makefile', 'objstore', 'pgcolumnar.control'] honouring srcdir: rc=1, "No such file or directory" -- the source fails sourcing SRCDIR: rc=0, the function under test runs So the parameter could never have worked: honouring it would have made every one of those arms measure a failed `source` instead of the function. The arms mean the real tree, the parameter was noise, and it is gone. Behaviour is unchanged -- the expressions that DO need the fixture interpolate it themselves, which is why the issue could not be decided by reading the call sites alone. THE ISSUE SAID FOUR CALL SITES AND THERE ARE NINE. I filed it from the four that pass `tmp_path`; five more pass `t`, the tree from the fixture helpers. Counted from the source this time rather than from the paragraph I wrote. THE CLASS, NOT THE INSTANCE. A corpus-wide AST scan requires that no helper takes a parameter it never reads. Two exclusions, both real rather than hatches: * a TEST function's parameters are pytest FIXTURES -- requesting one has an effect whether or not the body reads it, and three in this corpus are legitimately unread; * a HOOK's signature is pytest's API, where arguments arrive by NAME, so declaring one you do not read is how a hook says which of them it wants. Three of the four the scan found before this change were hooks: `pytest_collection_modifyitems(config)`, `pytest_xdist_node_collection_finished(node)` and `pytest_sessionfinish(exitstatus)`. Only `_sh` was a defect, so the budget was 1 and is now 0. The false-positive arm carries five shapes it must not flag, including a parameter read only inside an f-string, only in a nested function, and only in a comprehension -- each one a place `ast.walk` has to reach for the zero to mean anything. Gate: the file itself 39 passed driver-free job 10 files, 186 passed, psycopg absent from the venv full corpus 265 passed with a cluster on pg18a harness_selftest 588 checks, 588 passed + 0 failed + 0 unrunnable, PASSED the scan over the corpus: no unread parameters Fixes commandprompt#933. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
2eeb31f to
0e27b0a
Compare
|
Rebased onto
before, _ = _sh(f'pgc_source_fingerprint "{t}"')to the in-process before = _fp_of(t)This branch predates #944 and still drove Checked rather than asserted — the site count and the caller set, against main: Worth naming as a class: a merged cleanup can be undone by the conflict resolution of a branch that predates it, and the branch's author is the least likely person to catch it, because they resolve toward the version they wrote and tested. The guard that would have caught it is already in the tree — Gate on pg18a at |
Correction to my comment above: I named the wrong guardI wrote:
That is wrong, and I tested it rather than leaving it asserted. Measured, by resolving the conflict the wrong way on purpose: What does catch it
Why it works: #948 removes those arms and their TESTS.md rows. A resolution that restores the code without the rows leaves tests the document does not name, which is the And why that is contingent rather than general. It catches this only because #948 happened to edit TESTS.md in the same change. A cleanup that removed code from a file TESTS.md does not index would leave nothing to redden. So the honest statement is: this particular wrong resolution is caught, and the class is not. The conflict itself, and the resolutionMeasured in both orders — it is the only code-level conflict among my four open PRs: With #948 landed first, this PR's side of the hunk adds back the five arms #948 deleted — not because this PR touches them, but because it branched before they were removed. Git presents that as an addition, so "take theirs" restores the debt. Take With this PR landed first, #948 removes them as intended and there is nothing to decide. So the cheaper order is #942 before #948, and I will rebase #942 onto main after whichever of the two lands first rather than leaving the resolution to the merger. |
jdatcmd
left a comment
There was a problem hiding this comment.
Approving at 0e27b0a4, 13/13 green.
The scan discriminates, measured against main rather than asserted
The strongest thing here is that the population claim can fail. I ran your _unread_parameters over both corpora:
branch 22 files, 0 unread-parameter helper(s)
MAIN 22 files, 1 -> ('test_build_refusal.py:478', '_sh', 'srcdir')
Same scan, same corpus size, and on main it names the exact defect down to the file, line and parameter. A zero that is only ever a zero proves nothing; this one moves.
The control pair is the right shape and the false-positive budget is the half that usually goes missing: five honest forms — an unread pytest fixture, a hook argument, a parameter read only inside an f-string, only in a nested function, only in a comprehension — all correctly spared. The last three are the ones a naive "is the name in the body" check gets wrong, so the budget is doing real work rather than padding.
On your retraction, which I checked rather than took
You are right and it is worth stating precisely, because the claim you withdrew is one I would also have believed. test_no_pytest_file_drives_the_shell_harness does:
expect.text(repr(sorted(found)), repr(sorted(SHELL_REFERENCES)), ...)That compares sorted file names. test_build_refusal.py is declared either way, so arms added back inside it change nothing the arm looks at — exactly as your own CONTEXT.md text says, in the words "a file-level guard".
The part I would keep above the correction itself: you found it by resolving the conflict wrongly on purpose and watching the arm pass. That is the only way this class is ever found, and it is the same move I failed to make on my scan-path control.
The merge order, which I am treating as load-bearing
Your measurement of all twelve ordered pairs is the useful artifact here, and the conclusion matters for whoever merges:
#942 before #948 nothing to decide
#948 before #942 test_build_refusal.py conflicts, and #942's side ADDS BACK
the five arms #948 deleted -- not because it touches them,
but because it branched earlier and git presents that as an
addition, so "take theirs" restores the debt
A merged cleanup undone by the conflict resolution of a branch that predates it, invisible in the earlier branch's own diff because the line looks like the line it always was. Rebasing so #942 lands first is the right call and removes the decision rather than documenting it.
And your honest limit on the guard that does catch it is the part I would not want lost: test_every_file_and_test_is_named_in_the_document catches this wrong resolution only because #948 removes the arms and their TESTS.md rows in the same change. A removal from a file TESTS.md does not index would redden nothing. The instance is guarded; the class is not.
linuxhikerpm
left a comment
There was a problem hiding this comment.
Approving exact head 0e27b0a483ca. MERGEABLE, CLEAN, 13/13 on this SHA.
Rebase onto main (3d42c682, #944) resolved the conflict. The unread-parameter scan still sits at EOF on test_build_refusal.py after #944's new helpers, and those helpers do not take unread arguments, so the corpus-wide arm going 1 → 0 is the same claim as before. _sh(expr) sources SRCDIR; the fixture is interpolated by the caller. Matches #933.
Non-blocking: #948 also edits test_build_refusal.py. Landing one will dirty the other; the remaining unread srcdir after #948 is this PR's subject.
Merging main after commandprompt#942, commandprompt#947, commandprompt#949, commandprompt#950 and commandprompt#951 landed. Two conflicts and one of them could not be resolved by reading the diff. CHANGELOG.md -- union, both entries kept. check_ledger_budget.txt -- THE NUMBER CAME FROM A RUN, not from arithmetic. This branch carried 769 off main's old 756 baseline; commandprompt#947 has since landed 762; the composed ledger takes both row sets and holds 775. None of 769, 762, or any sum of deltas is the answer, because the rows are the source and the census is a measurement of them. selftest on the composed tree, census left stale on purpose 776 checks, 775 passed + 1 failed FAIL the committed census matches the committed ledger: got [762] want [775] That arm naming 775 is the derivation; the confirmation is a second run: census set to 775, derived from the ledger 776 checks, 776 passed + 0 failed CHECKED BEFORE TRUSTING THE LOG, because a reconciling log can still be evidence about the environment rather than the code (commandprompt#946): checks in the run absent from the ledger 0 rows in the ledger absent from the run 2, both pre-existing conditional premises in part 330 that also sit in main's ledger FAIL records in the run 1, the stale census arm itself So the ledger's auto-merge was correct and the only thing wrong was the number describing it. Ceiling untouched at 250: adding rows to an already-covered suite cannot move it. Third time this artifact has needed re-deriving on a merge. That is now a property rather than an accident -- the census is a measurement of the tree, and every merge invalidates it. Tracked in commandprompt#952. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw
Fixes #933.
_sh(srcdir, expr)read as "evaluate onelib.shexpression against a tree" — the docstring said so, nine call sites passed a fixture tree, and the body sourced the module-globalSRCDIR, the real source tree, instead. Whatever those arms measured, it was not parameterised by the tree they were handed.Which reading was intended is a measurement
The issue deliberately proposed no patch, because the fix depends on whether the arms meant the fixture tree or the real one. Every caller passes a tree built by
_tree_with_moduleor_tree_with_source, and none of those containstest/lib.sh:So the parameter could never have worked: honouring it would have made every one of those arms measure a failed
sourcerather than the function under test. The arms mean the real tree, the parameter was noise that made nine call sites read as something they were not, and it is gone.Behaviour is unchanged — the expressions that do need the fixture interpolate it themselves, which is exactly why the issue could not be settled by reading the call sites.
The issue said four call sites and there are nine. I filed it from the four that pass
tmp_path; five more passt, the tree from the fixture helpers. Counted from the source this time rather than from the paragraph I wrote.The class, not the instance
A corpus-wide AST scan requires that no helper takes a parameter it never reads.
Two exclusions, both real rather than hatches. A test function's parameters are pytest fixtures — requesting one has an effect whether or not the body reads it, and three in this corpus are legitimately unread. A hook's signature is pytest's API, where arguments arrive by name, so declaring one you do not read is how a hook says which it wants. Three of the four the scan found before this change were hooks:
So the budget was 1 and is now 0.
The false-positive arm carries five shapes the scan must not flag, including a parameter read only inside an f-string, only in a nested function, and only in a comprehension — each one a place
ast.walkhas to reach for the zero to mean anything.Gate
Based on main
cfe1fde9. Touches onlytest/pytest/, so it is independent of #940 and #941, which are shell-side.🤖 Generated with Claude Code
https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a