Files
ocf-managed-container/tests/verify_promotion_sql.py
T

95 lines
4.8 KiB
Python

#!/usr/bin/env python3
"""Run the production eligibility SQL against controlled PostgreSQL 18 fixtures.
Explicit opt-in integration test: creates/removes only its own disposable Podman
container. No host ports, volumes, cluster credentials or live databases used.
The postgres:18 image must already be present locally.
"""
from pathlib import Path
import os
import json
import shlex
import subprocess
import tempfile
import time
import uuid
ROOT = Path(__file__).resolve().parents[1]
with tempfile.TemporaryDirectory(prefix='mc-score-sql-') as directory:
query_file = Path(directory) / 'query.sql'
env = dict(os.environ, QUERY_FILE=str(query_file))
extract = '. ' + shlex.quote(str(ROOT / 'refactored/managed-container.d/postgres.sh')) + '''
as_service_user() { printf '%s' "${@: -1}" > "$QUERY_FILE"; echo t; }
mc_pg_promotion_eligible
'''
subprocess.run(['bash', '-c', extract], env=env, check=True)
query = query_file.read_text()
subprocess.run(['bash', '-c', extract.replace('mc_pg_promotion_eligible', 'mc_pg_standby_observation')], env=env, check=True)
observation_query = query_file.read_text()
name = 'mc-score-sql-' + uuid.uuid4().hex[:12]
subprocess.run(['podman', 'run', '-d', '--pull=never', '--name', name,
'--network=none', '--tmpfs', '/var/lib/postgresql',
'-e', 'POSTGRES_HOST_AUTH_METHOD=trust',
'docker.io/library/postgres:18'], check=True, capture_output=True)
try:
for _ in range(40):
ready = subprocess.run(['podman', 'exec', name, 'pg_isready', '-U', 'postgres'],
capture_output=True)
if ready.returncode == 0:
break
time.sleep(0.5)
else:
raise RuntimeError('Disposable PostgreSQL did not become ready')
def sql(text):
return subprocess.check_output(['podman', 'exec', '-i', name, 'psql',
'-X', '-U', 'postgres', '-v', 'ON_ERROR_STOP=1',
'-Atq'], input=text, text=True).strip()
assert sql('SHOW server_version_num;').startswith('18')
sql('''
CREATE SCHEMA score_test;
CREATE TABLE score_test.state (recovery bool, paused bool, replay pg_lsn);
INSERT INTO score_test.state VALUES (true,false,'0/20');
CREATE TABLE score_test.pg_stat_wal_receiver (status text, flushed_lsn pg_lsn);
CREATE FUNCTION score_test.pg_is_in_recovery() RETURNS bool LANGUAGE sql
AS 'SELECT recovery FROM score_test.state';
CREATE FUNCTION score_test.pg_is_wal_replay_paused() RETURNS bool LANGUAGE plpgsql AS $$
BEGIN
IF NOT (SELECT recovery FROM score_test.state) THEN
RAISE EXCEPTION 'replay pause is undefined on a primary';
END IF;
RETURN (SELECT paused FROM score_test.state);
END $$;
CREATE FUNCTION score_test.pg_last_wal_replay_lsn() RETURNS pg_lsn LANGUAGE sql
AS 'SELECT replay FROM score_test.state';
''')
cases = [
('caught-up streaming standby', True, False, '0/20', 'streaming', '0/20', 't'),
('replay ahead of receiver snapshot', True, False, '0/30', 'streaming', '0/20', 't'),
('replay lag', True, False, '0/10', 'streaming', '0/20', 'f'),
('paused replay', True, True, '0/20', 'streaming', '0/20', 'f'),
('no replay position', True, False, None, 'streaming', '0/20', 'f'),
('unknown flushed position', True, False, '0/20', 'streaming', None, 'f'),
('receiver stopped', True, False, '0/20', 'stopping', '0/20', 'f'),
('no receiver after primary failure', True, False, '0/20', None, None, 'f'),
('primary skips recovery-only function', False, False, '0/20', None, None, 'f'),
]
def literal(value):
return 'NULL' if value is None else "'" + value + "'"
for label, recovery, paused, replay, status, flushed, expected in cases:
setup = f'''SET search_path=score_test,pg_catalog;
UPDATE score_test.state SET recovery={str(recovery).lower()},paused={str(paused).lower()},replay={literal(replay)};
TRUNCATE score_test.pg_stat_wal_receiver;
'''
if status is not None:
setup += f'INSERT INTO score_test.pg_stat_wal_receiver VALUES ({literal(status)},{literal(flushed)});\n'
result = sql(setup + query)
assert result == expected, (label, result, expected)
observation = json.loads(sql(setup + observation_query))
assert (observation['strict'] is True) == (expected == 't'), (label, observation)
assert observation['identity'][0].isdigit() and observation['identity'][1].startswith('/')
assert observation['recovery'] == recovery
assert observation['receiver'] == status
print('PASS strict and observation SQL:', label)
finally:
subprocess.run(['podman', 'rm', '-f', name], check=True, capture_output=True)