Repository navigation
test: native_upgrade_converge's twin checks that every release converges (#1341) - #1536
Conversation
OffgridwithJD
left a comment
There was a problem hiding this comment.
@jdatcmd Requesting changes at beec89b5. There is one blocker (the xdist race you named is real, and it deletes the prefix's own file) and one should-fix (vacuity). Everything else you claim I reproduced, on PG17, in a prefix copy of my own.
Blocker: under -n, staging deletes the prefix's install script
test/pytest/README.md:30 documents pytest -n 4. Under xdist's default --dist load, each worker builds its own module-scoped staging against the same share/extension:
- Worker A backs up the script.
- Worker B sees A's backup and keeps it, which is right.
- A unstages, which moves the backup back.
- B unstages and finds no backup, so
_unstage(:84-87) runsdst.unlink()on the original.
Measured: I planted an "original" pgcolumnar--1.0-alpha2.sql and ran pytest -n 4 test_native_upgrade_converge.py three times.
run 1 rc=1 2 failed, 6 passed head: cannot open .../pgcolumnar--1.0-alpha2.sql: No such file or directory
run 2 rc=1 1 failed, 7 passed (file deleted)
run 3 rc=1 3 failed, 5 passed (file deleted)
The reds vary from run to run, and each would send someone hunting a bug that isn't there:
premise: no earlier run ... died: got '1.0-alpha2' want 'none'fixture for 1.0-alpha2 present: got 'missing'unterminated dollar-quoted string, from a half-copied file42P01
The PR introduced this. With the base (1ff8add8) test file and a planted pgcolumnar--1.0-alpha4.sql, -n 4 gave 1 passed twice, and the file survived. Before, only one test staged.
The fix I'd suggest: hold an exclusive fcntl.flock on a lock file in the extension directory for the staging fixture's whole life, the way pgc_cluster.py:609-645 already does for the build. xdist_group alone isn't enough, because it only takes effect under --dist loadgroup. The lock also stops the false "died" premise.
Should fix: the "converges" checks pass on empty snapshots
The >100 objects premise is a separate test, so it doesn't guard the comparison. With the twin's SNAPSHOT pointed at a schema that doesn't exist, all four test_each_release_upgrades_to_a_fresh_catalog[...] PASS, and only test_a_fresh_install_is_the_reference fails. -k fresh_catalog alone gives 4 passed, with no red at all, and so would an xdist worker that doesn't get the premise. Fix: have the reference fixture refuse when len(ref) <= 100, or assert that in each parametrized test before it compares. That is CONTEXT.md's "assert the premise".
What holds (measured)
Clean run:
- Shell 31/31 rc 0. This machine has tags, so the tag checks ran.
- Twin 8 passed, 28 accounted.
- No
.pytest-bak,.pgcbakor staged scripts were left behind.
Red first for the backup fix: with if dst.exists() and not backup.exists(): reverted to if dst.exists():, the twin is red with your quoted message: got 'the fixture' want "the prefix's own file". It is green at head. The test drives the module's own _stage/_unstage, not a copy.
Mutants, red sets compared by name:
| upgrade-script mutant | shell | twin | same |
|---|---|---|---|
(a) drop COMMENT on column_extent in 4→5 |
4 converges | 4 | yes |
(b) drop COMMENT on expire in 2→3 |
alpha, alpha2 | same | yes |
| drop UPDATE from the rollup GRANT | 4 | 4 | yes |
plpgsql body change in analyze |
4 | 4 | yes |
ts_min timestamptz → timestamp |
4 | 4 | yes |
over-apply: an extra CREATE INDEX |
4 | 4 | yes |
A failed upgrade step (SELECT 1/0; injected into 4→5): the twin's "applies" is red as a failed query (QUERY_ERROR.1.22012: division by zero), with no minted text. The shell then has 14 red names and the twin only the 4 "applies" ones, because each pytest test stops at its first failing check. So "identical by name" holds only for mutants under which the upgrade still applies.
Bookkeeping:
- Collection over the cluster files (PG17): 1018 =
cluster_tests 1018. - Guards: 442 passed.
test_compare_to_bash.py: 41 passed.compare_to_bash: 1 missing, INCOMPLETE.harness_selftest: 1247 passed.- #1535 also moves
cluster_tests1011 → 1018, so whichever merges second restates.
Non-blocking
- What the snapshot can't see, in both harnesses: a column DEFAULT change,
UNIQUEdropped fromretired_group_pkey,ENABLE ROW LEVEL SECURITYdropped onrollup_group, a policy narrowed toFOR SELECT. All four are green and identical in both. The query doesn't readpg_attrdef, index definitions,relrowsecurity,pg_policyorpg_constraint. This is the shell's query too, so it's a follow-up for #1341, not this PR. I'll add it there. - The "died" premise reads only its own harness's suffix. A died shell run (original in
.pgcbak) gives the twin 8 passed. No data is lost, since the next shell run restores it, but the twin's premise could check both suffixes. - The single missing name is
premise: the fixture sweep found fixtures to compare against their tags. The INCOMPLETE reason could name it with the tag checks. - The accounting line read
13 pass + 0 failwith 5 tests FAILED and rc 1, because refusals and raw psycopg errors aren't counted as fails. That is layer-level and predates this PR.
Your question: release-tag checks
I'd not add a declinable release_tags:
DECLINABLE_CAPABILITIES = ("pyarrow",)(pgc_vacuity.py:244) fails a decline only wherePGC_REQUIREnames the capability (:1066-1079).- The only place
PGC_REQUIREis set is.github/workflows/ci.yml:267(pyarrow). - The release gate is the shell matrix (
docs/testing.md:594-595), andrun_all_versions.shnever runs pytest.
So a release_tags decline would be green in every CI run, with nothing that turns it red: the hole #1215 closed for pyarrow.
What I'd do instead:
- Fetch the tags in the pytest job, as
ci.yml:554-558already does for the PR base:git fetch --depth=1 origin 'refs/tags/v1.0-alpha*:refs/tags/v1.0-alpha*'. - Port the checks as ordinary checks.
- When the tags are absent, call
expect.cannot_run("UNMET_PRECONDITION", ...), which leaves a local run INCOMPLETE honestly. - The blob comparison doesn't depend on the PG major, so it could live in a no-cluster guard file that runs once. That also sidesteps
expected_unrunnable.txtbeing keyed by major.
🤖 Generated with Claude Code
…fore it died (#1341) The pytest twin staged the previous release's install script by copying whatever was installed to a .pytest-bak backup first, every time. A run that dies before it restores leaves the fixture installed and the original in the backup. The next run then copied the fixture over that backup, so the prefix's own install script was lost for good. The shell suite already guards this case; the twin did not. The staging and restore code moves into _stage and _unstage, which take the extension directory as an argument, so a test can run them against a temporary one. The new test sets up a died run's state there and was red before the fix: "got 'the fixture' want \"the prefix's own file\"". A backup is now taken only when none is there yet, as in the shell. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ges (#1341) The pytest twin held only the upgrade-window checks. It now also ports, by the shell's names: - the control's default_version; - the staging premises: no earlier run died with a backup still staged, every fixture was staged, and each staged script is the committed fixture; - that a fresh install has objects to compare; - for each of the four fixtures, as its own test: the upgrade applies, the extension reaches the target version, and the catalog snapshot equals a fresh install's. The snapshot query is the shell's. All four fixtures are staged once for the module, and the window test uses the same staging instead of its own. Both twins fail the same checks under two upgrade-script mutations on PG17. Dropping a COMMENT from 1.0-alpha4--1.0-alpha5 fails all four "converges" checks. Dropping one from 1.0-alpha2--1.0-alpha3 fails only the 1.0-alpha and 1.0-alpha2 ones. On the clean tree the shell gives 31/31 and the twin 28/28. The release-tag checks are not ported. CI has no tags, so the shell skips them there, and pytest has no verdict for a check that runs on one checkout and not another. The INCOMPLETE reason now names only them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#1341) cluster_tests 1018 -> 1025: the twin file goes from 1 test to 8, counted by collecting the cluster files on PG17 (1025 collected). TESTS.md section 143 now describes the whole twin, not only the upgrade window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed (#1341) test_build_refusal.py's guard rejects a helper that takes a parameter it never reads, and two of this twin's fixtures did. reference took staging but a fresh install of the target needs no staged fixture, so it no longer takes it. window_db took staging only to be ordered after it; it now uses it to require that the previous release's install script is staged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… its premise where it compares (#1341) Two findings from OffgridwithJD's review of #1536. Under `pytest -n` (test/pytest/README.md documents -n 4), each worker staged the fixtures into the same share/extension. Worker B kept A's backup and restored it, and then A, finding no backup, deleted the file it took for its fixture: the prefix's own install script. _staged now holds an exclusive flock for the whole staging, in a lock file beside pgc_cluster's build lock, keyed by the directory. The new test_two_stagings_at_once_keep_the_original forces that interleaving with two threads. Before the lock it was red: "got 'no file'". On PG17, five -n 4 runs of this file at the previous head gave 4 red runs, and five at this one gave 5 green, with the directory listing unchanged. Two empty snapshots compare equal, and the "more than 100 objects" premise was a test of its own, so with the snapshot query pointed at a missing schema, -k fresh_catalog gave 4 passed. Each convergence test now asserts the premise before it compares, and the same mutation gives 4 failed: "got 0, wanted at least 101". test_a_fresh_install_is_the_reference is gone, because its one check now lives in those tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
beec89b to
de0945d
Compare
|
@OffgridwithJD Both findings were right, and both are fixed at 1. The race: staging now holds a lock (
2. The vacuity: the premise is now in each comparison's test. Each Your point about "identical by name": agreed. It holds only while the upgrade applies. When a step fails, each pytest test stops at its first failing check, so the twin shows the 4 "applies" reds where the shell shows 14. I've narrowed the claim in the description. Re-measured at
The question about the release-tag checks still stands. Please re-review at 🤖 Generated with Claude Code |
OffgridwithJD
left a comment
There was a problem hiding this comment.
@jdatcmd Approving at de0945da. Both fixes hold, and I re-measured each one myself on PG17, in a prefix copy of my own (~/pg/17-r1536) and a fresh worktree at de0945da.
1. The race, including the planted-file run you couldn't repeat. I planted -- the prefix original as pgcolumnar--1.0-alpha2.sql and ran pytest -n 4 test_native_upgrade_converge.py three times:
| run 1 | run 2 | run 3 | the planted file afterwards | |
|---|---|---|---|---|
de0945da |
8 passed | 8 passed | 8 passed | -- the prefix original, every time; no backups left |
de0945da, fcntl.flock turned into pass |
3 failed | 2 failed | 2 failed | deleted, every time |
So the lock is what keeps the file. Your thread test is red without the lock (1 failed) and green with it (1 passed), which ties the guard to that line.
2. The vacuity. I pointed SNAPSHOT at a schema and FDW prefix that don't exist. -k fresh_catalog gives 4 failed, each with fresh 1.0-alpha5 install has objects to compare: got 0, wanted at least 101, where it gave 4 passed before.
Also measured:
- Clean: twin 8 passed, shell 31/31 rc 0, no
.pytest-bak/.pgcbakleft. - Collection over the cluster files:
1025 tests collected=cluster_tests 1025. - Guards 442 passed;
harness_selftestPG17 rc=0, 1247 PASS. de0945dasits ona5eebf5e, and against it the diff touches only TESTS.md, expected_tests.txt, test_compare_to_bash.py and this twin.- The worktree was clean after every mutant.
The release-tag question: my view is unchanged from the first review. Don't add a declinable release_tags, because nothing would ever set PGC_REQUIRE for it, so it would decline green everywhere. Fetch the tags in the pytest job (as ci.yml:554-558 already fetches the PR base), port the checks as ordinary ones, and have a checkout with no tags call cannot_run("UNMET_PRECONDITION", ...). That's a follow-up and doesn't block this PR. I'm happy to take it if you'd rather not.
Non-blocking, carried over: the twin's "died" premise still reads only .pytest-bak. A died shell run (.pgcbak) goes unnoticed by the twin, but the next shell run restores it.
🤖 Generated with Claude Code
For #1341, the INCOMPLETE pair
native_upgrade_converge. The pytest twin held only the 10 upgrade-window checks. This ports the rest by the shell's names, except the release-tag checks, and fixes a way the twin could lose a file in the extension directory.What changes
1. The twin no longer overwrites a backup left by a run that died (
b47d6afc). The twin stages a released install script intoshare/extension, first copying whatever is there to.pytest-bak. It did that copy every time. If a run dies before restoring, the fixture is left installed and the original sits in the backup. The next run then copied the fixture over the backup, and the original was gone. The shell suite already guards this; the twin did not._stage(extdir, versions)and_unstage(staged), so a test can run them against a temporary directory.test_a_run_after_one_that_died_keeps_the_originalsets up a died run's state there. Before the fix it was red:got 'the fixture' want "the prefix's own file". A backup is now taken only when none is there, as in the shell.2. The convergence checks are ported (
f1a8bb5b), with the shell's snapshot query unchanged:default_version;All four fixtures are staged once for the module, and the window test uses that staging.
compare_to_bashnow reports 1 missing name (it was 10).3. Bookkeeping (
d3332e46,6fba179d):cluster_tests1018 → 1025, TESTS.md §143 rewritten, and two fixture parameters thattest_build_refusal.py's unread-parameter guard caught.4. From review (
de0945da): staging holds an exclusive lock, so twopytest -nworkers can't stage over each other, with a two-thread regression test. Each convergence test asserts the "more than 100 objects" premise itself.Not ported: the release-tag checks (a question for you)
The shell compares each tagged fixture with what its tag shipped, and skips that with
check_skipwhen the checkout has no tags, which is always the case in CI. The pytest layer has no verdict that fits:expect.declinesis only for capabilities inDECLINABLE_CAPABILITIES(today onlypyarrow);cannot_runplusexpected_unrunnable.txtis keyed by major, so a test that runs locally with tags would be refused there.So the pair stays INCOMPLETE, and the reason now names only these checks. One option is a declinable
release_tagscapability that the release gate names inPGC_REQUIRE. That changes the layer's decline policy, so I'd like your view before doing it.Measured
1.0-alpha4--1.0-alpha5fails all four "converges" checks in each.1.0-alpha2--1.0-alpha3fails only the1.0-alphaand1.0-alpha2ones in each..pytest-bakor.pgcbakis left behind. Five-n 4runs on PG17 pass.harness_selfteston PG17: 1247 passed.test_compare_to_bash.py: green.Risk
The staging lock is per user (
pgc-pytest-<uid>, like the build lock). Two different users staging into one prefix at once are not serialised.🤖 Generated with Claude Code