Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
d7f775d
Make pg_mkdir_p tolerant of a concurrent directory creation
adunstan Jun 8, 2026
d344450
python tests: add the libpq ctypes layer and in-process Session
adunstan Jun 6, 2026
df8b051
python tests: add the PostgresServer framework and pytest fixtures
adunstan Jun 6, 2026
00e4f0e
python tests: add the pgtap plugin and wire pytest into the meson build
adunstan Jun 6, 2026
a3bf0a2
python tests: add SSL, LDAP, Kerberos, OAuth and pg_regress helpers
adunstan Jun 6, 2026
59df7a4
python tests: pytest suites for the bin/ client tools
adunstan Jun 6, 2026
015f9fe
python tests: pytest suites for the backup and verify tools
adunstan Jun 6, 2026
9698edd
python tests: pytest suites for pg_dump and pg_upgrade
adunstan Jun 6, 2026
3e54c47
python tests: pytest suite for src/test/recovery
adunstan Jun 6, 2026
912c47d
python tests: pytest suite for src/test/subscription
adunstan Jun 6, 2026
4bc772f
python tests: pytest suites for contrib modules
adunstan Jun 6, 2026
db74101
python tests: pytest suites for src/test/modules
adunstan Jun 6, 2026
3c722c8
python tests: pytest suites for the SSL and authentication tests
adunstan Jun 6, 2026
0ade47c
python tests: pytest suites for libpq, ecpg, psql, pgbench and others
adunstan Jun 6, 2026
96d9834
python tests: add README for the pytest/session framework
adunstan Jun 8, 2026
3eb86bf
ci: run the pytest suite in CI
adunstan Jun 12, 2026
5d9d957
ci: temporarily disable the Perl TAP tests
adunstan Jun 12, 2026
18c46d8
libpq: SQLSTATE-based error matching for query failures
gburd Jun 16, 2026
e725bfe
Merge pull request #7 from gburd/feature/sqlstate-error-matching
adunstan Jun 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
26 changes: 22 additions & 4 deletions .github/workflows/pg-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,10 @@ env:
-Dplperl=enabled
-Dplpython=enabled
-Dpltcl=enabled
-Dpytest=enabled
-Dreadline=enabled
-Dssl=openssl
-Dtap_tests=enabled
-Dtap_tests=disabled
-Dzlib=enabled
-Dzstd=enabled

Expand Down Expand Up @@ -521,6 +522,7 @@ jobs:
--pkg-config-path /usr/lib/i386-linux-gnu/pkgconfig/ \
-DPERL=perl5.40-i386-linux-gnu \
-Dlibnuma=disabled \
-Dtap_tests=disabled \
build

- name: Build
Expand Down Expand Up @@ -607,6 +609,7 @@ jobs:
-Duuid=e2fs \
--buildtype=debug \
-Dllvm=enabled \
-Dtap_tests=disabled \
build

- name: Build
Expand All @@ -617,6 +620,15 @@ jobs:

- name: Test world
shell: *su_postgres_shell
# The pytest suite loads libpq in-process via ctypes. Here libpq is
# AddressSanitizer-instrumented, and ASan must come first in the link
# order; dlopening it into an otherwise uninstrumented python aborts
# with "ASan runtime does not come first". Preload the ASan runtime
# for the test run to satisfy that (a no-op for the already-instrumented
# server/client binaries). Scoped to this step so the build is
# unaffected; detect_leaks is already disabled via ASAN_OPTIONS.
env:
ADDITIONAL_SETUP: export LD_PRELOAD="$(gcc -print-file-name=libasan.so)"
run: *meson_test_world_cmd

- *linux_collect_cores_step
Expand Down Expand Up @@ -659,6 +671,8 @@ jobs:
openssl
p5.34-io-tty
p5.34-ipc-run
py312-pexpect
py312-pytest
python312
tcl
zstd
Expand Down Expand Up @@ -815,8 +829,9 @@ jobs:
-Dldap=enabled
-Dplperl=enabled
-Dplpython=enabled
-Dpytest=enabled
-Dssl=openssl
-Dtap_tests=enabled
-Dtap_tests=disabled

