#!/usr/bin/env bash # Tests de l'image, en local comme en CI. # # ./tests.sh IMAGE démarrage, extensions, tests SQL, redémarrage # ./tests.sh IMAGE PRECEDENTE en plus : reprise d'une base créée par l'image PRECEDENTE # # Tout tourne dans des conteneurs et un volume jetables, sans port publié, supprimés à la fin. set -euo pipefail cd "$(dirname "$0")" IMAGE="${1:?usage : tests.sh IMAGE [IMAGE_PRECEDENTE]}" PRECEDENTE="${2:-}" ID="ptvtest$$" # Utilisateur et base autres que `postgres`, comme chez les consommateurs de l'image : les # extensions doivent être créées dans POSTGRES_DB, par POSTGRES_USER. OPTIONS=(-e POSTGRES_USER=test -e POSTGRES_PASSWORD=test -e POSTGRES_DB=test --memory=768m --memory-swap=768m) # le runner de CI est limité en mémoire nettoyer() { docker rm -f -v "$ID" "$ID-reprise" >/dev/null 2>&1 || true docker volume rm "$ID-donnees" >/dev/null 2>&1 || true } trap nettoyer EXIT sql() { docker exec -i "$1" psql -X -q -v ON_ERROR_STOP=1 -U test -d test "${@:2}"; } # Trois sondes, de la plus exigeante à la moins exigeante. # Premier démarrage : le serveur temporaire de l'initialisation répond déjà aux requêtes, avant # que les extensions soient créées ; il faut aussi la fin de l'initialisation dans le journal. # Le journal est lu en entier avant la recherche : `grep -q` branché sur `docker logs` le coupe # dès la première correspondance, ce que `pipefail` compte comme un échec. initialise() { local journal journal=$(docker logs "$1" 2>&1) grep -q "PostgreSQL init process complete" <<<"$journal" && repond "$1" } repond() { sql "$1" -Atc 'select 1' >/dev/null 2>&1; } # Sans requête : après un changement de version de TimescaleDB, toute requête échoue tant que # l'extension n'est pas mise à niveau (tests/reprise-2-controle.sql). accepte() { docker exec "$1" pg_isready -q -U test -d test; } attendre() { # attendre SONDE CONTENEUR for _ in $(seq 1 90); do "$1" "$2" && return 0 sleep 1 done docker logs "$2" 2>&1 | tail -30 >&2 echo "ÉCHEC : $2 ne démarre pas" >&2 return 1 } echo "== Démarrage de $IMAGE" docker run -d --name "$ID" "${OPTIONS[@]}" "$IMAGE" >/dev/null attendre initialise "$ID" # L'étiquette pgN doit contenir un serveur N : PG_MAJOR vient de l'image de base. attendue=$(docker exec "$ID" printenv PG_MAJOR) servie=$(sql "$ID" -Atc "select current_setting('server_version_num')::int / 10000") [[ "$servie" == "$attendue" ]] || { echo "ÉCHEC : serveur $servie, image pg$attendue" >&2; exit 1; } for fichier in tests/test-*.sql; do echo "== $fichier" sql "$ID" < "$fichier" done echo "== Redémarrage sur des données existantes" docker restart "$ID" >/dev/null attendre repond "$ID" sql "$ID" < tests/test-1-config.sql if [[ -n "$PRECEDENTE" ]]; then echo "== Reprise d'une base créée par $PRECEDENTE" # Le volume se monte là où l'image déclare le sien : /var/lib/postgresql/data jusqu'à # PostgreSQL 17, /var/lib/postgresql à partir de 18. montage=$(docker image inspect -f '{{range $chemin, $_ := .Config.Volumes}}{{$chemin}}{{end}}' "$IMAGE") docker volume create "$ID-donnees" >/dev/null docker run -d --name "$ID-reprise" "${OPTIONS[@]}" -v "$ID-donnees:$montage" "$PRECEDENTE" >/dev/null attendre initialise "$ID-reprise" sql "$ID-reprise" < tests/reprise-1-semis.sql docker stop "$ID-reprise" >/dev/null docker rm "$ID-reprise" >/dev/null docker run -d --name "$ID-reprise" "${OPTIONS[@]}" -v "$ID-donnees:$montage" "$IMAGE" >/dev/null attendre accepte "$ID-reprise" sql "$ID-reprise" < tests/reprise-2-controle.sql fi echo "== Versions" sql "$ID" -At -c "select version()" \ -c "select string_agg(extname || ' ' || extversion, ', ' order by extname) from pg_extension" echo "Tests réussis : $IMAGE"