test: add podman/docker dbtest tool for postgres, mssql and mysql

This commit is contained in:
2026-10-03 10:01:48 +02:00
parent 53327b9a5a
commit ccba53c494
6 changed files with 245 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
# dbtest: container test databases
Podman (preferred) or Docker. One tool for postgres, mssql, mysql.
## Commands
| Command | Action |
|---|---|
| `./tests/dbtest/dbtest.sh up <db\|all>` | Start, wait for ready, run init |
| `down <db\|all>` | Stop and remove |
| `restart <db>` | Fresh container |
| `status <db\|all>` | State and connection string |
| `env <db\|all>` | Print `export` line for the test env var |
| `test <db> [pkgs]` | up, `go test`, down |
| `shell <db>` | Interactive client |
| `exec <db> <file>` | Run SQL file |
| `logs <db>` | Container logs |
## Databases
| db | Port | Env var | Init | Default test pkgs |
|---|---|---|---|---|
| postgres | 5439 | `RELSPEC_TEST_PG_CONN` | `tests/postgres/init.sql` | readers/pgsql, writers/pgsql, writers/sqlexec |
| mssql | 1439 | `RELSPEC_TEST_MSSQL_CONN` | `test_data/mssql/test_schema.sql` (creates `RelSpecTest`) | readers/mssql, writers/mssql |
| mysql | 3309 | `RELSPEC_TEST_MYSQL_CONN` | `tests/dbtest/init/mysql.sql` | none (no Go driver/reader yet) |
## Env
| Var | Effect |
|---|---|
| `DBTEST_RUNTIME` | Force `podman` or `docker` |
| `DBTEST_TIMEOUT` | Ready wait, seconds (default 120) |
| `DBTEST_KEEP=1` | Keep container after `test` |
| `DBTEST_GOFLAGS` | Extra `go test` flags |
## Add a database
1. Add `dbs/<name>.sh` defining: `DB_NAME DB_IMAGE DB_CONTAINER DB_PORT DB_INTERNAL_PORT DB_ENV DB_INIT_MOUNT DB_CONN_VAR DB_CONN DB_DEFAULT_PKGS` and functions `db_ready db_post_init db_shell db_exec_file`.
2. Add the name to `ALL_DBS` in `dbtest.sh`.
## Notes
- Containers are named `relspec-test-<db>`; existing `tests/postgres/*.sh` and `make docker-*` use the same postgres name/port 5439 and remain independent.
- mssql needs ~2GB RAM and takes longer to become ready.
- Tests skip when the env var is unset.
+17
View File
@@ -0,0 +1,17 @@
# SQL Server container definition (sourced by dbtest.sh)
DB_NAME=mssql
DB_IMAGE=mcr.microsoft.com/mssql/server:2022-latest
DB_CONTAINER=relspec-test-mssql
DB_PORT=1439
DB_INTERNAL_PORT=1433
DB_ENV=(-e ACCEPT_EULA=Y -e "MSSQL_SA_PASSWORD=StrongPassword123!" -e MSSQL_PID=Express)
DB_INIT_MOUNT="$ROOT/test_data/mssql/test_schema.sql:/init/test_schema.sql"
DB_CONN_VAR=RELSPEC_TEST_MSSQL_CONN
DB_CONN="sqlserver://sa:StrongPassword123!@localhost:1439?database=RelSpecTest"
DB_DEFAULT_PKGS="./pkg/readers/mssql/ ./pkg/writers/mssql/"
_sqlcmd() { rt exec -i "$DB_CONTAINER" /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'StrongPassword123!' "$@"; }
db_ready() { _sqlcmd -Q "SELECT 1" >/dev/null 2>&1; }
db_post_init() { _sqlcmd -b -i /init/test_schema.sql >/dev/null; }
db_shell() { rt exec -it "$DB_CONTAINER" /opt/mssql-tools18/bin/sqlcmd -C -S localhost -U sa -P 'StrongPassword123!' -d RelSpecTest "$@"; }
db_exec_file() { _sqlcmd -b -d RelSpecTest < "$1"; }
+16
View File
@@ -0,0 +1,16 @@
# MySQL container definition (sourced by dbtest.sh)
DB_NAME=mysql
DB_IMAGE=docker.io/library/mysql:8.4
DB_CONTAINER=relspec-test-mysql
DB_PORT=3309
DB_INTERNAL_PORT=3306
DB_ENV=(-e MYSQL_ROOT_PASSWORD=relspec_root_password -e MYSQL_DATABASE=relspec_test -e MYSQL_USER=relspec -e MYSQL_PASSWORD=relspec_test_password)
DB_INIT_MOUNT="$ROOT/tests/dbtest/init/mysql.sql:/docker-entrypoint-initdb.d/init.sql"
DB_CONN_VAR=RELSPEC_TEST_MYSQL_CONN
DB_CONN="relspec:relspec_test_password@tcp(localhost:3309)/relspec_test"
DB_DEFAULT_PKGS=""
db_ready() { rt exec "$DB_CONTAINER" mysqladmin ping -h 127.0.0.1 -urelspec -prelspec_test_password --silent >/dev/null 2>&1; }
db_post_init() { :; }
db_shell() { rt exec -it "$DB_CONTAINER" mysql -urelspec -prelspec_test_password relspec_test "$@"; }
db_exec_file() { rt exec -i "$DB_CONTAINER" mysql -urelspec -prelspec_test_password relspec_test < "$1"; }
+16
View File
@@ -0,0 +1,16 @@
# Postgres container definition (sourced by dbtest.sh)
DB_NAME=postgres
DB_IMAGE=docker.io/library/postgres:16-alpine
DB_CONTAINER=relspec-test-postgres
DB_PORT=5439
DB_INTERNAL_PORT=5432
DB_ENV=(-e POSTGRES_USER=relspec -e POSTGRES_PASSWORD=relspec_test_password -e POSTGRES_DB=relspec_test)
DB_INIT_MOUNT="$ROOT/tests/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql"
DB_CONN_VAR=RELSPEC_TEST_PG_CONN
DB_CONN="postgres://relspec:relspec_test_password@localhost:5439/relspec_test"
DB_DEFAULT_PKGS="./pkg/readers/pgsql/ ./pkg/writers/pgsql/ ./pkg/writers/sqlexec/"
db_ready() { rt exec "$DB_CONTAINER" pg_isready -U relspec -d relspec_test >/dev/null 2>&1; }
db_post_init() { :; }
db_shell() { rt exec -it "$DB_CONTAINER" psql -U relspec -d relspec_test "$@"; }
db_exec_file() { rt exec -i "$DB_CONTAINER" psql -v ON_ERROR_STOP=1 -U relspec -d relspec_test < "$1"; }
+130
View File
@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# Reusable podman/docker test database tool for postgres, mssql and mysql.
# Usage: dbtest.sh <command> <db> [args]
set -euo pipefail
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)"
TIMEOUT="${DBTEST_TIMEOUT:-120}"
ALL_DBS=(postgres mssql mysql)
usage() {
cat <<USAGE
dbtest.sh <command> <db|all> [args]
commands:
up <db> start container, wait until ready, run init
down <db> stop and remove container
restart <db> down + up
status <db> running state and connection string
env <db> print 'export VAR=conn' (use: eval "\$(dbtest.sh env postgres)")
logs <db> container logs
shell <db> interactive client
exec <db> <file> run a SQL file against the database
test <db> [pkgs] up, run go tests with conn env set, down (keep with DBTEST_KEEP=1)
list supported databases
dbs: ${ALL_DBS[*]}
env: DBTEST_RUNTIME=podman|docker DBTEST_TIMEOUT=secs DBTEST_KEEP=1 DBTEST_GOFLAGS=...
USAGE
}
die() { echo "error: $*" >&2; exit 1; }
log() { echo "[dbtest] $*" >&2; }
detect_runtime() {
if [ -n "${DBTEST_RUNTIME:-}" ]; then echo "$DBTEST_RUNTIME"; return; fi
if command -v podman >/dev/null 2>&1; then echo podman
elif command -v docker >/dev/null 2>&1; then echo docker
else die "neither podman nor docker is installed"; fi
}
RUNTIME="$(detect_runtime)"
rt() { "$RUNTIME" "$@"; }
load_db() {
local f="$HERE/dbs/${1:-}.sh"
[ -f "$f" ] || die "unknown db '${1:-}' (supported: ${ALL_DBS[*]})"
# shellcheck disable=SC1090
source "$f"
}
is_running() { [ "$(rt inspect -f '{{.State.Running}}' "$DB_CONTAINER" 2>/dev/null || true)" = "true" ]; }
cmd_up() {
if is_running; then
log "$DB_NAME already running"
else
rt rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
log "starting $DB_NAME ($DB_IMAGE) on port $DB_PORT using $RUNTIME"
rt run -d --name "$DB_CONTAINER" "${DB_ENV[@]}" \
-p "$DB_PORT:$DB_INTERNAL_PORT" \
-v "$DB_INIT_MOUNT:ro,Z" "$DB_IMAGE" >/dev/null
fi
log "waiting for $DB_NAME (max ${TIMEOUT}s)"
local i=0
until db_ready; do
i=$((i + 1))
if [ "$i" -ge "$TIMEOUT" ]; then
rt logs --tail 50 "$DB_CONTAINER" >&2 || true
die "$DB_NAME did not become ready"
fi
sleep 1
done
db_post_init
log "$DB_NAME ready: $DB_CONN_VAR=$DB_CONN"
}
cmd_down() {
rt rm -f "$DB_CONTAINER" >/dev/null 2>&1 || true
log "$DB_NAME removed"
}
cmd_status() {
if is_running; then echo "$DB_NAME: running ($DB_CONTAINER, port $DB_PORT)"; else echo "$DB_NAME: stopped"; fi
echo "$DB_CONN_VAR=$DB_CONN"
}
cmd_test() {
local pkgs="${*:-$DB_DEFAULT_PKGS}"
[ -n "$pkgs" ] || die "no test packages for $DB_NAME; pass packages as arguments"
cmd_up
[ -n "${DBTEST_KEEP:-}" ] || trap cmd_down EXIT
export "$DB_CONN_VAR=$DB_CONN"
cd "$ROOT"
# shellcheck disable=SC2086
go test -count=1 ${DBTEST_GOFLAGS:-} $pkgs
}
cmd="${1:-}"
case "$cmd" in
""|-h|--help|help) usage; exit 0 ;;
list) printf '%s\n' "${ALL_DBS[@]}"; exit 0 ;;
esac
shift
target="${1:-}"
[ -n "$target" ] || die "missing <db>"
shift || true
run_one() {
load_db "$1"
shift
case "$cmd" in
up) cmd_up ;;
down) cmd_down ;;
restart) cmd_down; cmd_up ;;
status) cmd_status ;;
env) echo "export $DB_CONN_VAR='$DB_CONN'" ;;
logs) rt logs "$DB_CONTAINER" ;;
shell) db_shell "$@" ;;
exec) [ -f "${1:-}" ] || die "usage: exec <db> <file>"; db_exec_file "$1" ;;
test) cmd_test "$@" ;;
*) usage; die "unknown command '$cmd'" ;;
esac
}
if [ "$target" = "all" ]; then
case "$cmd" in up|down|restart|status|env) ;; *) die "'all' is only valid for up/down/restart/status/env" ;; esac
for d in "${ALL_DBS[@]}"; do (run_one "$d"); done
else
run_one "$target" "$@"
fi
+21
View File
@@ -0,0 +1,21 @@
-- Minimal MySQL fixture for relspec container tests.
-- Extend when a MySQL reader/writer is added.
CREATE TABLE users (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_users_email (email)
) ENGINE=InnoDB;
CREATE TABLE posts (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
title VARCHAR(255) NOT NULL,
body TEXT NULL,
KEY idx_posts_user (user_id),
CONSTRAINT fk_posts_user FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
) ENGINE=InnoDB;
CREATE VIEW v_user_posts AS
SELECT u.id AS user_id, u.email, p.id AS post_id, p.title
FROM users u JOIN posts p ON p.user_id = u.id;