defaults:
run:
Expand Down Expand Up @@ -902,9 +917,11 @@ jobs:
- name: Install dependencies
shell: pwsh
run: |
# meson is not preinstalled on windows-2022. Install via pip
# meson is not preinstalled on windows-2022. Install via pip.
# pytest enables the Python test suite (pexpect is omitted: it needs
# a pty, which Windows lacks, and the interactive tests importorskip).
echo ::group::pip
python -m pip install --upgrade meson
python -m pip install --upgrade meson pytest
if (!$?) { throw 'cmdfail' }
echo ::endgroup::

Expand Down Expand Up @@ -1042,6 +1059,7 @@ jobs:
${MINGW_PACKAGE_PREFIX}-meson \
${MINGW_PACKAGE_PREFIX}-perl \
${MINGW_PACKAGE_PREFIX}-pkgconf \
${MINGW_PACKAGE_PREFIX}-python-pytest \
${MINGW_PACKAGE_PREFIX}-readline \
${MINGW_PACKAGE_PREFIX}-zlib \
${MINGW_PACKAGE_PREFIX}-zstd
Expand Down
10 changes: 10 additions & 0 deletions contrib/amcheck/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,14 @@ tests += {
't/006_verify_gin.pl',
],
},
'pytest': {
'tests': [
'pyt/test_001_verify_heapam.py',
'pyt/test_002_cic.py',
'pyt/test_003_cic_2pc.py',
'pyt/test_004_verify_nbtree_unique.py',
'pyt/test_005_pitr.py',
'pyt/test_006_verify_gin.py',
],
},
}
216 changes: 216 additions & 0 deletions contrib/amcheck/pyt/test_001_verify_heapam.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# Copyright (c) 2021-2026, PostgreSQL Global Development Group

"""Test amcheck's verify_heapam against deliberately corrupted heap pages."""

import os
import re
import struct


# Regexes matching the various line-pointer-corruption checks in
# verify_heapam.c, hit by corrupt_first_page on both little-endian and
# big-endian architectures.
_HEAP_CORRUPTION_RES = [
re.compile(
r"line pointer redirection to item at offset \d+ "
r"precedes minimum offset \d+"
),
re.compile(
r"line pointer redirection to item at offset \d+ exceeds maximum offset \d+"
),
re.compile(r"line pointer to page offset \d+ is not maximally aligned"),
re.compile(
r"line pointer length \d+ is less than the minimum tuple header size \d+"
),
re.compile(
r"line pointer to page offset \d+ with length \d+ "
r"ends beyond maximum page offset \d+"
),
]


def relation_filepath(node, session, relname):
"""Return the filesystem path for the named relation."""
pgdata = node.data_dir
rel = session.query_oneval(f"SELECT pg_relation_filepath('{relname}')")
assert rel is not None, f"path not found for relation {relname}"
return os.path.join(pgdata, rel)


def fresh_test_table(session, relname):
"""(Re)create and populate a test table of the given name."""
return session.do(
f"""
DROP TABLE IF EXISTS {relname} CASCADE;
CREATE TABLE {relname} (a integer, b text);
ALTER TABLE {relname} SET (autovacuum_enabled=false);
ALTER TABLE {relname} ALTER b SET STORAGE external;
INSERT INTO {relname} (a, b)
(SELECT gs, repeat('b',gs*10) FROM generate_series(1,1000) gs);
BEGIN;
SAVEPOINT s1;
SELECT 1 FROM {relname} WHERE a = 42 FOR UPDATE;
UPDATE {relname} SET b = b WHERE a = 42;
RELEASE s1;
SAVEPOINT s1;
SELECT 1 FROM {relname} WHERE a = 42 FOR UPDATE;
UPDATE {relname} SET b = b WHERE a = 42;
COMMIT;
"""
)


def fresh_test_sequence(session, seqname):
"""Create a test sequence of the given name."""
return session.do(
f"""
DROP SEQUENCE IF EXISTS {seqname} CASCADE;
CREATE SEQUENCE {seqname}
INCREMENT BY 13
MINVALUE 17
START WITH 23;
SELECT nextval('{seqname}');
SELECT setval('{seqname}', currval('{seqname}') + nextval('{seqname}'));
"""
)


def advance_test_sequence(session, seqname):
"""Call SQL functions to increment the sequence."""
return session.query_oneval(f"SELECT nextval('{seqname}')")


def set_test_sequence(session, seqname):
"""Call SQL functions to set the sequence."""
return session.query_oneval(f"SELECT setval('{seqname}', 102)")


