test/README.md: lead with the shared-test-function-library structure #40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # =========================================================================== | |
| # Test strategy | |
| # | |
| # count_nulls can be arrived at several ways, each of which can break | |
| # differently, so each is exercised by its own job below: | |
| # | |
| # test -- FRESH install: CREATE EXTENSION at the current | |
| # version, across every supported PostgreSQL | |
| # major x schema leg (empty/none and Quoted - | |
| # see TEST_SCHEMA in the Makefile). | |
| # extension-update-test -- IN-PLACE update: CREATE EXTENSION at 0.9.6 | |
| # (the oldest version we still ship a full | |
| # install script for) then ALTER EXTENSION | |
| # UPDATE (same PostgreSQL, no pg_upgrade), same | |
| # PG x schema matrix as test. | |
| # pg-upgrade-test -- BINARY pg_upgrade: install 0.9.6 on an OLD | |
| # PostgreSQL major, binary-upgrade the cluster | |
| # to a NEWER major, then update the extension to | |
| # current - proves objects created on an old | |
| # server still work when read on a new one. A | |
| # smaller old_pg/new_pg x schema matrix (not the | |
| # full PG matrix - this job is by far the most | |
| # expensive, installing two full PostgreSQL | |
| # majors and running the real pg_upgrade binary | |
| # per leg). | |
| # | |
| # All three matrices cross PostgreSQL major with the TEST_SCHEMA axis: | |
| # without a schema specified at all (empty - CREATE EXTENSION lands wherever | |
| # the session's own default search_path resolves) and with one explicitly | |
| # targeted, using a name that requires SQL identifier quoting. Neither leg is | |
| # redundant with the other - see the Makefile's TEST_SCHEMA comment. | |
| # | |
| # `changes` is a cheap gate that lets all three heavy jobs above skip | |
| # themselves on doc-only pushes, and also derives the shared PostgreSQL-major | |
| # list those jobs consume from a single set of constants. `all-checks-passed` | |
| # is the single stable required-status-check name. | |
| # =========================================================================== | |
| name: CI | |
| on: [push, pull_request] | |
| jobs: | |
| # Cheap gate that lets the heavy jobs below skip themselves on commits that | |
| # touch only docs. Must run on every push/pull_request (no paths-ignore on | |
| # the workflow itself), otherwise the required all-checks-passed check | |
| # would never report on doc-only pushes and get stuck Pending in branch | |
| # protection. | |
| # | |
| # Also derives, from a SINGLE set of constants, the supported-PostgreSQL- | |
| # major list the test / extension-update-test jobs consume (see the | |
| # "Derive ..." step): every job that cares which majors are supported reads | |
| # the SAME list, so they can't silently drift onto different sets, and | |
| # adding a new major is a one-line change here instead of an edit in | |
| # several jobs. | |
| changes: | |
| name: 🔍 Detect docs-only 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 to running the full matrix: default docs_only to false | |
| # immediately, before anything below has a chance to compute or | |
| # fail. Writing the same GITHUB_OUTPUT key twice is fine (the last | |
| # write wins), so the only way this step ends with docs_only=true | |
| # is by genuinely proving it further down - never by skipping past | |
| # an edge case with a default. | |
| 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. docs_only is already false from above; | |
| # just stop here rather than risk skipping tests. | |
| if [ -z "$HEAD" ] || [ -z "$BASE" ] || [[ "$BASE" =~ ^0+$ ]]; then | |
| exit 0 | |
| fi | |
| CHANGED=$(git diff --name-only "$BASE" "$HEAD" || echo __DIFF_FAILED__) | |
| DOCS_ONLY=true | |
| if [ "$CHANGED" = "__DIFF_FAILED__" ] || [ -z "$CHANGED" ]; then | |
| DOCS_ONLY=false | |
| else | |
| while IFS= read -r f; do | |
| if ! [[ "$f" =~ \.(md|asc)$ ]]; then | |
| DOCS_ONLY=false | |
| break | |
| fi | |
| done <<< "$CHANGED" | |
| fi | |
| echo "changed files:" | |
| echo "$CHANGED" | |
| echo "docs_only=$DOCS_ONLY" >> "$GITHUB_OUTPUT" | |
| - name: Derive the supported-PostgreSQL-major list | |
| id: pg | |
| run: | | |
| # A dozen-odd lines to replace what looks like a handful of version | |
| # references, but it buys CONSISTENCY: both the fresh-install | |
| # `test` matrix and the `extension-update-test` matrix derive their | |
| # PostgreSQL set from this ONE source, so they cannot silently | |
| # drift onto different lists. Adding a new major is a one-line | |
| # NEWEST bump here, not an edit in N places. | |
| # | |
| # Only one floor is needed here: 0.9.6 (the oldest version | |
| # count_nulls still ships a full install script for) is pure SQL | |
| # over anyarray/json/jsonb with no catalog-version sensitivity, so | |
| # it installs on every PostgreSQL major count_nulls supports - | |
| # there's no separate legacy-only floor to carve out. | |
| NEWEST=18 | |
| FLOOR=10 | |
| supported=$(seq "$NEWEST" -1 "$FLOOR") | |
| # Emit a JSON array from a list of ints, for the job matrices to | |
| # consume with fromJSON (GitHub evaluates a literal dollar-brace | |
| # expression even inside a run block, so none is written here). | |
| json() { printf '%s\n' "$@" | paste -sd, - | sed 's/^/[/; s/$/]/'; } | |
| echo "supported_pg=$(json $supported)" >> "$GITHUB_OUTPUT" | |
| # Baseline: fresh CREATE EXTENSION, across the PG matrix AND a schema | |
| # matrix (TEST_SCHEMA, picked up from the environment by test/deps.sql via | |
| # the count_nulls.test_schema GUC - see the Makefile). Empty ('') runs | |
| # WITHOUT specifying a schema at all - CREATE EXTENSION with no targeting, | |
| # landing wherever the session's own default search_path resolves. | |
| # 'Quoted' runs WITH one explicitly specified, using a name that requires | |
| # SQL identifier quoting (mixed case - unquoted would fold to lowercase), | |
| # exercising the suite's %I schema-qualification rather than just its | |
| # literal test data. Both legs matter; neither is redundant with the | |
| # other (see the Makefile's TEST_SCHEMA comment). | |
| test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| strategy: | |
| matrix: | |
| # From the single source in the changes job. | |
| pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} | |
| schema: ["", Quoted] | |
| name: 🐘 PostgreSQL ${{ matrix.pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }}) | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| env: | |
| TEST_SCHEMA: ${{ matrix.schema }} | |
| steps: | |
| - name: Start PostgreSQL ${{ matrix.pg }} | |
| run: pg-start ${{ matrix.pg }} | |
| - name: Check out the repo | |
| uses: actions/checkout@v4 | |
| - name: Test on PostgreSQL ${{ matrix.pg }} | |
| run: pg-build-test | |
| # Proves the in-place extension update path: CREATE EXTENSION at the | |
| # oldest version we still ship a full install script for (0.9.6), then | |
| # ALTER EXTENSION UPDATE (no pg_upgrade, same PostgreSQL), then run the | |
| # FULL suite against the updated database (same expected output as a | |
| # fresh install of the same schema, so an updated DB must behave | |
| # identically). Complements pg-upgrade-test, which covers the | |
| # cross-major-version binary upgrade instead. | |
| extension-update-test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| strategy: | |
| matrix: | |
| # From the single source in the changes job. | |
| pg: ${{ fromJSON(needs.changes.outputs.supported_pg) }} | |
| schema: ["", Quoted] | |
| name: ⬆️ Extension update test on PostgreSQL ${{ matrix.pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }}) | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| steps: | |
| - name: Start PostgreSQL ${{ matrix.pg }} | |
| run: pg-start ${{ matrix.pg }} | |
| - name: Check out the repo | |
| uses: actions/checkout@v4 | |
| - name: Install count_nulls | |
| run: make install | |
| - name: Update 0.9.6 -> current and run the suite (existing mode) | |
| # update-scenario creates a real database at 0.9.6, plants + proves | |
| # the dependency guard, ALTER EXTENSION UPDATEs to the current | |
| # version, and runs the suite against that updated database in | |
| # existing mode (asserting the version and that the guard still | |
| # blocked a drop, before dropping it itself - see bin/test_existing). | |
| run: bin/test_existing update-scenario count_nulls_update "${{ matrix.schema }}" 0.9.6 | |
| # Proves count_nulls survives a BINARY pg_upgrade (in-place catalog | |
| # migration to a newer PostgreSQL major), not just an in-place extension | |
| # update. Installs 0.9.6 on an old cluster, plants a dependency guard, | |
| # binary-pg_upgrades to a newer cluster, updates the extension to current, | |
| # then runs the suite against the REAL migrated objects in existing mode. | |
| # No bridge-update step first: count_nulls has always been pure SQL | |
| # functions with no SELECT-*-over-catalog views, so it has no known | |
| # pg_upgrade-unsafe old version to bridge past. | |
| # | |
| # Deliberately not doing a stepwise every-major-in-sequence climb (one | |
| # cluster walking 10->11->12->...->newest, vs. the single big jumps here): | |
| # that would catch a regression specific to one particular major-to-major | |
| # boundary, which would matter if count_nulls had views/functions touching | |
| # catalog internals, but it doesn't - pure SQL functions over anyarray/ | |
| # json/jsonb, nothing version-sensitive to break at a specific boundary. | |
| # The two big-jump legs below already cover the axis that matters here. | |
| # Revisit if count_nulls ever grows something catalog-touching. | |
| pg-upgrade-test: | |
| needs: [changes] | |
| if: needs.changes.outputs.docs_only != 'true' | |
| strategy: | |
| matrix: | |
| include: | |
| - old_pg: "10" | |
| new_pg: "18" | |
| schema: "" | |
| - old_pg: "10" | |
| new_pg: "18" | |
| schema: Quoted | |
| - old_pg: "12" | |
| new_pg: "18" | |
| schema: "" | |
| - old_pg: "12" | |
| new_pg: "18" | |
| schema: Quoted | |
| name: 🔄 Binary pg_upgrade ${{ matrix.old_pg }} → ${{ matrix.new_pg }} (schema ${{ matrix.schema == '' && 'none' || matrix.schema }}) | |
| runs-on: ubuntu-latest | |
| container: pgxn/pgxn-tools | |
| env: | |
| # Both clusters must use the same initdb options or pg_upgrade | |
| # refuses to run. | |
| INITDB_OPTS: --data-checksums --auth trust | |
| steps: | |
| - name: Start PostgreSQL ${{ matrix.old_pg }} | |
| run: pg-start ${{ matrix.old_pg }} | |
| - name: Recreate old cluster with data checksums enabled | |
| run: | | |
| pg_ctlcluster ${{ matrix.old_pg }} test stop | |
| pg_dropcluster ${{ matrix.old_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 ${{ matrix.old_pg }} test -- $INITDB_OPTS | |
| pg_ctlcluster ${{ matrix.old_pg }} test start | |
| pg_isready -t 30 | |
| - name: Check out the repo | |
| uses: actions/checkout@v4 | |
| - name: Install count_nulls into old cluster | |
| run: make install | |
| - name: Prepare the old cluster (install + dependency guard) | |
| # prepare-old installs count_nulls at 0.9.6 in the leg's schema, | |
| # then plants + proves the dependency guard, so a later accidental | |
| # CASCADE drop anywhere in this job cannot silently make the | |
| # eventual existing-mode run test a fresh install instead. | |
| run: bin/test_existing prepare-old count_nulls_upgrade "${{ matrix.schema }}" 0.9.6 | |
| - name: Install PostgreSQL ${{ matrix.new_pg }} | |
| run: apt-get install -y postgresql-${{ matrix.new_pg }} postgresql-server-dev-${{ matrix.new_pg }} | |
| - name: Install count_nulls 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: | | |
| pg_ctlcluster ${{ matrix.old_pg }} test stop | |
| pg_createcluster -p 5432 ${{ matrix.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/${{ matrix.new_pg }}/bin/pg_upgrade \ | |
| -b /usr/lib/postgresql/${{ matrix.old_pg }}/bin \ | |
| -B /usr/lib/postgresql/${{ matrix.new_pg }}/bin \ | |
| -d /var/lib/postgresql/${{ matrix.old_pg }}/test \ | |
| -D /var/lib/postgresql/${{ matrix.new_pg }}/test \ | |
| -o '-c config_file=/etc/postgresql/${{ matrix.old_pg }}/test/postgresql.conf' \ | |
| -O '-c config_file=/etc/postgresql/${{ matrix.new_pg }}/test/postgresql.conf'" postgres \ | |
| || { find /tmp/pg_upgrade_logs \ | |
| /var/lib/postgresql/${{ matrix.new_pg }}/test/pg_upgrade_output.d \ | |
| -name '*.log' 2>/dev/null | sort | xargs -r tail -n +1; exit 1; } | |
| pg_ctlcluster ${{ matrix.new_pg }} test start | |
| - name: Update the pg_upgraded extension to the current version | |
| # Exercises ALTER EXTENSION UPDATE on genuinely pg_upgraded objects | |
| # (the extension binary pg_upgrade just migrated), running the | |
| # 0.9.6->1.0.0 update script. | |
| run: bin/test_existing update count_nulls_upgrade | |
| - name: Run the suite against the pg_upgraded database (existing mode) | |
| # run-suite asserts the version, re-proves the dependency guard | |
| # still blocks a non-CASCADE drop (i.e. it survived pg_upgrade), | |
| # drops the guard, then runs the suite against the REAL pg_upgraded | |
| # + updated database via --use-existing (so pg_regress does not | |
| # drop/recreate it) - a plain fresh `make test` would silently test | |
| # a fresh install instead of the migrated objects. | |
| run: bin/test_existing run-suite count_nulls_upgrade "${{ matrix.schema }}" | |
| # A single stable check name for use as a required status check in branch | |
| # protection rules. Matrix jobs produce check names like | |
| # "🐘 PostgreSQL 14 (schema none)" which would all need to be listed | |
| # individually and updated whenever the matrix changes. This job passes if | |
| # all others passed or were 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, extension-update-test, pg-upgrade-test] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - 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 |