diff --git a/.github/scripts/pg_upgrade_cluster b/.github/scripts/pg_upgrade_cluster new file mode 100755 index 0000000..6ee59c0 --- /dev/null +++ b/.github/scripts/pg_upgrade_cluster @@ -0,0 +1,90 @@ +#!/usr/bin/env bash +# +# pg_upgrade_cluster - Shared binary pg_upgrade mechanics for CI. Modeled on +# Postgres-Extensions/cat_tools's script of the same name/purpose -- this +# part of the flow (recreating the pgxn-tools image's "test" cluster with +# matching initdb options, then driving pg_upgrade between two majors) has +# nothing extension-specific about it, so it lives here once instead of +# being inlined in the workflow. +# +# Lives under .github/, not bin/: unlike bin/test_existing (which a developer +# can run locally against a scratch database), this is CI-mechanics-only -- +# pg_ctlcluster/pg_createcluster/pg_upgrade against the pgxn-tools image's +# "test" cluster convention isn't a locally-runnable workflow. +# +# USAGE: .github/scripts/pg_upgrade_cluster [args] +# +# recreate-old PG_VERSION +# pg-start's default "test" cluster doesn't have data checksums +# enabled, but binary pg_upgrade requires the old and new clusters to +# have MATCHING checksum/auth settings (see $INITDB_OPTS below) -- +# stop, drop, and recreate the "test" cluster for PG_VERSION with them, +# then start it and wait for readiness. +# +# upgrade OLD_PG NEW_PG +# Stop the old cluster, create the new cluster's "test" cluster (same +# $INITDB_OPTS), binary pg_upgrade OLD_PG -> NEW_PG, then start the new +# cluster and wait for readiness. On pg_upgrade failure, dumps its logs +# (PG17+ writes them to $new_datadir/pg_upgrade_output.d/; older +# versions write to CWD -- both are searched) before failing. +# +# $INITDB_OPTS (env var, required): initdb options both clusters must share so +# pg_upgrade sees consistent settings on old and new (e.g. +# "--data-checksums --auth trust"). Deliberately left unquoted at each use +# site so its (space-separated) options word-split into separate arguments. +set -euo pipefail + +: "${INITDB_OPTS:?INITDB_OPTS must be set}" + +recreate_old() { + local pg=$1 + pg_ctlcluster "$pg" test stop + pg_dropcluster "$pg" test + # -p 5432: pg_createcluster assigns the next available port, which may not + # be 5432 after pg-start has claimed and released it. Force 5432 so + # subsequent psql/createdb calls connect without -p. + pg_createcluster -p 5432 "$pg" test -- $INITDB_OPTS + pg_ctlcluster "$pg" test start + pg_isready -t 30 +} + +upgrade() { + local old_pg=$1 new_pg=$2 + pg_ctlcluster "$old_pg" test stop + pg_createcluster -p 5432 "$new_pg" test -- $INITDB_OPTS + # PG17+ writes logs to $new_datadir/pg_upgrade_output.d/; older versions + # write to CWD. Search both on failure. + mkdir -p /tmp/pg_upgrade_logs + chown postgres:postgres /tmp/pg_upgrade_logs + su -c "cd /tmp/pg_upgrade_logs && /usr/lib/postgresql/$new_pg/bin/pg_upgrade \ + -b /usr/lib/postgresql/$old_pg/bin \ + -B /usr/lib/postgresql/$new_pg/bin \ + -d /var/lib/postgresql/$old_pg/test \ + -D /var/lib/postgresql/$new_pg/test \ + -o '-c config_file=/etc/postgresql/$old_pg/test/postgresql.conf' \ + -O '-c config_file=/etc/postgresql/$new_pg/test/postgresql.conf'" postgres \ + || { find /tmp/pg_upgrade_logs \ + "/var/lib/postgresql/$new_pg/test/pg_upgrade_output.d" \ + -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; } + pg_ctlcluster "$new_pg" test start + pg_isready -t 30 +} + +usage() { + echo "usage: .github/scripts/pg_upgrade_cluster [args]" >&2 + echo " recreate-old PG_VERSION" >&2 + echo " upgrade OLD_PG NEW_PG" >&2 + exit 2 +} + +main() { + local cmd=${1:-} + shift || true + case "$cmd" in + recreate-old) recreate_old "$@" ;; + upgrade) upgrade "$@" ;; + *) usage ;; + esac +} + +main "$@" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4aaec9..7c68aed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,135 @@ +# Test strategy +# +# A test_factory install can be arrived at two ways, each of which can break +# differently, so each gets its own job below: +# +# test -- FRESH install: CREATE EXTENSION at the current +# version, on every supported PostgreSQL major. The +# baseline a brand-new user gets. +# pg-upgrade-test -- BINARY pg_upgrade: install the current version on an +# OLD major, binary-upgrade the cluster to a NEWER +# major, then run the suite against the migrated +# objects in "existing" mode (test/install/load.sql). +# Proves objects created on an old server still work +# read back on a new one -- fresh-install testing +# never exercises this at all. +# +# test_factory has shipped only one version (0.5.0), so there is no +# in-place `ALTER EXTENSION UPDATE` path to test yet (no extension-update-test +# job) and the pg_upgrade job needs no bridge step (it always installs the +# CURRENT version on the old cluster -- there's no older, pg_upgrade-unsafe +# version in the wild to carry forward). Both jobs derive their PostgreSQL +# major list from the single source of truth computed in the `changes` job +# below, so adding/dropping a supported major is a one-line edit there. name: CI -on: [push, pull_request] +on: + # Post-merge CI only; pull_request already covers every PR commit. Without + # this, a commit on a branch with an open PR fires the workflow TWICE for + # the identical SHA (once for push, once for pull_request/synchronize) -- + # double the compute, and a flake on one duplicate run shows a confusing + # red check right next to an identical green one for the same commit. + push: + branches: + - master + pull_request: +env: + PGUSER: postgres jobs: + # Cheap gate that lets the heavy jobs below skip themselves on commits that + # touch only docs, without a workflow-level `paths-ignore` (which would skip + # the *whole* workflow, including all-checks-passed, on a docs-only push -- + # leaving a required check stuck Pending forever). Also derives, from a + # single set of constants, the supported-PostgreSQL-major list every heavy + # job below consumes, so adding a major is a one-line edit here instead of + # N separate matrix edits. + changes: + name: 🔍 Detect changes & derive PG matrix + runs-on: ubuntu-latest + outputs: + docs_only: ${{ steps.diff.outputs.docs_only }} + supported_pg: ${{ steps.pg.outputs.supported_pg }} + steps: + - name: Check out the repo + uses: actions/checkout@v4 + with: + # Full history needed so BASE and HEAD below are both reachable + # for `git diff`. + fetch-depth: 0 + - name: Compute per-push changed files + id: diff + run: | + # Fail-safe is the literal first line: any early exit or error + # further down (a bad BASE/HEAD, a failed git diff) leaves this in + # place, so a broken check never silently skips real testing. + echo "docs_only=false" >> "$GITHUB_OUTPUT" + + if [ "${{ github.event_name }}" = "pull_request" ] && \ + [ "${{ github.event.action }}" = "synchronize" ] && \ + [ -n "${{ github.event.before }}" ]; then + # A push to an already-open PR: before/after give the true + # per-push diff, same as for a branch push. + BASE="${{ github.event.before }}" + HEAD="${{ github.event.after }}" + elif [ "${{ github.event_name }}" = "pull_request" ]; then + # First run for this PR (opened/reopened/etc, or synchronize + # without a usable before): fall back to the whole base...head + # diff. + BASE="${{ github.event.pull_request.base.sha }}" + HEAD="${{ github.event.pull_request.head.sha }}" + else + BASE="${{ github.event.before }}" + HEAD="${{ github.event.after }}" + fi + + echo "base=$BASE" + echo "head=$HEAD" + + # A missing HEAD, or an all-zeros BASE (e.g. a new branch's first + # push, where GitHub reports no prior commit), means we can't + # compute a real diff -- the fail-safe default above stands. + if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then + exit 0 + fi + + CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__) + echo "changed files:" + echo "$CHANGED" + + if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then + exit 0 + fi + + DOCS_ONLY=true + while IFS= read -r f; do + if ! [[ "$f" =~ \.(md|asc)$ ]]; then + DOCS_ONLY=false + break + fi + done <<< "$CHANGED" + + echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT" + - name: Derive the supported-PostgreSQL-major list + id: pg + run: | + # Single source of truth for the supported PostgreSQL majors: the + # `test` and `pg-upgrade-test` matrices below both derive their + # version lists from here, so they cannot silently drift onto + # different lists. To add or drop a major, edit only NEWEST/FLOOR. + NEWEST=17 + FLOOR=10 + supported=$(seq "$NEWEST" -1 "$FLOOR") + # Emit a JSON array from a list of ints, for matrix: to consume + # with fromJSON. + json() { printf '%s\n' "$@" | paste -sd, - | sed 's/^/[/; s/$/]/'; } + echo "supported_pg=$(json $supported)" >> "$GITHUB_OUTPUT" + test: + needs: [changes] + if: needs.changes.outputs.docs_only != 'true' strategy: matrix: - pg: [17, 16, 15, 14, 13, 12, 11, 10] + # Current-supported majors, from the single source in the changes job. + pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} name: 🐘 PostgreSQL ${{ matrix.pg }} runs-on: ubuntu-latest container: pgxn/pgxn-tools @@ -14,4 +139,150 @@ jobs: - name: Check out the repo uses: actions/checkout@v4 - name: Test on PostgreSQL ${{ matrix.pg }} - run: pg-build-test + # verify-results (not a bare `make test`, and not pg-build-test's own + # `make installcheck || status=$?`) is the real gate: pgxntool marks + # installcheck .IGNORE so make itself always exits 0 there, silently + # passing even when regression.diffs is nonempty. verify-results + # depends on `test` (so it builds+installs+runs the suite) and then + # actually inspects the TAP output/regression.diffs for failures. + # + # `make install` first, as its own separate invocation, is required, + # not redundant with verify-results' own `install` prerequisite: this + # container's ambient MAKEFLAGS enables parallel make, and `install` + # and `installcheck` are independent prerequisites of the same `test` + # target, so under `-j` pg_regress can start (and fail with "extension + # ... is not available") before `install`'s file copy finishes. Two + # separate `make` invocations can't race with each other. + run: | + make install + make verify-results + + # Proves test_factory survives a BINARY pg_upgrade (in-place catalog + # migration to a newer PostgreSQL major), not just a fresh CREATE EXTENSION. + # Each leg: install the CURRENT version on an old cluster, binary-pg_upgrade + # to a newer major, then run the suite against the REAL migrated objects in + # "existing" mode (test/install/load.sql asserts the version, plants, and + # proves the dependency guard -- see bin/test_existing and + # test/install/load.sql). No bridge step: unlike an extension with multiple + # shipped versions, test_factory has only ever shipped 0.5.0, so there is no + # older, pg_upgrade-unsafe install to carry forward -- every leg installs + # the current version directly on the old cluster. + # + # Two legs, not an every-major stepwise climb (deliberately out of scope -- + # see the PR description): the oldest-to-newest jump (widest catalog + # distance) and the newest-boundary jump (most likely to hit a *new* + # PostgreSQL major's catalog change first). test_factory has no views or + # functions touching catalog internals (no SELECT * over a system catalog), + # so the per-major-boundary risk a full stepwise climb protects against is + # low here. + pg-upgrade-test: + needs: [changes] + if: needs.changes.outputs.docs_only != 'true' + strategy: + matrix: + include: + - old_pg: "10" + new_pg: "17" + - old_pg: "16" + new_pg: "17" + name: 🔄 Binary pg_upgrade ${{ matrix.old_pg }} → ${{ matrix.new_pg }} + runs-on: ubuntu-latest + container: pgxn/pgxn-tools + env: + # Both clusters must use the same initdb options so pg_upgrade sees + # consistent settings (checksums, auth) on old and new clusters. + INITDB_OPTS: --data-checksums --auth trust + steps: + - name: Start PostgreSQL ${{ matrix.old_pg }} + run: pg-start ${{ matrix.old_pg }} + - name: Check out the repo + uses: actions/checkout@v4 + - name: Recreate old cluster with data checksums enabled + run: .github/scripts/pg_upgrade_cluster recreate-old ${{ matrix.old_pg }} + - name: Install pgtap into the old cluster + # test_factory_pgtap requires pgtap (see prepare-old below). Unlike + # the fresh-install `test` job, which gets this for free because + # pg-start's default cluster already has it, this job recreates the + # cluster from scratch (previous step) with none of the fresh-install + # job's setup -- pgtap must be installed explicitly here too, for + # every PostgreSQL major this job touches, old and new alike. + # --pg_config is explicit (not relying on pg-start having already + # pointed the bare `pg_config` on PATH at this major) so this step's + # correctness doesn't depend on that PATH-switching behavior. + run: pgxn install pgtap --sudo --pg_config /usr/lib/postgresql/${{ matrix.old_pg }}/bin/pg_config + - name: Install test_factory into old cluster + run: make install + - name: Prepare the old cluster (install current version + tap schema) + # test_factory_pgtap needs pgtap installed into a dedicated "tap" + # schema BEFORE it installs (see bin/test_existing's prepare-old + # comment) -- a bare CREATE EXTENSION test_factory_pgtap CASCADE with + # no schema prep leaves pgtap wherever the ambient search_path + # resolves, and the suite's SET search_path = tap then can't find + # pgtap's functions. + run: bin/test_existing prepare-old test_factory_upgrade + - name: Install PostgreSQL ${{ matrix.new_pg }} + run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }} + - name: Install pgtap into the new cluster + # pg_upgrade validates that every extension installed in a database + # being upgraded is also available (control file + library) on the + # NEW cluster's PostgreSQL install -- without this, the upgrade + # itself fails, not just the post-upgrade suite run. --pg_config is + # required here (unlike the previous pgtap install step): this step + # runs before pg-start ever points at the new major, so the plain + # `pg_config` on PATH would still resolve to the OLD version's. + run: pgxn install pgtap --sudo --pg_config /usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config + - name: Install test_factory into new cluster + # PG_CONFIG must be specified explicitly: at this point both old and + # new PostgreSQL are installed, and the default pg_config on PATH may + # not be the new version's. + run: make install PG_CONFIG=/usr/lib/postgresql/${{ matrix.new_pg }}/bin/pg_config + - name: Stop old cluster, binary pg_upgrade to PostgreSQL ${{ matrix.new_pg }}, start new cluster + run: .github/scripts/pg_upgrade_cluster upgrade ${{ matrix.old_pg }} ${{ matrix.new_pg }} + - name: Run the suite against the pg_upgraded database (existing mode) + # run-suite dynamically asserts the installed version (never + # hardcoded), then runs the suite via --use-existing (so pg_regress + # does not drop/recreate the database) and gates on verify-results -- + # test/install/load.sql's existing-mode branch plants and proves the + # dependency guard as part of this same invocation. + run: bin/test_existing run-suite test_factory_upgrade + + # A single stable check name for use as a required status check in branch + # protection rules (not configured by this PR -- that needs a repo admin). + # Matrix jobs produce check names like "🐘 PostgreSQL 14", which would all + # need to be listed individually and updated whenever the matrix changes. + # This job passes if every other job passed or was skipped (e.g. the heavy + # jobs gated off by the `changes` job on a docs-only push), and fails if any + # failed or were cancelled. + all-checks-passed: + needs: [changes, test, pg-upgrade-test] + if: always() + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v4 + - name: Verify all jobs are listed in needs + # Ensures this job won't silently ignore a newly-added job that was + # omitted from the needs list above. + run: | + DEFINED=$(python3 -c " + import yaml + with open('.github/workflows/ci.yml') as f: + w = yaml.safe_load(f) + print('\n'.join(sorted(j for j in w['jobs'] if j != 'all-checks-passed'))) + ") + NEEDED=$(echo '${{ toJson(needs) }}' | python3 -c " + import json, sys + print('\n'.join(sorted(json.load(sys.stdin)))) + ") + if [ "$DEFINED" != "$NEEDED" ]; then + echo "Some jobs are missing from all-checks-passed needs:" + diff <(echo "$DEFINED") <(echo "$NEEDED") + exit 1 + fi + - name: Check all jobs passed or were skipped + run: | + if [[ "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" == "true" ]]; then + echo "One or more jobs failed or were cancelled" + exit 1 + fi +# vi: expandtab ts=2 sw=2 diff --git a/Makefile b/Makefile index 96f8589..019a98a 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,14 @@ include pgxntool/base.mk PGXNTOOL_ENABLE_TEST_INSTALL = yes +# Already pgxntool's own default, but pinned explicitly (same reasoning as +# ENABLE_TEST_INSTALL above): CI's `test`/`pg-upgrade-test` jobs gate on +# `make verify-results`, not a bare `make test` -- pgxntool marks +# installcheck .IGNORE, so a plain test run exits 0 even when +# regression.diffs is nonempty. Relying on a default silently protects that +# gate only until pgxntool's own default changes. +PGXNTOOL_ENABLE_VERIFY_RESULTS = yes + # ------------------------------------------------------------------------------ # TEST_LOAD_SOURCE: how test/install/load.sql gets the extension to its # target state (fresh/update/existing). See test/install/load.sql for what diff --git a/bin/test_existing b/bin/test_existing new file mode 100755 index 0000000..96b6a66 --- /dev/null +++ b/bin/test_existing @@ -0,0 +1,111 @@ +#!/usr/bin/env bash +# +# Exercise the test_factory test suite against a REAL database whose +# extension was installed -- and, for the pg_upgrade CI job, binary +# pg_upgraded -- OUTSIDE the suite ("existing" mode; see +# test/install/load.sql and test/CLAUDE.md). Modeled on +# Postgres-Extensions/cat_tools's bin/test_existing (same purpose), scoped +# down: test_factory has shipped only one version, so there is no +# bridge/multi-origin update path to carry, and every subcommand below +# always targets the CURRENT build. +# +# USAGE: bin/test_existing [args] +# +# prepare-old DB +# Old-cluster prep for the pg_upgrade CI job: create DB, then install +# pgtap into a dedicated "tap" schema BEFORE test_factory_pgtap +# installs, then CREATE EXTENSION test_factory_pgtap CASCADE (which +# pulls in test_factory) at the current version -- there is no older +# version to bridge from. The tap-schema-first ordering matters: if +# pgtap lands wherever the ambient search_path resolves instead (e.g. +# a bare CREATE EXTENSION test_factory_pgtap CASCADE with no schema +# prep), test_factory_pgtap's own suite (SET search_path = tap) can't +# find pgtap's functions and every test errors with "function +# no_plan() does not exist" -- hit for real while testing the +# foundation this script builds on (PR #22). +# +# run-suite DB +# Assert the installed version matches the current build (never +# hardcoded -- derived from `make -s print-PGXNVERSION`, so a broken +# extraction can't silently compare "" to ""), then run the suite in +# existing mode via --use-existing (pg_regress must not drop/recreate +# DB) and gate on verify-results, not a bare `make test` (pgxntool +# marks installcheck .IGNORE, so a plain test run exits 0 even when +# regression.diffs is nonempty). +# +# No separate guard-planting subcommand: unlike cat_tools, test_factory's +# dependency guard (a view in schema test_factory_drop_guard depending on +# tf.tap(text,text)) is planted AND proved entirely inside +# test/install/load.sql's existing-mode branch, which run-suite's `make +# test` invocation triggers as its own first regression step. This script +# doesn't need to plant or re-prove it separately -- it exists to protect +# the SUITE RUN itself (a stray non-CASCADE drop from a bug elsewhere in the +# suite) rather than to prove the guard survived pg_upgrade, since binary +# pg_upgrade either carries every user object over intact or fails outright; +# there's no partial-survival case here for a pre-upgrade guard to detect +# that a post-upgrade one wouldn't already catch. +set -euo pipefail + +# Run from the repository root regardless of the caller's cwd, same +# resolution cat_tools's bin/test_existing uses. +cd "$(dirname "$(readlink -f "$0")")/.." + +psql_do() { + local db=$1 + shift + psql -d "$db" -v ON_ERROR_STOP=1 "$@" +} + +current_version() { + make -s print-PGXNVERSION 2>/dev/null | sed -n 's/.*set to "\(.*\)"$/\1/p' +} + +installed_version() { + psql -d "$1" -tAc "SELECT extversion FROM pg_extension WHERE extname = 'test_factory'" +} + +# The empty-value guards matter -- "" != "" is false, so a broken extraction +# on either side must not silently pass. +assert_version() { + local db=$1 expected installed + expected=$(current_version) + installed=$(installed_version "$db") + echo "version check '$db': installed='$installed' expected='$expected'" + if [ -z "$installed" ] || [ -z "$expected" ] || [ "$installed" != "$expected" ]; then + echo "FAIL: test_factory in '$db' is '$installed', expected '$expected'" >&2 + exit 1 + fi +} + +prepare_old() { + local db=$1 + createdb "$db" + psql_do "$db" -c "CREATE SCHEMA tap; CREATE EXTENSION pgtap SCHEMA tap; CREATE EXTENSION test_factory_pgtap CASCADE;" +} + +run_suite() { + local db=$1 + assert_version "$db" + local existing_args="TEST_LOAD_SOURCE=existing CONTRIB_TESTDB=$db EXTRA_REGRESS_OPTS=--use-existing PGXNTOOL_ENABLE_TEST_BUILD=no" + make test $existing_args + make verify-results $existing_args +} + +usage() { + echo "usage: bin/test_existing [args]" >&2 + echo " prepare-old DB" >&2 + echo " run-suite DB" >&2 + exit 2 +} + +main() { + local cmd=${1:-} + shift || true + case "$cmd" in + prepare-old) prepare_old "$@" ;; + run-suite) run_suite "$@" ;; + *) usage ;; + esac +} + +main "$@" diff --git a/test/expected/base.out b/test/expected/base.out index 57f115a..f0ec4a5 100644 --- a/test/expected/base.out +++ b/test/expected/base.out @@ -5,11 +5,11 @@ ok 2 - Create function customer__add ok 3 - Register test invoices ok 4 - Ensure original_role temp table was dropped ok 5 - Ensure role is put back after install -ok 6 - Security definer function _tf.schema__getsert has search_path=pg_catalog -ok 7 - Security definer function _tf.test_factory__get has search_path=pg_catalog -ok 8 - Security definer function _tf.test_factory__set has search_path=pg_catalog -ok 9 - Security definer function _tf.table_create has search_path=pg_catalog -ok 10 - Security definer function _tf.get has search_path=pg_catalog +ok 6 - Security definer function _tf.get has search_path=pg_catalog +ok 7 - Security definer function _tf.schema__getsert has search_path=pg_catalog +ok 8 - Security definer function _tf.table_create has search_path=pg_catalog +ok 9 - Security definer function _tf.test_factory__get has search_path=pg_catalog +ok 10 - Security definer function _tf.test_factory__set has search_path=pg_catalog ok 11 - customer table is empty ok 12 - invoice table is empty ok 13 - invoice factory output diff --git a/test/expected/base_1.out b/test/expected/base_1.out index 0076bd0..496f39e 100644 --- a/test/expected/base_1.out +++ b/test/expected/base_1.out @@ -6,11 +6,11 @@ ok 2 - Create function customer__add ok 3 - Register test invoices ok 4 - Ensure original_role temp table was dropped ok 5 # SKIP role-restore check only applies when this session ran CREATE EXTENSION itself (test_load_mode=fresh) -ok 6 - Security definer function _tf.schema__getsert has search_path=pg_catalog -ok 7 - Security definer function _tf.test_factory__get has search_path=pg_catalog -ok 8 - Security definer function _tf.test_factory__set has search_path=pg_catalog -ok 9 - Security definer function _tf.table_create has search_path=pg_catalog -ok 10 - Security definer function _tf.get has search_path=pg_catalog +ok 6 - Security definer function _tf.get has search_path=pg_catalog +ok 7 - Security definer function _tf.schema__getsert has search_path=pg_catalog +ok 8 - Security definer function _tf.table_create has search_path=pg_catalog +ok 9 - Security definer function _tf.test_factory__get has search_path=pg_catalog +ok 10 - Security definer function _tf.test_factory__set has search_path=pg_catalog ok 11 - customer table is empty ok 12 - invoice table is empty ok 13 - invoice factory output diff --git a/test/expected/pgtap.out b/test/expected/pgtap.out index b2b7706..4aafb78 100644 --- a/test/expected/pgtap.out +++ b/test/expected/pgtap.out @@ -7,11 +7,11 @@ ok 3 - Create function customer__add ok 4 - Register test invoices ok 5 - Ensure original_role temp table was dropped ok 6 - Ensure role is put back after install -ok 7 - Security definer function _tf.schema__getsert has search_path=pg_catalog -ok 8 - Security definer function _tf.test_factory__get has search_path=pg_catalog -ok 9 - Security definer function _tf.test_factory__set has search_path=pg_catalog -ok 10 - Security definer function _tf.table_create has search_path=pg_catalog -ok 11 - Security definer function _tf.get has search_path=pg_catalog +ok 7 - Security definer function _tf.get has search_path=pg_catalog +ok 8 - Security definer function _tf.schema__getsert has search_path=pg_catalog +ok 9 - Security definer function _tf.table_create has search_path=pg_catalog +ok 10 - Security definer function _tf.test_factory__get has search_path=pg_catalog +ok 11 - Security definer function _tf.test_factory__set has search_path=pg_catalog ok 12 - Get test data set "base" for table invoice ok 13 - Get test data set "base" for table invoice ok 14 - Ensure we get sane error for a non-existent table diff --git a/test/expected/pgtap_1.out b/test/expected/pgtap_1.out index e7c2b20..8b79556 100644 --- a/test/expected/pgtap_1.out +++ b/test/expected/pgtap_1.out @@ -4,11 +4,11 @@ ok 2 - Create function customer__add ok 3 - Register test invoices ok 4 - Ensure original_role temp table was dropped ok 5 # SKIP role-restore check only applies when this session ran CREATE EXTENSION itself (test_load_mode=fresh) -ok 6 - Security definer function _tf.schema__getsert has search_path=pg_catalog -ok 7 - Security definer function _tf.test_factory__get has search_path=pg_catalog -ok 8 - Security definer function _tf.test_factory__set has search_path=pg_catalog -ok 9 - Security definer function _tf.table_create has search_path=pg_catalog -ok 10 - Security definer function _tf.get has search_path=pg_catalog +ok 6 - Security definer function _tf.get has search_path=pg_catalog +ok 7 - Security definer function _tf.schema__getsert has search_path=pg_catalog +ok 8 - Security definer function _tf.table_create has search_path=pg_catalog +ok 9 - Security definer function _tf.test_factory__get has search_path=pg_catalog +ok 10 - Security definer function _tf.test_factory__set has search_path=pg_catalog ok 11 - Get test data set "base" for table invoice ok 12 - Get test data set "base" for table invoice ok 13 - Ensure we get sane error for a non-existent table diff --git a/test/helpers/create.sql b/test/helpers/create.sql index f320e1f..9d474bb 100644 --- a/test/helpers/create.sql +++ b/test/helpers/create.sql @@ -1,7 +1,9 @@ SET ROLE = DEFAULT; --- test_role itself is created once, idempotently, by test/install/load.sql --- (test/roles.sql is the single source of truth for the name; \i'd via --- test/helpers/deps.sql). +/* + * test_role itself is created once, idempotently, by test/install/load.sql + * (test/roles.sql is the single source of truth for the name; \i'd via + * test/helpers/deps.sql). + */ GRANT USAGE ON SCHEMA tap TO :test_role; /* * DO NOT GRANT test_role TO test_factory__owner; the whole point test_role is @@ -81,9 +83,11 @@ SELECT hasnt_table( , 'Ensure original_role temp table was dropped' ); --- Only meaningful when this session actually ran CREATE EXTENSION itself --- (test_load_mode=fresh; the tables don't exist under update/existing, --- where test/install/load.sql installed/updated the extension earlier). +/* + * Only meaningful when this session actually ran CREATE EXTENSION itself + * (test_load_mode=fresh; the tables don't exist under update/existing, + * where test/install/load.sql installed/updated the extension earlier). + */ SELECT to_regclass('pg_temp.pre_install_role') IS NOT NULL AS has_role_capture \gset \if :has_role_capture SELECT is( @@ -95,6 +99,15 @@ SELECT is( SELECT skip('role-restore check only applies when this session ran CREATE EXTENSION itself (test_load_mode=fresh)', 1); \endif +/* + * ORDER BY is load-bearing, not cosmetic: with no ordering this scans + * pg_proc in physical order, which happens to match creation order on a + * fresh CREATE EXTENSION but is NOT preserved by pg_upgrade (its + * dump/restore reconstructs pg_proc in a different, e.g. name-sorted, + * order) -- discovered because the pg_upgrade CI leg's "existing" run + * produced a row-reordered (but otherwise identical) diff against this + * same query's fresh-install expected output. + */ SELECT cmp_ok( proconfig , '@>' @@ -105,6 +118,7 @@ SELECT cmp_ok( JOIN pg_namespace n ON n.oid = pronamespace WHERE n.nspname IN ( 'tf', '_tf' ) AND p.prosecdef + ORDER BY p.oid::regproc::text ; diff --git a/test/helpers/create_extension.sql b/test/helpers/create_extension.sql index d35d776..8dbe0f9 100644 --- a/test/helpers/create_extension.sql +++ b/test/helpers/create_extension.sql @@ -1,11 +1,13 @@ \echo Creating extension :extension_name --- In 'update'/'existing' mode, test/install/load.sql already installed (and, --- for 'update', updated) the extensions in its own earlier committed --- session -- skip re-creating here instead of erroring or, worse, silently --- replacing the state those modes exist to test. In 'fresh' mode (the only --- mode test/install/load.sql leaves untouched), keep the original --- behavior: no IF NOT EXISTS, so we're confused loudly if something's --- already there instead of silently testing stale state. +/* + * In 'update'/'existing' mode, test/install/load.sql already installed (and, + * for 'update', updated) the extensions in its own earlier committed + * session -- skip re-creating here instead of erroring or, worse, silently + * replacing the state those modes exist to test. In 'fresh' mode (the only + * mode test/install/load.sql leaves untouched), keep the original + * behavior: no IF NOT EXISTS, so we're confused loudly if something's + * already there instead of silently testing stale state. + */ SELECT current_setting('test_factory.test_load_mode') <> 'fresh' AND EXISTS (SELECT 1 FROM pg_extension WHERE extname = :'extension_name') diff --git a/test/install/load.sql b/test/install/load.sql index e74812a..f2fa03f 100644 --- a/test/install/load.sql +++ b/test/install/load.sql @@ -26,9 +26,11 @@ SET client_min_messages = WARNING; --- Read unconditionally, without missing_ok: the Makefile always exports --- test_factory.test_load_mode, so an unset GUC here means the harness --- itself is broken, not "assume fresh". +/* + * Read unconditionally, without missing_ok: the Makefile always exports + * test_factory.test_load_mode, so an unset GUC here means the harness + * itself is broken, not "assume fresh". + */ SELECT current_setting('test_factory.test_load_mode') AS load_mode \gset SELECT current_setting('test_factory.test_update_from') AS update_from \gset SELECT current_setting('test_factory.test_update_to') AS update_to \gset @@ -41,26 +43,32 @@ BEGIN END IF; END $$; --- test_role is test infrastructure, not part of what fresh/update/existing --- describe -- (re)create it idempotently in every mode. A real pg_upgrade --- target (existing mode) carries global objects like roles over, but don't --- assume that; a from-scratch "existing" target might not have it. +/* + * test_role is test infrastructure, not part of what fresh/update/existing + * describe -- (re)create it idempotently in every mode. A real pg_upgrade + * target (existing mode) carries global objects like roles over, but don't + * assume that; a from-scratch "existing" target might not have it. + */ SELECT NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = :'test_role') AS need_role \gset \if :need_role CREATE ROLE :test_role; \endif --- psql's \if only accepts a plain boolean token, not a comparison --- expression -- compute it via SQL first (\if :load_mode = 'existing' would --- silently misparse instead of erroring). +/* + * psql's \if only accepts a plain boolean token, not a comparison + * expression -- compute it via SQL first (\if :load_mode = 'existing' would + * silently misparse instead of erroring). + */ SELECT :'load_mode' = 'existing' AS is_existing \gset \if :is_existing - -- existing: never drop/create/update -- only assert the extensions are - -- actually there, at the version this build considers current. Never - -- hardcode the expected version (see doc's CI dynamic-version-assertion - -- guidance); default_version comes from the same control file `make` - -- itself builds from. + /* + * existing: never drop/create/update -- only assert the extensions are + * actually there, at the version this build considers current. Never + * hardcode the expected version (see doc's CI dynamic-version-assertion + * guidance); default_version comes from the same control file `make` + * itself builds from. + */ DO $$ DECLARE v_installed text := (SELECT extversion FROM pg_extension WHERE extname = 'test_factory'); diff --git a/test/roles.sql b/test/roles.sql index 4f2f5f1..a48f56f 100644 --- a/test/roles.sql +++ b/test/roles.sql @@ -1,8 +1,10 @@ --- Single source of truth for test-only role names. Roles are global (not --- schema- or session-scoped), so this can't be a table constant -- it's a --- psql variable, \i'd from any session that needs to reference the name: --- test/install/load.sql, and test/helpers/create.sql (via --- test/helpers/deps.sql) for the test/sql/*.sql files. +/* + * Single source of truth for test-only role names. Roles are global (not + * schema- or session-scoped), so this can't be a table constant -- it's a + * psql variable, \i'd from any session that needs to reference the name: + * test/install/load.sql, and test/helpers/create.sql (via + * test/helpers/deps.sql) for the *.sql files under test/sql/. + */ \set test_role test_role -- vi: expandtab ts=2 sw=2 diff --git a/test/sql/install.sql b/test/sql/install.sql index eb4e76f..130e4ba 100644 --- a/test/sql/install.sql +++ b/test/sql/install.sql @@ -3,8 +3,10 @@ SET client_min_messages = WARNING; --- psql's \if only accepts a plain boolean token, not a comparison --- expression -- compute it via SQL first. +/* + * psql's \if only accepts a plain boolean token, not a comparison + * expression -- compute it via SQL first. + */ SELECT current_setting('test_factory.test_load_mode') = 'existing' AS is_existing \gset \if :is_existing diff --git a/test/sql/pgtap.sql b/test/sql/pgtap.sql index 10ca458..fda29dc 100644 --- a/test/sql/pgtap.sql +++ b/test/sql/pgtap.sql @@ -3,8 +3,10 @@ SET search_path = tap; --- psql's \if only accepts a plain boolean token, not a comparison --- expression -- compute it via SQL first. +/* + * psql's \if only accepts a plain boolean token, not a comparison + * expression -- compute it via SQL first. + */ SELECT current_setting('test_factory.test_load_mode') = 'existing' AS is_existing \gset \if :is_existing