def reset_test_sequence(session, seqname):
"""Call SQL functions to reset the sequence."""
return session.do(f"ALTER SEQUENCE {seqname} RESTART WITH 51")


def corrupt_first_page(node, session, relname):
"""Stop the node, corrupt the first page of the relation, restart it."""
relpath = relation_filepath(node, session, relname)

session.close()
node.stop()

with open(relpath, "r+b") as fh:
# Corrupt some line pointers. The values are chosen to hit the
# various line-pointer-corruption checks in verify_heapam.c
# on both little-endian and big-endian architectures.
fh.seek(32)
fh.write(
struct.pack(
"<6L",
0xAAA15550,
0xAAA0D550,
0x00010000,
0x00008000,
0x0000800F,
0x001E8000,
)
)

node.start()
session.reconnect()


def detects_corruption(session, function, *res):
"""Assert that verify_heapam output matches each corruption regex."""
result = session.query_tuples(f"SELECT * FROM {function}")
for regex in res:
assert regex.search(result), f"expected /{regex.pattern}/ in:\n{result}"


def detects_heap_corruption(session, function):
"""Assert verify_heapam reports the expected heap corruption messages."""
detects_corruption(session, function, *_HEAP_CORRUPTION_RES)


def detects_no_corruption(session, function):
"""Assert verify_heapam reports no corruption (empty output)."""
result = session.query_tuples(f"SELECT * FROM {function}")
assert result == "", f"expected no corruption, got:\n{result}"


def check_all_options_uncorrupted(session, relname):
"""Check various options are stable and report no corruption.

The relname *must* be an uncorrupted table, or this will fail.
"""
for stop in ("true", "false"):
for check_toast in ("true", "false"):
for skip in ("'none'", "'all-frozen'", "'all-visible'"):
for startblock in ("NULL", "0"):
for endblock in ("NULL", "0"):
opts = (
f"on_error_stop := {stop}, "
f"check_toast := {check_toast}, "
f"skip := {skip}, "
f"startblock := {startblock}, "
f"endblock := {endblock}"
)
detects_no_corruption(
session, f"verify_heapam('{relname}', {opts})"
)


def test_001_verify_heapam(create_pg):
#
# Test set-up
#
node = create_pg("test", start=False, initdb_extra=["--no-data-checksums"])
node.append_conf("autovacuum=off")
node.start()
session = node.session()

session.do("CREATE EXTENSION amcheck")

#
# Check a table with data loaded but no corruption, freezing, etc.
#
fresh_test_table(session, "test")
check_all_options_uncorrupted(session, "test")

#
# Check a corrupt table
#
fresh_test_table(session, "test")
corrupt_first_page(node, session, "test")
detects_heap_corruption(session, "verify_heapam('test')")
detects_heap_corruption(session, "verify_heapam('test', skip := 'all-visible')")
detects_heap_corruption(session, "verify_heapam('test', skip := 'all-frozen')")
detects_heap_corruption(session, "verify_heapam('test', check_toast := false)")
detects_heap_corruption(
session, "verify_heapam('test', startblock := 0, endblock := 0)"
)

#
# Check a corrupt table with all-frozen data
#
fresh_test_table(session, "test")
session.do("VACUUM (FREEZE, DISABLE_PAGE_SKIPPING) test")
detects_no_corruption(session, "verify_heapam('test')")
corrupt_first_page(node, session, "test")
detects_heap_corruption(session, "verify_heapam('test')")
detects_no_corruption(session, "verify_heapam('test', skip := 'all-frozen')")

#
# Check a sequence with no corruption. The current implementation of
# sequences doesn't require its own test setup, since sequences are really
# just heap tables under-the-hood. To guard against future implementation
# changes made without remembering to update verify_heapam, we create and
# exercise a sequence, checking along the way that it passes corruption
# checks.
#
fresh_test_sequence(session, "test_seq")
check_all_options_uncorrupted(session, "test_seq")
advance_test_sequence(session, "test_seq")
check_all_options_uncorrupted(session, "test_seq")
set_test_sequence(session, "test_seq")
check_all_options_uncorrupted(session, "test_seq")
reset_test_sequence(session, "test_seq")
check_all_options_uncorrupted(session, "test_seq")
Loading