Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,22 @@ jobs:
run: pg-start ${{ matrix.pg }}
- name: Check out the repo
uses: actions/checkout@v4
# pgxntool's test-build (test/build/build.sql) syncs test/build/*.sql via
# rsync (pgxntool/run-test-build.sh); the pgxn/pgxn-tools image lacks it.
- name: Install rsync
run: apt-get install -y rsync
- name: Test on PostgreSQL ${{ matrix.pg }}
# verify-results runs the same TEST_DEPS chain as `make test`, then
# inspects the pgTAP results (PGXNTOOL_ENABLE_VERIFY_RESULTS defaults
# to yes, PGXNTOOL_VERIFY_RESULTS_MODE to pgtap) and exits non-zero on
# failure.
run: make verify-results
- name: Test the update path on PostgreSQL ${{ matrix.pg }}
# Update-path suite (test/install/load.sql installs TEST_UPDATE_FROM,
# then runs ALTER EXTENSION UPDATE) plus the fresh-vs-update comparison.
# Plain `test` suffices: the fresh verify-results step above already
# checks the same expected output.
run: make test-update

# A single stable check name for use as a required status check in branch
# protection rules. Matrix jobs produce check names like "🐘 PostgreSQL 14"
Expand Down
31 changes: 31 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# Explicit, not auto-detected: an empty test/install/ gives one clear make-time
# error instead of a pile of test failures.
PGXNTOOL_ENABLE_TEST_INSTALL = yes

include pgxntool/base.mk

#
Expand All @@ -6,3 +10,30 @@ include pgxntool/base.mk

