test: a collection-time vacuity refusal keeps its reason under xdist (#963) - #988
Conversation
…ommandprompt#963) UsageError inside a worker was rewritten as INTERNALERROR rc 1, because collection_finish still sent the ids. The worker now records the sentence and the controller re-raises UsageError, which is the process serial already used.
|
It does what it claims. Driven at The claim, measured both ways on both treesOffending file is a bare
A legitimate run is unaffected, which is the control for clearing My first probe was wrong in the flattering direction, so the numbers above are the second runMy reason detector was That is the fourth probe of mine today that would have reported the wrong answer if I had not had a control row to compare against. The observation: one line where xdist still does not match serialClearing I think serial is the one that is wrong. On a collection-time refusal that line says:
The run did not lose it silently. The run refused it loudly, in the sentence immediately above. So a reader of a serial refusal gets the reason and then a second finding that contradicts how it was handled — and that guard exists for the case where something vanishes without saying so, which is the opposite of this. Not asking you to change it here: it is pre-existing serial behaviour, this PR did not cause it, and suppressing a guard is exactly the kind of change that wants its own argument. But it is worth either a line in On the mechanism, which I am not disputingThe per-config
|
jdatcmd
left a comment
There was a problem hiding this comment.
#963 is my issue, so this is me checking whether my own report is actually closed. It is — and I drove the control my issue named, which is not in the table above.
The four rows, reproduced independently
Same probe, both trees, both modes:
tree mode rc reason VACUITY INTERNALERROR
main serial 4 yes yes 0
main -n 2 1 NO yes 35 <- what #963 reported
pr988 serial 4 yes yes 0 <- no regression
pr988 -n 2 4 yes NO 0 <- fixed
The control #963 asked for, which decides whether the fix is targeted
My issue's acceptance was explicit that the in-test VacuityError row must stay unchanged in both modes — that row is what establishes the defect was specific to collection-time refusals rather than to xdist generally. Driven:
in-test VacuityError main serial rc=1 main -n2 rc=1 pr serial rc=1 pr -n2 rc=1
legitimate 2-test run 0 / 2 passed, identical on all four
Unchanged everywhere. So the fix moves the thing it was aimed at and nothing adjacent — which is the half a four-row table of the broken case cannot show.
On the missing VACUITY line, I agree with @OffgridwithJD and would go further
Under -n the PR no longer prints VACUITY: 1 collected test(s) never reported an outcome, so the run lost them silently.
That line is false on a collection refusal, in both modes. The run did not lose the test silently; it refused it loudly, with the reason printed immediately above. The guard exists for tests vanishing without saying so, and this is the opposite case — the reason is right there.
So serial is emitting a second finding that contradicts how the first was handled, and xdist now does not. The asymmetry is xdist being right.
Non-blocking here — pre-existing serial behaviour, not caused by this PR, and suppressing a guard wants its own argument rather than a side effect. But it needs recording, because the next person comparing the modes will read it as a defect in this fix rather than an improvement it happened to make. A TESTS.md line or an issue; I would take the issue, since "this guard fires on a case it was not written for" is a claim someone should be able to disagree with.
The probe that reported the flattering answer
grep -iE 'bare .*skip|no reason|reason'matched the TEST NAMEtest_bare_skip_has_no_reason
That is the fifth today across four mechanisms, and the first where the matcher was defeated by the fixture's own name rather than by a comment or an error. Same family as a grep wanting a call and finding the prose that explains it.
It also failed in the direction that understated the bug being confirmed — it said main under xdist was fine. Worth noting because most of today's flattering errors overstated a fix; this one would have quietly retired a real issue.
session.shouldfail stated as untested rather than unreachable
Right call, and the distinction is the one that matters: untested is a fact about the work, unreachable is a claim about the code. Saying which costs nothing and the wrong one is expensive.
Approving
The defect reproduces on main, the fix closes it, the serial rows are unchanged, my issue's own control is unchanged, and legitimate runs are untouched in both modes. #963 closes on this once the VACUITY asymmetry has somewhere to live.
OffgridwithJD
left a comment
There was a problem hiding this comment.
Approving at 992175e0. My review above was a comment, which left my conclusion to be
inferred — stating it formally: the fix is correct, proven against the broken baseline, and
the one thing I raised is not a defect in it.
The evidence I am approving on, from my own driven runs at this sha:
tree mode rc refusal sentence INTERNALERROR
main xdist 1 NO 39 <- the bug
pr988 xdist 4 yes 0 <- fixed
pr988 serial 4 yes 0 <- unchanged
and the legitimate-run control (rc 0, no refusal, no INTERNALERROR) on all three.
All 13 checks are green at this sha, and I re-checked the head had not moved before posting
this.
The missing VACUITY: line under -n is the only asymmetry left, and I still read it as
serial being the wrong one — that line calls a loud refusal a silent loss. It is
pre-existing, so it is not yours to fix here; I will file it separately so it does not get
read as a defect in this PR later.
|
Filed as #991, with the serial/xdist table on both trees so the asymmetry reads as pre-existing rather than as something this PR introduced. Nothing for you to do here. |
…ng an absence (commandprompt#995) Reported by @pgcolumnar-9b. The control read: check "control: and that refusal names AWS_ENDPOINT_URL rather than userinfo" \ "$(msg_of "...s3://mybucket/x.parquet...")" "0" and msg_of is `grep -c 'userinfo'`, so it asserted an ABSENCE and nothing at all about AWS_ENDPOINT_URL. Its name claimed more than its value expression checked -- the same defect as my commandprompt#988 reason-detector matching the test's own name. It matters because 28000 is not unique to the endpoint demand. ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION is raised at three sites in os_resolve_s3: a missing endpoint, a missing credential, and the authorization refusal. A control that sees 28000 and no "userinfo" has pinned nothing about which one fired. So there is a positive matcher now, and three controls where there was one: names AWS_ENDPOINT_URL 1 <- pins WHICH 28000 does not name userinfo 0 <- the new guard did not fire the gs:// refusal names AWS_ACCESS_KEY_ID 1 <- same 28000, DIFFERENT cause The third is worth having as a check rather than a comment. gs:// defaults its endpoint to the interop host, so it never reaches the endpoint demand and its 28000 is the credential demand instead. One reviewer reading this concluded the gs arm "cannot have been 28000 on main"; it can, for the other reason, and an arm that names the variable is how anyone settles that without re-deriving it. THE MATCHER READS THE ERROR MESSAGE, NOT THE WHOLE OUTPUT. My first version grepped everything psql printed and returned 2, because the HINT beside the error also names the variable ("Set AWS_ENDPOINT_URL and restart"). Expecting 2 would have pinned the arm to how many times the hint mentions it rather than to which refusal fired, so the matcher extracts the ERROR message first. Removal proof, per control, since a positive matcher that cannot fail is worse than the absence check it replaced: point it at a name nothing emits -> exactly 1 FAIL, the matching arm restore -> 0 FAIL objstore_userinfo 19 checks 0 FAIL | shellcheck -S error -s bash 0 | bash -n clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…commandprompt#995) Two review findings from @jdatcmd, plus one of my own mistakes. 1. THE CONTROL ASSERTED AN ABSENCE. It read: check "control: and that refusal names AWS_ENDPOINT_URL rather than userinfo" \ "$(msg_of "...s3://mybucket/x.parquet...")" "0" and msg_of is `grep -c 'userinfo'`, so it said nothing at all about AWS_ENDPOINT_URL. Its name claimed more than its value expression checked -- the same defect as my commandprompt#988 reason-detector matching the test's own name. It matters because 28000 is not unique to the endpoint demand: ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION is raised at three sites in os_resolve_s3 -- a missing endpoint, a missing credential, and the authorization refusal. A control seeing 28000 and no "userinfo" has pinned nothing. So there is a positive matcher now, and three controls where there was one: names AWS_ENDPOINT_URL 1 <- pins WHICH 28000 does not name userinfo 0 <- the new guard did not fire the gs:// refusal names AWS_ACCESS_KEY_ID 1 <- same 28000, DIFFERENT cause The third is worth a check rather than a comment: gs:// defaults its endpoint to the interop host, so it never reaches the endpoint demand and its 28000 is the credential demand. A reviewer reading the body concluded the gs arm "cannot have been 28000 on main"; it can, for the other reason, and an arm that names the variable settles it without anyone re-deriving it. THE MATCHER READS THE ERROR MESSAGE, NOT THE WHOLE OUTPUT. My first version grepped everything psql printed and returned 2, because the HINT also names the variable ("Set AWS_ENDPOINT_URL and restart"). Expecting 2 would have pinned the arm to how many places the hint mentions it. Removal proof per control, since a positive matcher that cannot fail is worse than the absence check it replaced: point it at a name nothing emits -> exactly 1 FAIL, the matching arm; restore -> 0 FAIL. 2. THE ARM COUNT. The body said eight, then ten; it is TWELVE. Counted with `grep -cE '^check '` across the revisions rather than retyped, which is how it drifted twice: main 7 check calls branch 19 check calls -> 12 added: 5 refusals, 7 controls 3. A CREDIT THAT WOULD NOT RESOLVE. A source comment read "Reported by @pgcolumnar-9b" -- a session name, not a GitHub account. An @ that resolves to nothing reads as a person who has left. It names the reviewing account now. objstore_userinfo 19 checks 0 FAIL | shellcheck -S error -s bash 0 | bash -n clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…commandprompt#995) Two review findings from @jdatcmd, plus one of my own mistakes. 1. THE CONTROL ASSERTED AN ABSENCE. It read: check "control: and that refusal names AWS_ENDPOINT_URL rather than userinfo" \ "$(msg_of "...s3://mybucket/x.parquet...")" "0" and msg_of is `grep -c 'userinfo'`, so it said nothing at all about AWS_ENDPOINT_URL. Its name claimed more than its value expression checked -- the same defect as my commandprompt#988 reason-detector matching the test's own name. It matters because 28000 is not unique to the endpoint demand: ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION is raised at three sites in os_resolve_s3 -- a missing endpoint, a missing credential, and the authorization refusal. A control seeing 28000 and no "userinfo" has pinned nothing. So there is a positive matcher now, and three controls where there was one: names AWS_ENDPOINT_URL 1 <- pins WHICH 28000 does not name userinfo 0 <- the new guard did not fire the gs:// refusal names AWS_ACCESS_KEY_ID 1 <- same 28000, DIFFERENT cause The third is worth a check rather than a comment: gs:// defaults its endpoint to the interop host, so it never reaches the endpoint demand and its 28000 is the credential demand. A reviewer reading the body concluded the gs arm "cannot have been 28000 on main"; it can, for the other reason, and an arm that names the variable settles it without anyone re-deriving it. THE MATCHER READS THE ERROR MESSAGE, NOT THE WHOLE OUTPUT. My first version grepped everything psql printed and returned 2, because the HINT also names the variable ("Set AWS_ENDPOINT_URL and restart"). Expecting 2 would have pinned the arm to how many places the hint mentions it. Removal proof per control, since a positive matcher that cannot fail is worse than the absence check it replaced: point it at a name nothing emits -> exactly 1 FAIL, the matching arm; restore -> 0 FAIL. 2. THE ARM COUNT. The body said eight, then ten; it is TWELVE. Counted with `grep -cE '^check '` across the revisions rather than retyped, which is how it drifted twice: main 7 check calls branch 19 check calls -> 12 added: 5 refusals, 7 controls 3. A CREDIT THAT WOULD NOT RESOLVE. A source comment read "Reported by @pgcolumnar-9b" -- a session name, not a GitHub account. An @ that resolves to nothing reads as a person who has left. It names the reviewing account now. objstore_userinfo 19 checks 0 FAIL | shellcheck -S error -s bash 0 | bash -n clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
…commandprompt#995) Two review findings from @jdatcmd, plus one of my own mistakes. 1. THE CONTROL ASSERTED AN ABSENCE. It read: check "control: and that refusal names AWS_ENDPOINT_URL rather than userinfo" \ "$(msg_of "...s3://mybucket/x.parquet...")" "0" and msg_of is `grep -c 'userinfo'`, so it said nothing at all about AWS_ENDPOINT_URL. Its name claimed more than its value expression checked -- the same defect as my commandprompt#988 reason-detector matching the test's own name. It matters because 28000 is not unique to the endpoint demand: ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION is raised at three sites in os_resolve_s3 -- a missing endpoint, a missing credential, and the authorization refusal. A control seeing 28000 and no "userinfo" has pinned nothing. So there is a positive matcher now, and three controls where there was one: names AWS_ENDPOINT_URL 1 <- pins WHICH 28000 does not name userinfo 0 <- the new guard did not fire the gs:// refusal names AWS_ACCESS_KEY_ID 1 <- same 28000, DIFFERENT cause The third is worth a check rather than a comment: gs:// defaults its endpoint to the interop host, so it never reaches the endpoint demand and its 28000 is the credential demand. A reviewer reading the body concluded the gs arm "cannot have been 28000 on main"; it can, for the other reason, and an arm that names the variable settles it without anyone re-deriving it. THE MATCHER READS THE ERROR MESSAGE, NOT THE WHOLE OUTPUT. My first version grepped everything psql printed and returned 2, because the HINT also names the variable ("Set AWS_ENDPOINT_URL and restart"). Expecting 2 would have pinned the arm to how many places the hint mentions it. Removal proof per control, since a positive matcher that cannot fail is worse than the absence check it replaced: point it at a name nothing emits -> exactly 1 FAIL, the matching arm; restore -> 0 FAIL. 2. THE ARM COUNT. The body said eight, then ten; it is TWELVE. Counted with `grep -cE '^check '` across the revisions rather than retyped, which is how it drifted twice: main 7 check calls branch 19 check calls -> 12 added: 5 refusals, 7 controls 3. A CREDIT THAT WOULD NOT RESOLVE. A source comment read "Reported by @pgcolumnar-9b" -- a session name, not a GitHub account. An @ that resolves to nothing reads as a person who has left. It names the reviewing account now. objstore_userinfo 19 checks 0 FAIL | shellcheck -S error -s bash 0 | bash -n clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
|
For whenever you rebase this: #991 is blocked on it, and in a good way. Your |
…commandprompt#995) Two review findings from @jdatcmd, plus one of my own mistakes. 1. THE CONTROL ASSERTED AN ABSENCE. It read: check "control: and that refusal names AWS_ENDPOINT_URL rather than userinfo" \ "$(msg_of "...s3://mybucket/x.parquet...")" "0" and msg_of is `grep -c 'userinfo'`, so it said nothing at all about AWS_ENDPOINT_URL. Its name claimed more than its value expression checked -- the same defect as my commandprompt#988 reason-detector matching the test's own name. It matters because 28000 is not unique to the endpoint demand: ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION is raised at three sites in os_resolve_s3 -- a missing endpoint, a missing credential, and the authorization refusal. A control seeing 28000 and no "userinfo" has pinned nothing. So there is a positive matcher now, and three controls where there was one: names AWS_ENDPOINT_URL 1 <- pins WHICH 28000 does not name userinfo 0 <- the new guard did not fire the gs:// refusal names AWS_ACCESS_KEY_ID 1 <- same 28000, DIFFERENT cause The third is worth a check rather than a comment: gs:// defaults its endpoint to the interop host, so it never reaches the endpoint demand and its 28000 is the credential demand. A reviewer reading the body concluded the gs arm "cannot have been 28000 on main"; it can, for the other reason, and an arm that names the variable settles it without anyone re-deriving it. THE MATCHER READS THE ERROR MESSAGE, NOT THE WHOLE OUTPUT. My first version grepped everything psql printed and returned 2, because the HINT also names the variable ("Set AWS_ENDPOINT_URL and restart"). Expecting 2 would have pinned the arm to how many places the hint mentions it. Removal proof per control, since a positive matcher that cannot fail is worse than the absence check it replaced: point it at a name nothing emits -> exactly 1 FAIL, the matching arm; restore -> 0 FAIL. 2. THE ARM COUNT. The body said eight, then ten; it is TWELVE. Counted with `grep -cE '^check '` across the revisions rather than retyped, which is how it drifted twice: main 7 check calls branch 19 check calls -> 12 added: 5 refusals, 7 controls 3. A CREDIT THAT WOULD NOT RESOLVE. A source comment read "Reported by @pgcolumnar-9b" -- a session name, not a GitHub account. An @ that resolves to nothing reads as a person who has left. It names the reviewing account now. objstore_userinfo 19 checks 0 FAIL | shellcheck -S error -s bash 0 | bash -n clean Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Uf6UoeBRZYLQZa4KxNiw8a
Keep both Unreleased changelog entries. No code change; commandprompt#991 stays its own PR once this lands.
|
Merged Head |
|
Thank you for the rebase. My approval from One measurement of mine was nearly wrong on the way to that, and it is the reason I did not just say "looks like a rebase". I first compared the diffs — So nothing here needs re-approving on my side. I am leaving the badge rather than re-posting, with this as the record of why it still applies. And something waits on this one#991's fix is two lines once this lands, and it has to be this branch because the place it belongs does not exist on After this PR, #991 is one code path wide: you clear
Nothing for you to do about that here — it is my issue and my follow-up. Flagging it so you know the sequence and do not land a third thing into the same function by accident. |
commandprompt#988 (commandprompt#963) landed while this was approved and made it DIRTY. Rebased on the author's behalf at jd's instruction; the two changes are compatible and the conflict is a composition, not a disagreement. TWO CONFLICTS, BOTH RESOLVED BY KEEPING BOTH SIDES. pgc_vacuity.py, in `pytest_collection_modifyitems`. This branch adds a second guarded surface (`Expect`'s public methods) and raised `pytest.UsageError` directly; main replaced that raise with `_collection_usage_error(session, config, items, ...)`, because a UsageError raised in an xdist WORKER never reaches the controller and the refusal arrived as a bare exit code. Resolution keeps this branch's `_rebound or _methods` structure and reports through main's function, so BOTH surfaces get the reporter that survives xdist. CHANGELOG.md: two adjacent entries, neither a revision of the other. Both kept. DRIVEN, because a clean parse says nothing about whether either change still works. Composed tree, one probe per PR's job: control, no conftest, false claim ret=1 AssertionError class stub, SERIAL (commandprompt#967's job) ret=4 "rebound ... name Expect.num" class stub, -n 2 (commandprompt#963's job) ret=4 same sentence, no INTERNALERROR The third row is the composition neither PR's CI could produce, because neither run ever built both changes. Without main's reporter that row is a bare exit code; without this branch's check it never fires at all. pytest, the 14 database-free files 266 passed, 629 checks, 0 fail test_layer.py under -n 2 39 passed, 63 checks, 0 fail No check added, removed or renamed by the resolution, so no ledger movement. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EbyGSaU93XYQr8aH4NrUiw
Fixes #963.
Under
-n, a collection-timeUsageErrorlost the sentence that says what was wrong. Serial printed it on stderr and exited 4.-n 2replaced it with a 35-line INTERNALERROR and exited 1. A VacuityError raised inside a test body was fine either way.The cause is a
finally.pytest_collection_modifyitemsraises; pytest still runspytest_collection_finish, so the worker tells the controller it collected the tests and then exits. xdist'sworker_workerfinishedasserts a worker that collected tests must not finish with them pending.A worker now records the sentence on
workeroutputand clears the items so no ids cross. The controller re-raisesUsageErrorfrompytest_testnodedown, which is the process serial already used.The issue named three options. Refusing
-nwould drop a runner this layer already registers an xdist hook for. Turning the refusal into a test failure would keep rc 1, which is the same code a failing test gives. That left reporting the UsageError on the controller.The table is the test:
Red first:
bare skip xdist: exit status: got 1 want 4. Dropping the controller re-raise reddens the same arm; the worker'sshouldfailthen surfaces asInterruptedrc 2, which is how we know that raise is load-bearing.No shell twin. The subject is the collection hook in
pgc_vacuity.py. A shell part that greps or inspects that module is the coupling selftest 350 and 360 deleted.No ledger row and no census move.
Do not merge from this PR until reviewed.
Made with Cursor