Bump to Postgres 18 in tests and prevent container leaks (#207)
Upgrade the test postgres container from 17-alpine to 18-alpine
to match what we use in the Docker image.
Also, use with_postgres.sh in change_processing.py, since we would
otherwise sometimes leak a Docker container when a test fails.
Assisted-by: Claude
diff --git a/tests/server/ui/change_processing.py b/tests/server/ui/change_processing.py
index 7213b70..d1fa555 100644
--- a/tests/server/ui/change_processing.py
+++ b/tests/server/ui/change_processing.py
@@ -1,13 +1,14 @@
# Check that the LNT REST JSON API is working.
-# RUN: python %s %{utils}
+# RUN: %{utils}/with_postgres.sh %t.pg.log \
+# RUN: python %s
import datetime
import logging
import os
-import subprocess
-import sys
import unittest
+import uuid
+import sqlalchemy
from sqlalchemy import or_
from sqlalchemy.orm import joinedload
@@ -20,10 +21,6 @@
logging.basicConfig(level=logging.DEBUG)
-UTILS_DIR = sys.argv.pop(1)
-START_POSTGRES = os.path.join(UTILS_DIR, 'start_postgres.sh')
-STOP_POSTGRES = os.path.join(UTILS_DIR, 'stop_postgres.sh')
-
def _mkorder(session, ts, rev):
order = ts.Order()
@@ -35,13 +32,26 @@
class ChangeProcessingTests(unittest.TestCase):
"""Test fieldchange and regression building."""
+ def _admin_execute(self, sql):
+ """Run a DDL statement (CREATE/DROP DATABASE) against the server."""
+ base_uri = os.environ['LNT_TEST_DB_URI']
+ engine = sqlalchemy.create_engine(base_uri + '/postgres', isolation_level='AUTOCOMMIT')
+ try:
+ with engine.connect() as conn:
+ conn.execute(sqlalchemy.text(sql))
+ finally:
+ engine.dispose()
+
def setUp(self):
- output = subprocess.check_output([START_POSTGRES, os.devnull], text=True)
- env = dict(line.split('=', 1) for line in output.strip().splitlines())
- self._container = env['LNT_PG_CONTAINER']
+ base_uri = os.environ['LNT_TEST_DB_URI']
+
+ # Create a fresh database for each test method so tests don't
+ # interfere with each other via leftover data.
+ self._db_name = 'lnt_test_' + uuid.uuid4().hex[:8]
+ self._admin_execute(f'CREATE DATABASE {self._db_name}')
self.db = v4db.V4DB(
- env['LNT_TEST_DB_URI'] + "/" + env['LNT_TEST_DB_NAME'],
+ base_uri + "/" + self._db_name,
Config.dummy_instance())
session = self.session = self.db.make_session()
@@ -129,8 +139,7 @@
def tearDown(self):
self.session.close()
self.db.close()
- subprocess.call([STOP_POSTGRES, self._container],
- stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
+ self._admin_execute(f'DROP DATABASE IF EXISTS {self._db_name}')
def test_startup(self):
pass
diff --git a/tests/utils/start_postgres.sh b/tests/utils/start_postgres.sh
deleted file mode 100755
index 7cd5a66..0000000
--- a/tests/utils/start_postgres.sh
+++ /dev/null
@@ -1,110 +0,0 @@
-#!/usr/bin/env bash
-#
-# start_postgres.sh <LOG_FILE>
-#
-# Start a fresh PostgreSQL container. Sets and outputs KEY=VALUE lines for:
-#
-# LNT_PG_CONTAINER Container name (needed to stop it later)
-# LNT_TEST_DB_URI Base connection URL (no database), e.g. postgresql://postgres@127.0.0.1:PORT
-# LNT_TEST_DB_NAME Name of the ready-to-use database ("lnt_test")
-#
-# Shell callers can capture and eval the output, e.g.:
-# pg_output=$(start_postgres.sh /tmp/pg.log)
-# eval "${pg_output}"
-# Python callers can execute it and parse the KEY=VALUE stdout lines.
-#
-# LOG_FILE receives the PostgreSQL server logs (pass /dev/null to discard).
-# The caller is responsible for stopping and removing the container.
-#
-set -euo pipefail
-
-if ! command -v docker > /dev/null 2>&1; then
- echo 1>&2 "error: Could not find 'docker' -- Docker is required to run the tests"
- exit 1
-fi
-
-if [ $# -lt 1 ]; then
- echo 1>&2 "usage: start_postgres.sh <LOG_FILE>"
- exit 1
-fi
-
-LOG_FILE="$1"
-
-# ---------------------------------------------------------------------------
-# Unique container name — allows parallel test runs without collision.
-# ---------------------------------------------------------------------------
-CONTAINER_NAME="lnt_pg_$(uuidgen | tr '[:upper:]' '[:lower:]' | tr -d '-')"
-
-# ---------------------------------------------------------------------------
-# Start the container in the background and bind a random port from the host
-# to the container's 5432 port.
-#
-# Key flags:
-# POSTGRES_HOST_AUTH_METHOD=trust no password needed
-# --tmpfs /var/lib/postgresql/data store data in RAM — faster, no cleanup needed
-# ---------------------------------------------------------------------------
-echo 1>&2 "Starting container ${CONTAINER_NAME} ..."
-docker run \
- --detach \
- --name "${CONTAINER_NAME}" \
- --publish "127.0.0.1::5432" \
- --env POSTGRES_HOST_AUTH_METHOD=trust \
- --tmpfs /var/lib/postgresql/data \
- postgres:17-alpine \
- > /dev/null 2>&1
-
-echo 1>&2 "Streaming PostgreSQL server logs into ${LOG_FILE}"
-docker logs --follow "${CONTAINER_NAME}" >> "${LOG_FILE}" 2>&1 &
-
-# ---------------------------------------------------------------------------
-# Discover the host port that Docker assigned.
-# ---------------------------------------------------------------------------
-HOST_PORT=$(docker port "${CONTAINER_NAME}" 5432/tcp | head -1 | sed 's/.*://')
-if [ -z "${HOST_PORT}" ]; then
- echo 1>&2 "error: could not determine host port for container ${CONTAINER_NAME}"
- docker logs "${CONTAINER_NAME}" 1>&2
- exit 1
-fi
-
-echo 1>&2 "PostgreSQL available at: postgresql://postgres@127.0.0.1:${HOST_PORT}"
-
-# ---------------------------------------------------------------------------
-# Wait for PostgreSQL to accept connections (up to 30 seconds).
-#
-# NOTE: We use '-h localhost' to check via TCP rather than the Unix socket.
-# The official postgres image runs a temporary server during initdb that
-# listens only on the Unix socket (listen_addresses=''). Without '-h localhost',
-# pg_isready can succeed against that temp server, then createdb fails when
-# the socket disappears during the transition to the real server.
-# ---------------------------------------------------------------------------
-MAX_TRIES=30
-for i in $(seq 1 "${MAX_TRIES}"); do
- if docker exec "${CONTAINER_NAME}" pg_isready --quiet -h localhost; then
- echo 1>&2 "PostgreSQL ready after ${i} attempt(s)."
- break
- fi
- if [ "${i}" -eq "${MAX_TRIES}" ]; then
- echo 1>&2 "error: PostgreSQL did not become ready after ${MAX_TRIES}s."
- docker logs "${CONTAINER_NAME}" 1>&2
- exit 1
- fi
- sleep 1
-done
-
-# ---------------------------------------------------------------------------
-# Create a default database for tests to use.
-# ---------------------------------------------------------------------------
-docker exec "${CONTAINER_NAME}" createdb --username=postgres lnt_test
-
-# ---------------------------------------------------------------------------
-# Output machine-readable results to stdout (for callers that capture output,
-# e.g. Python subprocess). Shell callers can source this script directly and
-# use the variables.
-# ---------------------------------------------------------------------------
-LNT_PG_CONTAINER="${CONTAINER_NAME}"
-LNT_TEST_DB_URI="postgresql://postgres@127.0.0.1:${HOST_PORT}"
-LNT_TEST_DB_NAME="lnt_test"
-
-echo "LNT_PG_CONTAINER=${LNT_PG_CONTAINER}"
-echo "LNT_TEST_DB_URI=${LNT_TEST_DB_URI}"
-echo "LNT_TEST_DB_NAME=${LNT_TEST_DB_NAME}"
diff --git a/tests/utils/stop_postgres.sh b/tests/utils/stop_postgres.sh
deleted file mode 100755
index 33ae9dd..0000000
--- a/tests/utils/stop_postgres.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env bash
-#
-# stop_postgres.sh <CONTAINER_NAME>
-#
-# Stop and remove a PostgreSQL container by name. Silently succeeds if the
-# container does not exist or has already been removed.
-#
-set -euo pipefail
-
-if [ $# -lt 1 ]; then
- echo 1>&2 "usage: $(basename "$0") <CONTAINER_NAME>"
- exit 1
-fi
-
-docker stop "$1" > /dev/null 2>&1 || true
-docker rm "$1" > /dev/null 2>&1 || true
diff --git a/tests/utils/with_postgres.sh b/tests/utils/with_postgres.sh
index eadefc0..284b517 100755
--- a/tests/utils/with_postgres.sh
+++ b/tests/utils/with_postgres.sh
@@ -15,8 +15,13 @@
set -euo pipefail
# ---------------------------------------------------------------------------
-# Arguments
+# Prerequisites
# ---------------------------------------------------------------------------
+if ! command -v docker > /dev/null 2>&1; then
+ echo 1>&2 "error: Could not find 'docker' -- Docker is required to run the tests"
+ exit 1
+fi
+
if [ $# -lt 2 ]; then
echo 1>&2 "usage: $(basename "$0") <LOG_FILE> <command> [args...]"
exit 1
@@ -31,26 +36,89 @@
fi
# ---------------------------------------------------------------------------
-# Start the container via start_postgres.sh and capture its output (environment
-# variables) so we can get the DB URI and name.
+# Unique container name — allows parallel test runs without collision.
# ---------------------------------------------------------------------------
-SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
-pg_output=$("${SCRIPT_DIR}/start_postgres.sh" "${LOG_FILE}")
-eval "${pg_output}"
-export LNT_TEST_DB_URI LNT_TEST_DB_NAME
+CONTAINER_NAME="lnt_pg_$(uuidgen | tr '[:upper:]' '[:lower:]' | tr -d '-')"
+
+# ---------------------------------------------------------------------------
+# Start the container in the background and bind a random port from the host
+# to the container's 5432 port.
+#
+# Key flags:
+# POSTGRES_HOST_AUTH_METHOD=trust no password needed
+# --tmpfs /var/lib/postgresql store data in RAM — faster, no cleanup needed
+# ---------------------------------------------------------------------------
+echo 1>&2 "Starting container ${CONTAINER_NAME} ..."
+docker run \
+ --detach \
+ --name "${CONTAINER_NAME}" \
+ --publish "127.0.0.1::5432" \
+ --env POSTGRES_HOST_AUTH_METHOD=trust \
+ --tmpfs /var/lib/postgresql \
+ postgres:18-alpine \
+ > /dev/null 2>&1
# ---------------------------------------------------------------------------
# Cleanup — always stop and remove the container, success or failure.
+# Registered immediately after docker run so that failures during the
+# readiness wait or createdb don't leak the container.
# ---------------------------------------------------------------------------
cleanup() {
local exit_code=$?
- echo "Stopping container ${LNT_PG_CONTAINER} ..."
- "${SCRIPT_DIR}/stop_postgres.sh" "${LNT_PG_CONTAINER}"
+ echo "Stopping container ${CONTAINER_NAME} ..."
+ docker stop "${CONTAINER_NAME}" > /dev/null 2>&1 || true
+ docker rm "${CONTAINER_NAME}" > /dev/null 2>&1 || true
exit "${exit_code}"
}
trap cleanup EXIT
+echo 1>&2 "Streaming PostgreSQL server logs into ${LOG_FILE}"
+docker logs --follow "${CONTAINER_NAME}" >> "${LOG_FILE}" 2>&1 &
+
# ---------------------------------------------------------------------------
-# Run the wrapped command.
+# Discover the host port that Docker assigned.
# ---------------------------------------------------------------------------
+HOST_PORT=$(docker port "${CONTAINER_NAME}" 5432/tcp | head -1 | sed 's/.*://')
+if [ -z "${HOST_PORT}" ]; then
+ echo 1>&2 "error: could not determine host port for container ${CONTAINER_NAME}"
+ docker logs "${CONTAINER_NAME}" 1>&2
+ exit 1
+fi
+
+echo 1>&2 "PostgreSQL available at: postgresql://postgres@127.0.0.1:${HOST_PORT}"
+
+# ---------------------------------------------------------------------------
+# Wait for PostgreSQL to accept connections (up to 30 seconds).
+#
+# NOTE: We use '-h localhost' to check via TCP rather than the Unix socket.
+# The official postgres image runs a temporary server during initdb that
+# listens only on the Unix socket (listen_addresses=''). Without '-h localhost',
+# pg_isready can succeed against that temp server, then createdb fails when
+# the socket disappears during the transition to the real server.
+# ---------------------------------------------------------------------------
+MAX_TRIES=30
+for i in $(seq 1 "${MAX_TRIES}"); do
+ if docker exec "${CONTAINER_NAME}" pg_isready --quiet -h localhost; then
+ echo 1>&2 "PostgreSQL ready after ${i} attempt(s)."
+ break
+ fi
+ if [ "${i}" -eq "${MAX_TRIES}" ]; then
+ echo 1>&2 "error: PostgreSQL did not become ready after ${MAX_TRIES}s."
+ docker logs "${CONTAINER_NAME}" 1>&2
+ exit 1
+ fi
+ sleep 1
+done
+
+# ---------------------------------------------------------------------------
+# Create a default database for tests to use.
+# ---------------------------------------------------------------------------
+docker exec "${CONTAINER_NAME}" createdb --username=postgres lnt_test
+
+# ---------------------------------------------------------------------------
+# Export the connection details and run the wrapped command.
+# ---------------------------------------------------------------------------
+export LNT_TEST_DB_URI="postgresql://postgres@127.0.0.1:${HOST_PORT}"
+export LNT_TEST_DB_NAME="lnt_test"
+
"$@"