diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 609953660..caf2b853b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,15 @@ jobs: steps: - uses: actions/checkout@v4 + # THE RELEASE TAGS, for native_upgrade_converge's fixture checks (#1341). Each + # fixture in test/fixtures must be byte-identical to what its tag shipped, and + # the pytest twin compares it with `git rev-parse v:`. + # actions/checkout fetches no tags, and with none the twin declares those + # checks unrunnable, which leaves this job incomplete. Depth 1 is enough: only + # the file at each tag is read. + - name: fetch the release tags, for the fixture checks + run: git fetch --depth=1 origin '+refs/tags/v*:refs/tags/v*' + - name: Add the PGDG repository and install PostgreSQL ${{ matrix.pg }} with its headers run: | set -euo pipefail diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 02c8b54b4..cd3482d54 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -727,6 +727,15 @@ jobs: steps: - uses: actions/checkout@v4 + # THE RELEASE TAGS, for native_upgrade_converge's fixture checks (#1341). Each + # fixture in test/fixtures must be byte-identical to what its tag shipped, and + # the pytest twin compares it with `git rev-parse v:`. + # actions/checkout fetches no tags, and with none the twin declares those + # checks unrunnable, which leaves this job incomplete. Depth 1 is enough: only + # the file at each tag is read. + - name: fetch the release tags, for the fixture checks + run: git fetch --depth=1 origin '+refs/tags/v*:refs/tags/v*' + - name: Add the PGDG repository and install PostgreSQL ${{ matrix.pg }} with its headers run: | set -euo pipefail diff --git a/test/pytest/TESTS.md b/test/pytest/TESTS.md index ab5ab84d8..343f3c4ea 100644 --- a/test/pytest/TESTS.md +++ b/test/pytest/TESTS.md @@ -7531,7 +7531,7 @@ Part of the pytest twin of `test/drop_cleanup.sh` (#1265, ported for #1341): its ## 143. test_native_upgrade_converge.py: every release upgrades to a fresh catalog, and the window before it -The pytest twin of `test/native_upgrade_converge.sh` (#1341), by the shell's names, except its release-tag checks, so the pair is declared INCOMPLETE. `ALTER EXTENSION pgcolumnar UPDATE` must bring a database on any released version to a catalog identical to a fresh install of the current `default_version`: every function's definition, ACL and comment, and every relation, column, type and foreign-data wrapper. The released starting points are the install scripts in `test/fixtures`. They are staged into the extension directory once for the module, under a lock that makes a second stager wait, and whatever was there is put back after. A backup already there means an earlier run died, so it holds the original and is kept. +The pytest twin of `test/native_upgrade_converge.sh` (#1341), every check by the shell's name. `ALTER EXTENSION pgcolumnar UPDATE` must bring a database on any released version to a catalog identical to a fresh install of the current `default_version`: every function's definition, ACL and comment, and every relation, column, type and foreign-data wrapper. The released starting points are the install scripts in `test/fixtures`. They are staged into the extension directory once for the module, under a lock that makes a second stager wait, and whatever was there is put back after. A backup already there means an earlier run died, so it holds the original and is kept. Between installing a new library and running the update, a database runs the new library over its previous release's catalog (#1341 item 66). Reads and writes must work there. `DROP TABLE`, `TRUNCATE` and `pgcolumnar.vacuum()` need a catalog table the old one lacks, so they must be refused with 42P01, change nothing, and carry a hint naming the update. After the update all three succeed on the same rows. @@ -7541,6 +7541,8 @@ Between installing a new library and running the update, a database runs the new | --- | --- | | `test_a_run_after_one_that_died_keeps_the_original` | with a died run's state in a temporary directory, staging and restoring leave the prefix's own file, not the fixture | | `test_two_stagings_at_once_keep_the_original` | a second staging that starts inside the first, as another `pytest -n` worker's would, waits for it under the staging lock, and the prefix's own file survives both | +| `test_the_fixture_sweep_found_fixtures` | premise: the fixtures read from `test/fixtures`, less the synthetic `1.0-alpha`, are not empty | +| `test_each_tagged_fixture_is_what_its_tag_shipped` | parametrized by those fixtures: each one's blob id equals the file's at its release tag. With no git repository or no release tags, unrunnable (`UNMET_PRECONDITION`); the CI pytest jobs fetch the tags | | `test_the_staged_fixtures_are_the_committed_ones` | the control's `default_version`; no backup was already staged; every fixture was staged, and each staged script is the committed fixture | | `test_each_release_upgrades_to_a_fresh_catalog` | parametrized by the four fixtures: a fresh install of the target has more than 100 objects to compare, the upgrade applies, the extension is at the target, and its catalog equals the fresh install's | | `test_the_upgrade_window` | at the previous catalog: rows written, deleted and read; the three operations refused with the hint and nothing changed; after the update the rows are intact and all three succeed | diff --git a/test/pytest/expected_tests.txt b/test/pytest/expected_tests.txt index 6b01c6e7c..ffea328e5 100644 --- a/test/pytest/expected_tests.txt +++ b/test/pytest/expected_tests.txt @@ -858,4 +858,6 @@ guard_tests 442 # tests, one per check, by collection over the cluster files. # cluster_tests 1018 -> 1025 for #1341, test_native_upgrade_converge.py ported by name except its # release-tag arms (1 -> 8 tests), by collection over the cluster files. -cluster_tests 1025 +# cluster_tests 1025 -> 1029 for #1341, test_native_upgrade_converge.py's release-tag arms: the sweep +# premise and one test per tagged fixture (8 -> 12 tests), by collection over the cluster files. +cluster_tests 1029 diff --git a/test/pytest/test_compare_to_bash.py b/test/pytest/test_compare_to_bash.py index 88f4f7038..d26a7219e 100644 --- a/test/pytest/test_compare_to_bash.py +++ b/test/pytest/test_compare_to_bash.py @@ -86,6 +86,7 @@ # The shape is `SHELL_REFERENCES`' in `test_harness_deps.py`, asserted in both # directions for the same reason: a one-way list rots into a permanent exemption. COMPLETE = ["advisory_lock_class", + "native_upgrade_converge", "range_pruning", "native_format", "iceberg_data_files", @@ -226,12 +227,6 @@ # about the PORT rather than a licence: the standing arm does not grade it, so the # reason is the only thing standing between a declared gap and a forgotten one. INCOMPLETE = { - # #1341 ported every arm but the release-tag ones: each tagged fixture is - # byte-identical to what its tag shipped. CI checks out without tags, so the shell - # skips them there with check_skip, and this layer has no verdict for an arm that - # runs on one checkout and not another. - "native_upgrade_converge": "every arm is ported except the release-tag sweep and " - "the per-tag fixture-blob comparisons", # #1341 item 29 ported the arms four fixes added with shell arms only. Each # pair below holds that fix's arms and nothing else of its shell suite. "inheritance": "the #1259 arms (a partitioned columnar parent is refused with " diff --git a/test/pytest/test_native_upgrade_converge.py b/test/pytest/test_native_upgrade_converge.py index d1f220b17..765cc9e00 100644 --- a/test/pytest/test_native_upgrade_converge.py +++ b/test/pytest/test_native_upgrade_converge.py @@ -40,6 +40,12 @@ # The released starting points, oldest first; the last is the previous release. VERSIONS = ["1.0-alpha", "1.0-alpha2", "1.0-alpha3", "1.0-alpha4"] +# The fixtures a release tag shipped, read from disk so a fixture added at the next +# cycle-open is checked without anyone adding it here. 1.0-alpha is synthetic: v1.0-alpha +# shipped pgcolumnar--1.0-dev.sql, so no tag holds a pgcolumnar--1.0-alpha.sql. +TAGGED = sorted(p.name[len("pgcolumnar--"):-len(".sql")] for p in FIXTURES.glob("pgcolumnar--*.sql") + if p.name != "pgcolumnar--1.0-alpha.sql") + # One line per object in the pgcolumnar schema: every function's definition, ACL and # comment; every relation's kind, ACL and comment; every column's type, NOT NULL and # comment; every non-base type; every foreign-data wrapper's handler and validator. The @@ -294,6 +300,34 @@ def reference(pgc_cluster): _admin(pgc_cluster, "DROP DATABASE IF EXISTS pgc_conv_fresh WITH (FORCE)") +def _git(*args): + """-> git's output in the tree under test, or None when it fails.""" + r = subprocess.run(["git", "-C", str(REPO), *args], capture_output=True, text=True) + return r.stdout.strip() if r.returncode == 0 else None + + +def test_the_fixture_sweep_found_fixtures(expect): + expect.num(len(TAGGED) > 0 and 1 or 0, 1, + "premise: the fixture sweep found fixtures to compare against their tags") + + +@pytest.mark.parametrize("v", TAGGED) +def test_each_tagged_fixture_is_what_its_tag_shipped(v, expect): + """Each fixture is the released starting point only if it is what its tag shipped + (#901): compared by blob id, which is a lookup and has no similarity heuristic. The + local tag is trusted; if this fails and the fixture looks right, check the tag with + `git ls-remote origin refs/tags/v`.""" + name = f"the {v} fixture is byte-identical to what v{v} shipped" + if _git("rev-parse", "--git-dir") is None: + expect.cannot_run("UNMET_PRECONDITION", "no git repository in the tree under test", name=name) + return + if not _git("tag", "-l", "v1.0-alpha*"): + expect.cannot_run("UNMET_PRECONDITION", "no release tags in this checkout", name=name) + return + expect.text(_git("hash-object", str(FIXTURES / f"pgcolumnar--{v}.sql")) or "fixture missing", + _git("rev-parse", f"v{v}:pgcolumnar--{v}.sql") or f"no such path at v{v}", name) + + def test_the_staged_fixtures_are_the_committed_ones(staging, expect): extdir, staged, died = staging target, _ = _target_and_previous()