test_core_files = $(wildcard $(TESTDIR)/core/*.sql)
testdeps: $(test_core_files)

# TEST_LOAD_SOURCE selects how test/install/load.sql installs trunklet, once and
# committed, for the whole test run:
# - fresh (default): CREATE EXTENSION at the current version.
# - update: CREATE EXTENSION at TEST_UPDATE_FROM, store a test language and
# templates, then ALTER EXTENSION UPDATE to the current version and check
# them (test/helpers/update_with_data.sql). The suite and its expected
# output are the same, so a pass means the updated install behaves like a
# fresh one.
# The settings reach load.sql as placeholder GUCs via PGOPTIONS, which the psql
# processes pg_regress spawns inherit. Both are always exported so load.sql can
# fail if they didn't propagate; test_update_from is only read in update mode.
# There is no `existing` mode: trunklet has no pg_upgrade job.
TEST_LOAD_SOURCE ?= fresh
ifeq ($(filter $(TEST_LOAD_SOURCE),fresh update),)
$(error TEST_LOAD_SOURCE must be 'fresh' or 'update', got '$(TEST_LOAD_SOURCE)')
endif
TEST_UPDATE_FROM ?= 0.2.0
export PGOPTIONS := $(PGOPTIONS) -c trunklet.test_load_mode=$(TEST_LOAD_SOURCE) -c trunklet.test_update_from=$(TEST_UPDATE_FROM)

# Runs the suite against an updated install, then diffs it against a fresh one
# (bin/compare_fresh_vs_update). Recurses so the PGOPTIONS export above is
# re-evaluated with update set.
.PHONY: test-update
test-update:
$(MAKE) test TEST_LOAD_SOURCE=update
bin/compare_fresh_vs_update $(TEST_UPDATE_FROM)
117 changes: 117 additions & 0 deletions bin/compare_fresh_vs_update
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#!/usr/bin/env bash
#
# USAGE: bin/compare_fresh_vs_update FROM_VERSION
#
# Fails, printing a diff, if any trunklet extension member differs between a
# fresh install and one created at FROM_VERSION then ALTER EXTENSION UPDATEd.
# The test suite only proves the behavior it asserts; this catches an update
# script that leaves a member defined differently from a fresh install.
#
# Members come from pg_depend, so new objects are covered without editing this.
# Each one is rendered as its identity, its definition (functions, views, and
# table columns and constraints), its comments and its object-level ACL.
# Not compared: sequence settings (identity, comments and ACL only; the query
# must run on every supported version, where sequence metadata lives in
# different places) and column-level ACLs (pg_attribute.attacl; trunklet grants
# none). Both installs run in scratch databases on the server the usual PG*
# environment points at, and are dropped on exit.
set -euo pipefail

from_version=${1:?usage: $0 FROM_VERSION}

suffix="$$_$RANDOM"
fresh_db="trunklet_compare_fresh_$suffix"
update_db="trunklet_compare_update_$suffix"
tmp=$(mktemp -d)

cleanup() {
dropdb --if-exists "$fresh_db"
dropdb --if-exists "$update_db"
rm -rf "$tmp"
}
trap cleanup EXIT

run_psql() {
local db=$1
shift
psql -X -q -v ON_ERROR_STOP=1 -d "$db" "$@"
}

snapshot() {
run_psql "$1" -tA <<'SQL'
SELECT 'extension trunklet version ' || extversion FROM pg_extension WHERE extname = 'trunklet';

WITH member AS (
SELECT d.classid, d.objid, pg_describe_object(d.classid, d.objid, 0) AS identity
FROM pg_depend d
WHERE d.refclassid = 'pg_extension'::regclass
AND d.refobjid = (SELECT oid FROM pg_extension WHERE extname = 'trunklet')
AND d.deptype = 'e'
)
SELECT concat_ws(E'\n'
, '== ' || m.identity
, CASE WHEN m.classid = 'pg_proc'::regclass THEN pg_get_functiondef(m.objid) END
, (SELECT pg_get_viewdef(c.oid) FROM pg_class c WHERE m.classid = 'pg_class'::regclass AND c.oid = m.objid AND c.relkind = 'v')
, (SELECT string_agg(
format('column %I %s%s%s'
, a.attname
, format_type(a.atttypid, a.atttypmod)
, CASE WHEN a.attnotnull THEN ' NOT NULL' END
, ' DEFAULT ' || pg_get_expr(ad.adbin, ad.adrelid)
)
, E'\n' ORDER BY a.attnum
)
FROM pg_class c
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped
LEFT JOIN pg_attrdef ad ON ad.adrelid = a.attrelid AND ad.adnum = a.attnum
WHERE m.classid = 'pg_class'::regclass AND c.oid = m.objid AND c.relkind = 'r'
)
, (SELECT string_agg(format('constraint %I %s', conname, pg_get_constraintdef(oid)), E'\n' ORDER BY conname)
FROM pg_constraint
WHERE m.classid = 'pg_class'::regclass AND conrelid = m.objid
)
, (SELECT string_agg(
format('comment on %s: %s', pg_describe_object(classoid, objoid, objsubid), description)
, E'\n' ORDER BY objsubid
)
FROM pg_description
WHERE classoid = m.classid AND objoid = m.objid
)
, 'acl: ' || coalesce(
CASE
WHEN m.classid = 'pg_proc'::regclass THEN (SELECT proacl FROM pg_proc WHERE oid = m.objid)
WHEN m.classid = 'pg_class'::regclass THEN (SELECT relacl FROM pg_class WHERE oid = m.objid)
WHEN m.classid = 'pg_namespace'::regclass THEN (SELECT nspacl FROM pg_namespace WHERE oid = m.objid)
WHEN m.classid = 'pg_type'::regclass THEN (SELECT typacl FROM pg_type WHERE oid = m.objid)
END::text
, '(default)'
)
)
FROM member m
ORDER BY m.identity
;
SQL
}

createdb "$fresh_db"
run_psql "$fresh_db" <<'SQL'
SET client_min_messages = warning;
CREATE EXTENSION trunklet;
SQL

createdb "$update_db"
run_psql "$update_db" -v from_version="$from_version" <<'SQL'
SET client_min_messages = warning;
CREATE EXTENSION trunklet VERSION :'from_version';
ALTER EXTENSION trunklet UPDATE;
SQL

snapshot "$fresh_db" > "$tmp/fresh"
snapshot "$update_db" > "$tmp/update"

if diff -u --label "fresh install" --label "updated from $from_version" "$tmp/fresh" "$tmp/update"; then
echo "OK: an install updated from $from_version matches a fresh install"
else
echo "FAIL: an install updated from $from_version differs from a fresh install (diff above)" >&2
exit 1
fi
File renamed without changes.
File renamed without changes.
127 changes: 2 additions & 125 deletions test/core/functions.sql
Original file line number Diff line number Diff line change
Expand Up @@ -151,107 +151,8 @@ BEGIN
END
$body$;

/*
* LANGUAGE FACTORY
*/
CREATE FUNCTION bogus_language_name(
) RETURNS text LANGUAGE sql AS $$SELECT 'bogus template language that does not exist'::text$$;
CREATE FUNCTION get_test_language_name(
langtype text DEFAULT 'text'
) RETURNS text LANGUAGE sql AS $$SELECT format('Our internal %s test language', langtype)$$;
CREATE FUNCTION get_test_language_id(
) RETURNS int LANGUAGE plpgsql AS $body$
BEGIN
BEGIN
--SET LOCAL client_min_messages = debug;
PERFORM trunklet.template_language__add(
get_test_language_name()
, parameter_type := 'text[]'
, template_type := 'text'
, process_function_options := 'LANGUAGE plpgsql'
, process_function_body :=
$process$
DECLARE
-- Convert parameters into a string of ', <parameter 1>, <parameter 2>, ...'
v_args CONSTANT text := array_to_string(
array(
SELECT ', ' || quote_nullable(a)
FROM unnest(parameters) a(a)
)
, ''
);
sql CONSTANT text := format( 'SELECT format( %L%s )', template, v_args );
v_return text;
BEGIN
RAISE DEBUG 'EXECUTE INTO using sql %', sql;
EXECUTE sql INTO v_return;
RETURN v_return;
END
$process$
, extract_parameters_options := 'LANGUAGE sql'
, extract_parameters_body :=
$extract$
SELECT array(
SELECT parameters[i]
FROM generate_subscripts( parameters, 1 ) i
WHERE i = ANY( extract_list::int[] )
)
$extract$
);
EXCEPTION
-- TODO: incorrect return value
WHEN no_data_found OR unique_violation THEN
NULL;
END;
RETURN _trunklet.language__get_id( get_test_language_name() );
END
$body$;

CREATE FUNCTION get_json_language_id(
) RETURNS int LANGUAGE plpgsql AS $body$
BEGIN
BEGIN
--SET LOCAL client_min_messages = debug;
PERFORM trunklet.template_language__add(
get_test_language_name('json')
, parameter_type := 'jsonb'
, template_type := 'json'
, process_function_options := 'LANGUAGE plpgsql'
, process_function_body := $process$
DECLARE
k text;
v text;

v_replace text;
v_return text := template::text;
BEGIN
FOR k,v IN
SELECT key, parameters->>key FROM jsonb_object_keys(parameters) key
LOOP
v_replace := format('!%s!', k);
RAISE DEBUG 'replacing all occurences of % with %', v_replace, v;
v_return := replace( v_return, v_replace, v );
END LOOP;
RETURN v_return::json;
END
$process$
, extract_parameters_options := 'LANGUAGE sql'
, extract_parameters_body := $extract$
SELECT jsonb_object(keys, values) FROM (
SELECT array_agg(k) AS keys, array_agg(parameters->>k) AS values
FROM jsonb_object_keys(parameters) k
WHERE k = ANY( extract_list )
) a
$extract$
);
EXCEPTION
-- TODO: incorrect return value
WHEN no_data_found OR unique_violation THEN
NULL;
END;
RETURN _trunklet.language__get_id( get_test_language_name() );
END
$body$;
-- LANGUAGE FACTORY
\i test/helpers/language_factory.sql

/*
* FUNCTION _trunklet.language__get_id
Expand Down Expand Up @@ -627,30 +528,6 @@ CREATE FUNCTION any_to_trunklet_template(
CREATE FUNCTION any_to_trunklet_parameter(
anyelement
) RETURNS text LANGUAGE plpgsql VOLATILE AS $$BEGIN RETURN quote_literal($1::text) || '::' || pg_typeof($1); END$$;
CREATE FUNCTION get_test_templates(
) RETURNS int[] LANGUAGE plpgsql AS $body$
DECLARE
ids int[];
BEGIN
-- Do this even if the test_templates table already exists
PERFORM get_test_language_id();

BEGIN
SELECT ids INTO ids FROM test_templates;
EXCEPTION
WHEN undefined_table THEN
ids[1] := trunklet.template__add( get_test_language_name(), 'test template', --any_to_trunklet_template
('test 1'::text) );
ids[2] := trunklet.template__add( get_test_language_name(), 'test template', 2, --any_to_trunklet_template
('test 2'::text) );

-- See also test_template__remove
CREATE TEMP TABLE test_templates AS VALUES(ids);
END;

RETURN ids;
END
$body$;

CREATE FUNCTION test_template__add
() RETURNS SETOF text LANGUAGE plpgsql AS $body$
Expand Down
Loading
Loading