feat(\db-tool\): introduce unified db-tool package with postgres harness and tests (T0-T8)

This commit is contained in:
2026-04-30 09:06:44 +00:00
parent 395bddee94
commit b5dcbf08a1
27 changed files with 2417 additions and 1 deletions

View File

@@ -0,0 +1,82 @@
{ inputs, self, pkgs, system, ... }: let
lib = inputs.nixpkgs.lib;
# turn anything under test directory into a derivation that exposes $out/run.sh
mkTestDrv = folder: name: type:
if type == "directory" then
pkgs.runCommand "test-${name}" {} ''
if ! [ -f ${"${folder}/${name}/run.sh"} ]; then
echo no run.sh in test/${name}
exit 1
fi
mkdir -p "$out"
cp -r ${"${folder}/${name}"}/* "$out/"
chmod +x "$out/run.sh"
''
else if lib.hasSuffix ".sh" name then
pkgs.runCommand "test-${lib.removeSuffix ".sh" name}" {} ''
mkdir -p "$out"
install -Dm755 ${"${folder}/${name}"} "$out/run.sh"
''
else
null;
testDir = folder: builtins.readDir folder;
# attrset: testName -> drv with run.sh
testDrvs = folder:
lib.mapAttrs' (n: v:
lib.nameValuePair (lib.removeSuffix ".sh" n) v
) (lib.filterAttrs (_: v: v != null)
(lib.mapAttrs (n: t: mkTestDrv folder n t) (testDir folder)));
database = self.packages.${system}."db-tool";
postgresInit = self.packages.${system}."postgres-init";
postgresCleanup = self.packages.${system}."postgres-cleanup";
# Non-postgres tests: .sh files at ./test/ (excluding postgresql/ subdir)
nonPgTestDrvs =
lib.mapAttrs' (n: v: lib.nameValuePair (lib.removeSuffix ".sh" n) v)
(lib.filterAttrs (_: v: v != null)
(lib.mapAttrs (n: t: mkTestDrv ./test n t)
(lib.filterAttrs (n: _: n != "postgresql") (testDir ./test))));
# Postgres tests: subdirs at ./test/postgresql/
pgTestDrvs = testDrvs ./test/postgresql;
mkNonPgTest = testName: testDrv: pkgs.runCommand "db-tool-${testName}"
{
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
buildInputs = [ database postgresInit postgresCleanup pkgs.postgresql_17 pkgs.dash ];
} ''
${builtins.readFile self.legacyPackages.${system}.helpers.posix-shell.log}
test=${testDrv}
export HECTIC_LOG=trace
set -eu
# shellcheck disable=SC1090
. "$test/run.sh"
mkdir -p "$out"
'';
mkPgTest = testName: testDrv: pkgs.runCommand "db-tool-${testName}"
{
nativeBuildInputs = [ pkgs.coreutils pkgs.gnugrep pkgs.gnused ];
buildInputs = [ database postgresInit postgresCleanup pkgs.postgresql_17 pkgs.dash pkgs.netcat-openbsd ];
} ''
${builtins.readFile self.legacyPackages.${system}.helpers.posix-shell.log}
test=${testDrv}
export HECTIC_LOG=trace
set -eu
${builtins.readFile ./postgresql/_lib.sh}
# shellcheck disable=SC1090
. "$test/run.sh"
mkdir -p "$out"
'';
in
(lib.mapAttrs (name: drv: mkNonPgTest name drv) nonPgTestDrvs) //
(lib.mapAttrs (name: drv: mkPgTest name drv) pgTestDrvs)

View File

@@ -0,0 +1,147 @@
# shellcheck shell=dash
# Shared PostgreSQL harness for db-tool tests.
pg_harness__start_busy_socket() {
pg_harness_socket_dir="$1"
PG_HARNESS_BUSY_SOCKET_PATH="$pg_harness_socket_dir/.s.PGSQL.5432"
export PG_HARNESS_BUSY_SOCKET_PATH
rm -f "$PG_HARNESS_BUSY_SOCKET_PATH"
nc -l -U "$PG_HARNESS_BUSY_SOCKET_PATH" >/dev/null 2>&1 &
PG_HARNESS_BUSY_SOCKET_PID=$!
export PG_HARNESS_BUSY_SOCKET_PID
}
pg_harness_start() {
pg_harness_tmp_root="${TMPDIR:-/tmp}"
if [ "${PG_HARNESS_PGDATA_OVERRIDE+x}" ]; then
pgdata_dir="$PG_HARNESS_PGDATA_OVERRIDE"
else
pgdata_dir=$(mktemp -d "$pg_harness_tmp_root/pgdata_XXXXXX")
fi
PGDATA="$pgdata_dir"
export PGDATA
trap 'pg_harness_stop' EXIT INT TERM
if ! initdb -D "$pgdata_dir" --no-locale --encoding=UTF8 -U postgres >/dev/null 2>&1; then
pg_harness_stop
return 1
fi
if [ "${PG_HARNESS_INJECT_BUSY_SOCKET+x}" ] && ! [ "${PG_HARNESS_BUSY_SOCKET_PID+x}" ]; then
pg_harness__start_busy_socket "$pgdata_dir"
fi
if ! pg_ctl start -D "$pgdata_dir" -o "-k $pgdata_dir -h ''" >/dev/null 2>&1; then
pg_harness_stop
return 1
fi
PGHOST="$pgdata_dir"
PGPORT=""
PGUSER="postgres"
PGDATABASE="postgres"
export PGHOST PGPORT PGUSER PGDATABASE
POSTGRESQL_HOST="$PGHOST"
POSTGRESQL_PORT="$PGPORT"
POSTGRESQL_USER="$PGUSER"
POSTGRESQL_DATABASE="$PGDATABASE"
PGURL="postgresql://postgres@localhost/postgres?host=$pgdata_dir"
export POSTGRESQL_HOST POSTGRESQL_PORT POSTGRESQL_USER POSTGRESQL_DATABASE PGURL
pg_harness_ready=
pg_harness_attempt=0
while [ "$pg_harness_attempt" -lt 10 ]; do
if pg_isready -h "$pgdata_dir" >/dev/null 2>&1; then
pg_harness_ready=1
break
fi
sleep 0.5
pg_harness_attempt=$((pg_harness_attempt + 1))
done
if ! [ "$pg_harness_ready" = 1 ]; then
pg_harness_stop
return 1
fi
}
pg_harness_stop() {
trap - EXIT INT TERM
if [ "${PG_HARNESS_BUSY_SOCKET_PID+x}" ]; then
kill "$PG_HARNESS_BUSY_SOCKET_PID" >/dev/null 2>&1 || true
wait "$PG_HARNESS_BUSY_SOCKET_PID" >/dev/null 2>&1 || true
unset PG_HARNESS_BUSY_SOCKET_PID
fi
if [ "${PG_HARNESS_BUSY_SOCKET_PATH+x}" ] && [ -n "$PG_HARNESS_BUSY_SOCKET_PATH" ]; then
rm -f "$PG_HARNESS_BUSY_SOCKET_PATH"
fi
if [ "${PGDATA+x}" ] && [ -n "$PGDATA" ]; then
pg_ctl stop -D "$PGDATA" -m fast >/dev/null 2>&1 || true
rm -rf "$PGDATA"
fi
if [ "${PG_HARNESS_PGDATA_OVERRIDE+x}" ] && [ -n "$PG_HARNESS_PGDATA_OVERRIDE" ]; then
if ! [ "${PGDATA+x}" ] || [ "$PG_HARNESS_PGDATA_OVERRIDE" != "$PGDATA" ]; then
rm -rf "$PG_HARNESS_PGDATA_OVERRIDE"
fi
fi
if [ "${PG_HARNESS_CORRUPT_PATH+x}" ] && [ -n "$PG_HARNESS_CORRUPT_PATH" ]; then
rm -f "$PG_HARNESS_CORRUPT_PATH"
fi
unset PG_HARNESS_BUSY_SOCKET_PATH
unset PG_HARNESS_CORRUPT_PATH
unset PG_HARNESS_INJECT_BUSY_SOCKET
unset PG_HARNESS_PGDATA_OVERRIDE
unset PGHOST PGPORT PGUSER PGDATABASE PGDATA
unset POSTGRESQL_HOST POSTGRESQL_PORT POSTGRESQL_USER POSTGRESQL_DATABASE PGURL
}
pg_harness_start_corrupt_dir() {
pg_harness_tmp_root="${TMPDIR:-/tmp}"
PG_HARNESS_PGDATA_OVERRIDE=$(mktemp "$pg_harness_tmp_root/pgdata_corrupt_XXXXXX")
: > "$PG_HARNESS_PGDATA_OVERRIDE"
PG_HARNESS_CORRUPT_PATH="$PG_HARNESS_PGDATA_OVERRIDE"
export PG_HARNESS_PGDATA_OVERRIDE PG_HARNESS_CORRUPT_PATH
}
pg_harness_busy_socket() {
pg_harness_tmp_root="${TMPDIR:-/tmp}"
PG_HARNESS_INJECT_BUSY_SOCKET=1
export PG_HARNESS_INJECT_BUSY_SOCKET
if [ "${PGDATA+x}" ] && [ -d "$PGDATA" ]; then
pg_harness_socket_dir="$PGDATA"
else
if [ "${PG_HARNESS_PGDATA_OVERRIDE+x}" ]; then
pg_harness_socket_dir="$PG_HARNESS_PGDATA_OVERRIDE"
else
pg_harness_socket_dir=$(mktemp -d "$pg_harness_tmp_root/pgdata_busy_XXXXXX")
PG_HARNESS_PGDATA_OVERRIDE="$pg_harness_socket_dir"
export PG_HARNESS_PGDATA_OVERRIDE
fi
fi
if [ "${PGDATA+x}" ] && [ -d "$PGDATA" ] && ! [ "${PG_HARNESS_BUSY_SOCKET_PID+x}" ]; then
pg_harness__start_busy_socket "$pg_harness_socket_dir"
fi
}
pg_harness_kill_postmaster() {
if ! [ "${PGDATA+x}" ] || ! [ -f "$PGDATA/postmaster.pid" ]; then
return 1
fi
IFS= read -r pg_harness_postmaster_pid < "$PGDATA/postmaster.pid" || return 1
kill -KILL "$pg_harness_postmaster_pid"
}

View File

@@ -0,0 +1,19 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-help
log notice "test case: database --help exits 0"
if ! database --help > /tmp/help-out.txt 2>&1; then
log error "test failed: database --help exited non-zero"
exit 1
fi
for tok in deploy pull_staging cleanup check log init migrator; do
if ! grep -qF "$tok" /tmp/help-out.txt; then
log error "test failed: --help output missing token: $tok"
cat /tmp/help-out.txt >&2
exit 1
fi
done
log notice "test passed"

View File

@@ -0,0 +1,19 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-missing-dir
export PGURL=""
unset LOCAL_DIR DATABASE_DIR DB_URL 2>/dev/null || true
log notice "test case: database deploy fails without LOCAL_DIR"
set +e
database deploy 2>/tmp/missing-err.txt
code=$?
set -e
if [ "$code" = 0 ]; then
log error "test failed: database deploy exited 0 without LOCAL_DIR"
exit 1
fi
log notice "test passed: exited $code"

View File

@@ -0,0 +1,19 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-deploy-basic
pg_harness_start
LOCAL_DIR=$(mktemp -d)
export LOCAL_DIR
mkdir -p "$LOCAL_DIR/devshell"
printf '#!/bin/dash\nexit 0\n' > "$LOCAL_DIR/devshell/postgres-init.sh"
chmod +x "$LOCAL_DIR/devshell/postgres-init.sh"
log notice "test case: database deploy --no-hydrate --no-patch exits 0"
if ! database deploy --no-hydrate --no-patch; then
log error "database deploy failed"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,20 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-deploy-cleanup-flag
pg_harness_start
LOCAL_DIR=$(mktemp -d)
export LOCAL_DIR
mkdir -p "$LOCAL_DIR/devshell"
printf '#!/bin/dash\nexit 0\n' > "$LOCAL_DIR/devshell/postgres-init.sh"
printf '#!/bin/dash\nexit 0\n' > "$LOCAL_DIR/devshell/postgres-cleanup.sh"
chmod +x "$LOCAL_DIR/devshell/postgres-init.sh" "$LOCAL_DIR/devshell/postgres-cleanup.sh"
log notice "test case: database deploy --no-hydrate --no-patch --cleanup exits 0"
if ! database deploy --no-hydrate --no-patch --cleanup; then
log error "database deploy --cleanup failed"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,36 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-init-cleanup-roundtrip
PG_WORKING_DIR=$(mktemp -d)
export PG_WORKING_DIR PG_DATABASE=testdb PG_PORT=5432 PG_SHARED_PRELOAD_LIBRARIES=''
cleanup() {
postgres-cleanup
rm -rf "$PG_WORKING_DIR"
}
trap 'cleanup' EXIT INT TERM
log notice "test case: postgres-init starts cluster"
if ! postgres-init; then
log error "postgres-init failed"
exit 1
fi
pgurl="postgresql://$(id -un)@/testdb?host=${PG_WORKING_DIR}/sock&port=5432"
log notice "verifying connection"
if ! psql "$pgurl" -c 'SELECT 1;' >/dev/null 2>&1; then
log error "connection failed after postgres-init"
exit 1
fi
log notice "test case: postgres-cleanup stops cluster"
postgres-cleanup
log notice "verifying cluster stopped"
if pg_isready -h "${PG_WORKING_DIR}/sock" -p 5432 >/dev/null 2>&1; then
log error "postgres still running after cleanup"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,18 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-log-subcommand
PG_WORKING_DIR=$(mktemp -d)
LOCAL_DIR=$(mktemp -d)
export PG_WORKING_DIR LOCAL_DIR PGURL='postgresql://localhost/db'
mkdir -p "$PG_WORKING_DIR/data/log"
trap 'rm -rf "$PG_WORKING_DIR" "$LOCAL_DIR"' EXIT INT TERM
log notice "test case: database log list exits 0 with empty log dir"
if ! database log list; then
log error "database log list failed"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,19 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-postgres-cleanup-stale-pidfile
PG_WORKING_DIR=$(mktemp -d)
export PG_WORKING_DIR
mkdir -p "$PG_WORKING_DIR/data"
printf '99999999\n' > "$PG_WORKING_DIR/data/postmaster.pid"
trap 'rm -rf "$PG_WORKING_DIR"' EXIT INT TERM
log notice "test case: postgres-cleanup exits 0 with stale pidfile"
if ! postgres-cleanup; then
log error "postgres-cleanup failed with stale pidfile"
exit 1
fi
log notice "test passed"

View File

@@ -0,0 +1,42 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-postgres-init-busy-socket
PG_WORKING_DIR=$(mktemp -d)
export PG_WORKING_DIR PG_DATABASE=testdb PG_PORT=5432 PG_SHARED_PRELOAD_LIBRARIES=''
trap 'pg_harness_stop; rm -rf "$PG_WORKING_DIR"' EXIT INT TERM
log notice "setup: creating initial postgres cluster"
if ! postgres-init; then
log error "setup failed: initial postgres-init failed"
exit 1
fi
log notice "setup: stopping postgres to free socket"
postgres-cleanup
log notice "setup: occupying socket with netcat"
pg_harness__start_busy_socket "$PG_WORKING_DIR/sock"
i=0
while [ "$i" -lt 50 ] && ! [ -S "$PG_HARNESS_BUSY_SOCKET_PATH" ]; do
sleep 0.1
i=$((i + 1))
done
[ -S "$PG_HARNESS_BUSY_SOCKET_PATH" ] || { log error "busy socket not ready"; exit 1; }
printf '%d\n' "$PG_HARNESS_BUSY_SOCKET_PID" > "${PG_HARNESS_BUSY_SOCKET_PATH}.lock"
log notice "test case: postgres-init fails when socket is pre-occupied"
PG_REUSE=1
export PG_REUSE
set +e
postgres-init
code=$?
set -e
if [ "$code" = 0 ]; then
log error "test failed: postgres-init exited 0 with busy socket"
exit 1
fi
log notice "test passed: exited $code"

View File

@@ -0,0 +1,23 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-postgres-init-corrupt-pgdata
pg_harness_start_corrupt_dir
export PG_WORKING_DIR="$PG_HARNESS_PGDATA_OVERRIDE"
export PG_SHARED_PRELOAD_LIBRARIES=''
trap 'pg_harness_stop' EXIT INT TERM
log notice "test case: postgres-init fails when PG_WORKING_DIR is a regular file"
set +e
postgres-init
code=$?
set -e
if [ "$code" = 0 ]; then
log error "test failed: postgres-init exited 0 with corrupt dir"
exit 1
fi
log notice "test passed: exited $code"

View File

@@ -0,0 +1,26 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-pull-staging
export PGURL=""
unset STAGING_SSH_HOST STAGING_DB_URL STAGING_USER STAGING_HOST 2>/dev/null || true
log notice "test case: database pull_staging exits 3 without STAGING_SSH_HOST"
set +e
database pull_staging 2>/build/staging-err.txt
code=$?
set -e
if [ "$code" != 3 ]; then
log error "test failed: expected exit 3, got $code"
exit 1
fi
if ! grep -q 'STAGING_SSH_HOST' /build/staging-err.txt; then
log error "test failed: stderr does not mention STAGING_SSH_HOST"
exit 1
fi
log notice "test passed"
log notice "test passed"

View File

@@ -0,0 +1,16 @@
# shellcheck shell=dash
HECTIC_NAMESPACE=test-db-tool-unknown
log notice "test case: database nonsense exits non-zero (expected 1)"
set +e
database nonsense 2>/dev/null
code=$?
set -e
if [ "$code" = 0 ]; then
log error "test failed: database nonsense exited 0 (should be non-zero)"
exit 1
fi
log notice "test passed: exited $code"