From 01aed7fb15c3c2eba55a1bb3a6161f21362ad15d Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 28 Sep 2026 01:05:18 +0200 Subject: [PATCH 1/2] ci: wait for database test schemas with bounded probes --- .github/workflows/sqlx.yml | 25 +++++++----- tests/docker-compose.yml | 2 +- tests/mssql/configure-db.sh | 10 ++++- tests/test_wait_for_db.py | 80 +++++++++++++++++++++++++++++++++++++ tests/wait-for-db.sh | 57 ++++++++++++++++++++++++++ 5 files changed, 162 insertions(+), 12 deletions(-) create mode 100644 tests/test_wait_for_db.py create mode 100644 tests/wait-for-db.sh diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index 5162d034b9..d76ca66c7f 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -7,6 +7,13 @@ on: - main jobs: + test-infrastructure: + name: Test infrastructure + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - run: python3 -m unittest discover -s tests -p 'test_wait_for_db.py' + format: name: Format runs-on: ubuntu-24.04 @@ -168,7 +175,7 @@ jobs: - run: | docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_${{ matrix.postgres }} postgres_${{ matrix.postgres }} - docker exec postgres_${{ matrix.postgres }} bash -c "until pg_isready; do sleep 1; done" + bash tests/wait-for-db.sh postgres_${{ matrix.postgres }} postgres - run: | cargo test --no-default-features --features any,postgres,macros,all-types,${{ matrix.tls }} @@ -197,7 +204,7 @@ jobs: prefix-key: v1-sqlx shared-key: postgres-rustls save-if: ${{ false }} - - run: docker compose up --wait postgres_16 + - run: docker compose up --wait --wait-timeout 120 postgres_16 working-directory: tests - run: cargo test --no-default-features --features any,postgres,macros,all-types,rustls env: @@ -230,8 +237,8 @@ jobs: --features mysql,all-types,${{ matrix.tls }},macros,migrate \ -- -D warnings - - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 mysql_${{ matrix.mysql }} - - run: sleep 60 + - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mysql_${{ matrix.mysql }} mysql_${{ matrix.mysql }} + - run: bash tests/wait-for-db.sh mysql_${{ matrix.mysql }} mysql - run: | cargo test --no-default-features --features any,mysql,macros,migrate,all-types,${{ matrix.tls }} @@ -272,8 +279,8 @@ jobs: --features mysql,${{ matrix.tls }},macros,migrate \ -- -D warnings - - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 mariadb_${{ matrix.mariadb }} - - run: sleep 30 + - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mariadb_${{ matrix.mariadb }} mariadb_${{ matrix.mariadb }} + - run: bash tests/wait-for-db.sh mariadb_${{ matrix.mariadb }} mysql - run: | cargo test --no-default-features --features any,mysql,macros,migrate,${{ matrix.tls }} @@ -307,8 +314,8 @@ jobs: --features mssql,all-types,${{ matrix.tls }},macros,migrate \ -- -D warnings - - run: docker compose -f tests/docker-compose.yml run -d -p 1433:1433 mssql_${{ matrix.mssql }} - - run: sleep 80 # MSSQL takes a "bit" to startup + - run: docker compose -f tests/docker-compose.yml run -d -p 1433:1433 --name mssql_${{ matrix.mssql }} mssql_${{ matrix.mssql }} + - run: bash tests/wait-for-db.sh mssql_${{ matrix.mssql }} mssql - run: | cargo test --no-default-features --features any,mssql,macros,migrate,all-types,${{ matrix.tls }} @@ -330,7 +337,7 @@ jobs: - name: Start Postgres (no SSL) run: | docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_16_no_ssl postgres_16_no_ssl - docker exec postgres_16_no_ssl bash -c "until pg_isready; do sleep 1; done" + bash tests/wait-for-db.sh postgres_16_no_ssl postgres - name: Install unixODBC and ODBC drivers (PostgreSQL, SQLite) run: | sudo apt-get update diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index ee394cbda5..84376ee23d 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -88,7 +88,7 @@ services: POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_INITDB_ARGS: --auth-host=trust healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"] volumes: - "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql" command: > diff --git a/tests/mssql/configure-db.sh b/tests/mssql/configure-db.sh index 02b8ea4ad6..2d272b1895 100644 --- a/tests/mssql/configure-db.sh +++ b/tests/mssql/configure-db.sh @@ -1,11 +1,17 @@ #!/usr/bin/env bash +set -euo pipefail # Wait for SQL Server to be ready for connections -until /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -Q "SELECT 1;" -No +deadline=$((SECONDS + 120)) +until /opt/mssql-tools18/bin/sqlcmd -S tcp:127.0.0.1,1433 -U sa -P "$SA_PASSWORD" -d master -Q "SELECT 1;" -No -b -l 2 do + if ((SECONDS >= deadline)); then + echo "SQL Server did not become ready within 120 seconds" >&2 + exit 1 + fi echo "Waiting for SQL Server to be ready..." sleep 1 done # Run the setup script to create the DB and the schema in the DB -/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -i setup.sql -No +/opt/mssql-tools18/bin/sqlcmd -S tcp:127.0.0.1,1433 -U sa -P "$SA_PASSWORD" -d master -i setup.sql -No -b -l 2 diff --git a/tests/test_wait_for_db.py b/tests/test_wait_for_db.py new file mode 100644 index 0000000000..8c0d1f5202 --- /dev/null +++ b/tests/test_wait_for_db.py @@ -0,0 +1,80 @@ +"""Exercise readiness retries and failure reporting without a Docker daemon.""" +import os +from pathlib import Path +import subprocess +import sys +import tempfile +import unittest + + +class DatabaseReadinessTests(unittest.TestCase): + def run_wait(self, behavior, driver="postgres", wait_seconds="2"): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + docker = root / "docker" + docker.write_text( + f"#!{sys.executable}\n" + ''' +import os +from pathlib import Path +import sys +import time + +state = Path(os.environ["PROBE_STATE"]) +mode = os.environ["PROBE_BEHAVIOR"] +command = sys.argv[1] +if command == "exec": + attempts = int(state.read_text()) if state.exists() else 0 + state.write_text(str(attempts + 1)) + if mode == "hang": + time.sleep(60) + sys.exit(0 if mode == "ready" or (mode == "retry" and attempts > 0) else 1) +if command == "inspect": + print("false" if mode == "stopped" else "true") +if command == "logs": + print("database startup diagnostics") +''' + ) + docker.chmod(0o755) + env = dict(os.environ, PATH=f"{root}{os.pathsep}{os.environ['PATH']}", + PROBE_STATE=str(root / "attempts"), PROBE_BEHAVIOR=behavior) + result = subprocess.run( + ["bash", str(Path(__file__).with_name("wait-for-db.sh")), + "test-container", driver, wait_seconds], + env=env, text=True, capture_output=True, timeout=8, + ) + attempts = int((root / "attempts").read_text()) if (root / "attempts").exists() else 0 + return result, attempts + + def test_supported_drivers_succeed_when_ready(self): + for driver in ("postgres", "mysql", "mssql"): + with self.subTest(driver=driver): + result, attempts = self.run_wait("ready", driver) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(attempts, 1) + + def test_retries_until_schema_is_ready(self): + result, attempts = self.run_wait("retry", wait_seconds="5") + self.assertEqual(result.returncode, 0, result.stderr) + self.assertGreater(attempts, 1) + + def test_stopped_container_fails_with_logs(self): + result, attempts = self.run_wait("stopped", wait_seconds="60") + self.assertNotEqual(result.returncode, 0) + self.assertEqual(attempts, 1) + self.assertIn("database startup diagnostics", result.stderr) + + def test_hung_probe_is_bounded_and_reports_logs(self): + result, _ = self.run_wait("hang", wait_seconds="1") + self.assertNotEqual(result.returncode, 0) + self.assertIn("database startup diagnostics", result.stderr) + + def test_invalid_arguments_never_probe_database(self): + for driver, wait in (("unknown", "2"), ("postgres", "0")): + with self.subTest(driver=driver, wait=wait): + result, attempts = self.run_wait("ready", driver, wait) + self.assertEqual(result.returncode, 2) + self.assertEqual(attempts, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/wait-for-db.sh b/tests/wait-for-db.sh new file mode 100644 index 0000000000..9a24db8f96 --- /dev/null +++ b/tests/wait-for-db.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +set -euo pipefail + +container=${1:?usage: wait-for-db.sh CONTAINER postgres|mysql|mssql [TIMEOUT_SECONDS]} +driver=${2:?missing database driver} +wait_seconds=${3:-120} +if [[ ! $wait_seconds =~ ^[1-9][0-9]*$ ]]; then + echo "Timeout must be a positive number of seconds" >&2 + exit 2 +fi + +# Check the test schema over TCP, not just the server process or temporary +# initialization socket. SQL Server creates its schema in a background script. +case "$driver" in + postgres) + probe=(env PGPASSWORD=password psql -h 127.0.0.1 -U postgres -d sqlx + -v ON_ERROR_STOP=1 -c 'SELECT 1 FROM tweet LIMIT 0') + ;; + mysql) + probe=(sh -c ' + client=$(command -v mariadb || command -v mysql) + exec "$client" --protocol=TCP -h 127.0.0.1 -u root -ppassword -D sqlx \ + -e "SELECT 1 FROM tweet LIMIT 0" + ') + ;; + mssql) + probe=(bash -c ' + sqlcmd=/opt/mssql-tools18/bin/sqlcmd + if [[ ! -x "$sqlcmd" ]]; then sqlcmd=/opt/mssql-tools/bin/sqlcmd; fi + exec "$sqlcmd" -S tcp:127.0.0.1,1433 -U sa -P "$SA_PASSWORD" -d sqlx \ + -Q "SELECT TOP 0 * FROM dbo.tweet" -b -C -l 2 + ') + ;; + *) + echo "Unsupported database driver: $driver" >&2 + exit 2 + ;; +esac + +deadline=$((SECONDS + wait_seconds)) +while ((SECONDS < deadline)); do + remaining=$((deadline - SECONDS)) + if ((remaining <= 0)); then break; fi + if ((remaining > 5)); then remaining=5; fi + if timeout --foreground "${remaining}s" docker exec "$container" "${probe[@]}" >/dev/null 2>&1; then + echo "$container test schema is ready" + exit 0 + fi + if [[ $(timeout --foreground 5s docker inspect --format '{{.State.Running}}' "$container" 2>/dev/null) != true ]]; then + break + fi + sleep 1 +done + +echo "$container did not become ready within ${wait_seconds}s (or stopped)" >&2 +timeout --foreground 5s docker logs --tail 100 "$container" >&2 || true +exit 1 From 9c30a78fdc203f191a067d6f35d4348f1429ba22 Mon Sep 17 00:00:00 2001 From: lovasoa Date: Mon, 28 Sep 2026 01:22:15 +0200 Subject: [PATCH 2/2] ci: replace custom readiness framework with Compose healthchecks --- .github/workflows/sqlx.yml | 46 +++++++++++++-------- tests/docker-compose.yml | 62 ++++++++++++++++++++-------- tests/mssql/configure-db.sh | 10 +---- tests/test_wait_for_db.py | 80 ------------------------------------- tests/wait-for-db.sh | 57 -------------------------- 5 files changed, 77 insertions(+), 178 deletions(-) delete mode 100644 tests/test_wait_for_db.py delete mode 100644 tests/wait-for-db.sh diff --git a/.github/workflows/sqlx.yml b/.github/workflows/sqlx.yml index d76ca66c7f..332bb62ab2 100644 --- a/.github/workflows/sqlx.yml +++ b/.github/workflows/sqlx.yml @@ -6,14 +6,13 @@ on: branches: - main -jobs: - test-infrastructure: - name: Test infrastructure - runs-on: ubuntu-24.04 - steps: - - uses: actions/checkout@v6 - - run: python3 -m unittest discover -s tests -p 'test_wait_for_db.py' +# Tests and the ODBC DSN use these fixed ports; local Compose runs use random ports. +env: + SQLX_POSTGRES_PORT: "5432" + SQLX_MYSQL_PORT: "3306" + SQLX_MSSQL_PORT: "1433" +jobs: format: name: Format runs-on: ubuntu-24.04 @@ -174,8 +173,7 @@ jobs: RUSTFLAGS: --cfg postgres_${{ matrix.postgres }} - run: | - docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_${{ matrix.postgres }} postgres_${{ matrix.postgres }} - bash tests/wait-for-db.sh postgres_${{ matrix.postgres }} postgres + docker compose -f tests/docker-compose.yml up --wait --wait-timeout 120 postgres_${{ matrix.postgres }} - run: | cargo test --no-default-features --features any,postgres,macros,all-types,${{ matrix.tls }} @@ -192,6 +190,9 @@ jobs: # FIXME: needed to disable `ltree` tests in Postgres 9.6 # but `PgLTree` should just fall back to text format RUSTFLAGS: --cfg postgres_${{ matrix.postgres }} + - name: Show database logs on failure + if: failure() + run: docker compose -f tests/docker-compose.yml logs --tail 100 postgres_ssl_client_cert: name: Postgres with SSL client cert @@ -209,6 +210,9 @@ jobs: - run: cargo test --no-default-features --features any,postgres,macros,all-types,rustls env: DATABASE_URL: postgres://postgres@localhost:5432/sqlx?sslmode=verify-ca&sslrootcert=./tests/certs/ca.crt&sslcert=./tests/certs/client.crt&sslkey=./tests/keys/client.key + - name: Show database logs on failure + if: failure() + run: docker compose -f tests/docker-compose.yml logs --tail 100 mysql: name: MySQL @@ -237,8 +241,7 @@ jobs: --features mysql,all-types,${{ matrix.tls }},macros,migrate \ -- -D warnings - - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mysql_${{ matrix.mysql }} mysql_${{ matrix.mysql }} - - run: bash tests/wait-for-db.sh mysql_${{ matrix.mysql }} mysql + - run: docker compose -f tests/docker-compose.yml up --wait --wait-timeout 120 mysql_${{ matrix.mysql }} - run: | cargo test --no-default-features --features any,mysql,macros,migrate,all-types,${{ matrix.tls }} @@ -251,6 +254,9 @@ jobs: if: ${{ !(matrix.mysql == '5_7' && matrix.tls == 'rustls') }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx + - name: Show database logs on failure + if: failure() + run: docker compose -f tests/docker-compose.yml logs --tail 100 mariadb: name: MariaDB @@ -279,13 +285,15 @@ jobs: --features mysql,${{ matrix.tls }},macros,migrate \ -- -D warnings - - run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mariadb_${{ matrix.mariadb }} mariadb_${{ matrix.mariadb }} - - run: bash tests/wait-for-db.sh mariadb_${{ matrix.mariadb }} mysql + - run: docker compose -f tests/docker-compose.yml up --wait --wait-timeout 120 mariadb_${{ matrix.mariadb }} - run: | cargo test --no-default-features --features any,mysql,macros,migrate,${{ matrix.tls }} env: DATABASE_URL: mysql://root:password@localhost:3306/sqlx + - name: Show database logs on failure + if: failure() + run: docker compose -f tests/docker-compose.yml logs --tail 100 mssql: name: MSSQL @@ -314,13 +322,15 @@ jobs: --features mssql,all-types,${{ matrix.tls }},macros,migrate \ -- -D warnings - - run: docker compose -f tests/docker-compose.yml run -d -p 1433:1433 --name mssql_${{ matrix.mssql }} mssql_${{ matrix.mssql }} - - run: bash tests/wait-for-db.sh mssql_${{ matrix.mssql }} mssql + - run: docker compose -f tests/docker-compose.yml up --wait --wait-timeout 120 mssql_${{ matrix.mssql }} - run: | cargo test --no-default-features --features any,mssql,macros,migrate,all-types,${{ matrix.tls }} env: DATABASE_URL: mssql://sa:Password123!@localhost/sqlx + - name: Show database logs on failure + if: failure() + run: docker compose -f tests/docker-compose.yml logs --tail 100 odbc: name: ODBC (PostgreSQL and SQLite) @@ -336,8 +346,7 @@ jobs: save-if: ${{ github.ref == 'refs/heads/main' }} - name: Start Postgres (no SSL) run: | - docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_16_no_ssl postgres_16_no_ssl - bash tests/wait-for-db.sh postgres_16_no_ssl postgres + docker compose -f tests/docker-compose.yml up --wait --wait-timeout 120 postgres_16_no_ssl - name: Install unixODBC and ODBC drivers (PostgreSQL, SQLite) run: | sudo apt-get update @@ -356,3 +365,6 @@ jobs: run: cargo test --no-default-features --features any,odbc,macros,all-types,rustls env: DATABASE_URL: Driver={SQLite3};Database=./tests/odbc/sqlite.db + - name: Show database logs on failure + if: failure() + run: docker compose -f tests/docker-compose.yml logs --tail 100 diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 84376ee23d..f3c9ebb6ec 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -5,22 +5,28 @@ services: # mysql_8: + healthcheck: &mysql-health + test: ["CMD", "mysql", "--protocol=TCP", "-h", "127.0.0.1", "-uroot", "-ppassword", "sqlx", "-e", "SELECT 1 FROM tweet LIMIT 0"] + interval: 2s + timeout: 5s + retries: 60 image: mysql:8 volumes: - "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql" ports: - - 3306 + - "127.0.0.1:${SQLX_MYSQL_PORT:-}:3306" environment: MYSQL_ROOT_HOST: '%' MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: sqlx mysql_5_7: + healthcheck: *mysql-health image: mysql:5.7 volumes: - "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql" ports: - - 3306 + - "127.0.0.1:${SQLX_MYSQL_PORT:-}:3306" environment: MYSQL_ROOT_HOST: '%' MYSQL_ROOT_PASSWORD: password @@ -31,41 +37,45 @@ services: # mariadb_10_6: + healthcheck: *mysql-health image: mariadb:10.6 volumes: - "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql" ports: - - 3306 + - "127.0.0.1:${SQLX_MYSQL_PORT:-}:3306" environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: sqlx mariadb_10_5: + healthcheck: *mysql-health image: mariadb:10.5 volumes: - "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql" ports: - - 3306 + - "127.0.0.1:${SQLX_MYSQL_PORT:-}:3306" environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: sqlx mariadb_10_4: + healthcheck: *mysql-health image: mariadb:10.4 volumes: - "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql" ports: - - 3306 + - "127.0.0.1:${SQLX_MYSQL_PORT:-}:3306" environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: sqlx mariadb_10_3: + healthcheck: *mysql-health image: mariadb:10.3 volumes: - "./mysql/setup.sql:/docker-entrypoint-initdb.d/setup.sql" ports: - - 3306 + - "127.0.0.1:${SQLX_MYSQL_PORT:-}:3306" environment: MYSQL_ROOT_PASSWORD: password MYSQL_DATABASE: sqlx @@ -76,6 +86,11 @@ services: # postgres_16: # Test postgres 16 with required client cert + healthcheck: &postgres-health + test: ["CMD", "pg_isready", "-h", "127.0.0.1", "-U", "postgres", "-d", "sqlx"] + interval: 2s + timeout: 5s + retries: 60 build: context: . dockerfile: postgres/Dockerfile @@ -87,21 +102,20 @@ services: POSTGRES_DB: sqlx POSTGRES_HOST_AUTH_METHOD: trust POSTGRES_INITDB_ARGS: --auth-host=trust - healthcheck: - test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"] volumes: - "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql" command: > -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key -c ssl_ca_file=/var/lib/postgresql/ca.crt -c hba_file=/var/lib/postgresql/pg_hba.conf postgres_14: + healthcheck: *postgres-health build: context: . dockerfile: postgres/Dockerfile args: VERSION: 14 ports: - - 5432 + - "127.0.0.1:${SQLX_POSTGRES_PORT:-}:5432" environment: POSTGRES_DB: sqlx POSTGRES_USER: postgres @@ -114,13 +128,14 @@ services: -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key postgres_13: + healthcheck: *postgres-health build: context: . dockerfile: postgres/Dockerfile args: VERSION: 13 ports: - - 5432 + - "127.0.0.1:${SQLX_POSTGRES_PORT:-}:5432" environment: POSTGRES_DB: sqlx POSTGRES_USER: postgres @@ -133,13 +148,14 @@ services: -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key postgres_12: + healthcheck: *postgres-health build: context: . dockerfile: postgres/Dockerfile args: VERSION: 12 ports: - - 5432 + - "127.0.0.1:${SQLX_POSTGRES_PORT:-}:5432" environment: POSTGRES_DB: sqlx POSTGRES_USER: postgres @@ -152,13 +168,14 @@ services: -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key postgres_11: + healthcheck: *postgres-health build: context: . dockerfile: postgres/Dockerfile args: VERSION: 11 ports: - - 5432 + - "127.0.0.1:${SQLX_POSTGRES_PORT:-}:5432" environment: POSTGRES_DB: sqlx POSTGRES_USER: postgres @@ -171,13 +188,14 @@ services: -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key postgres_10: + healthcheck: *postgres-health build: context: . dockerfile: postgres/Dockerfile args: VERSION: 10 ports: - - 5432 + - "127.0.0.1:${SQLX_POSTGRES_PORT:-}:5432" environment: POSTGRES_DB: sqlx POSTGRES_USER: postgres @@ -190,13 +208,14 @@ services: -c ssl=on -c ssl_cert_file=/var/lib/postgresql/server.crt -c ssl_key_file=/var/lib/postgresql/server.key postgres_16_no_ssl: + healthcheck: *postgres-health build: context: . dockerfile: postgres/Dockerfile args: VERSION: 16 ports: - - 5432 + - "127.0.0.1:${SQLX_POSTGRES_PORT:-}:5432" environment: POSTGRES_DB: sqlx POSTGRES_USER: postgres @@ -213,25 +232,36 @@ services: # mssql_2019: + healthcheck: &mssql-health + test: + - CMD-SHELL + - >- + /opt/mssql-tools18/bin/sqlcmd -S tcp:127.0.0.1,1433 + -U sa -P "$$SA_PASSWORD" -d sqlx + -Q 'SELECT TOP 0 * FROM dbo.tweet' -b -C -l 2 + interval: 2s + timeout: 5s + retries: 60 build: context: . dockerfile: mssql/Dockerfile args: VERSION: 2019-latest ports: - - 1433 + - "127.0.0.1:${SQLX_MSSQL_PORT:-}:1433" environment: ACCEPT_EULA: "Y" SA_PASSWORD: Password123! mssql_2022: + healthcheck: *mssql-health build: context: . dockerfile: mssql/Dockerfile args: VERSION: 2022-latest ports: - - 1433 + - "127.0.0.1:${SQLX_MSSQL_PORT:-}:1433" volumes: - "./mssql/mssql.conf:/var/opt/mssql/mssql.conf" environment: diff --git a/tests/mssql/configure-db.sh b/tests/mssql/configure-db.sh index 2d272b1895..02b8ea4ad6 100644 --- a/tests/mssql/configure-db.sh +++ b/tests/mssql/configure-db.sh @@ -1,17 +1,11 @@ #!/usr/bin/env bash -set -euo pipefail # Wait for SQL Server to be ready for connections -deadline=$((SECONDS + 120)) -until /opt/mssql-tools18/bin/sqlcmd -S tcp:127.0.0.1,1433 -U sa -P "$SA_PASSWORD" -d master -Q "SELECT 1;" -No -b -l 2 +until /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -Q "SELECT 1;" -No do - if ((SECONDS >= deadline)); then - echo "SQL Server did not become ready within 120 seconds" >&2 - exit 1 - fi echo "Waiting for SQL Server to be ready..." sleep 1 done # Run the setup script to create the DB and the schema in the DB -/opt/mssql-tools18/bin/sqlcmd -S tcp:127.0.0.1,1433 -U sa -P "$SA_PASSWORD" -d master -i setup.sql -No -b -l 2 +/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -i setup.sql -No diff --git a/tests/test_wait_for_db.py b/tests/test_wait_for_db.py deleted file mode 100644 index 8c0d1f5202..0000000000 --- a/tests/test_wait_for_db.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Exercise readiness retries and failure reporting without a Docker daemon.""" -import os -from pathlib import Path -import subprocess -import sys -import tempfile -import unittest - - -class DatabaseReadinessTests(unittest.TestCase): - def run_wait(self, behavior, driver="postgres", wait_seconds="2"): - with tempfile.TemporaryDirectory() as directory: - root = Path(directory) - docker = root / "docker" - docker.write_text( - f"#!{sys.executable}\n" + ''' -import os -from pathlib import Path -import sys -import time - -state = Path(os.environ["PROBE_STATE"]) -mode = os.environ["PROBE_BEHAVIOR"] -command = sys.argv[1] -if command == "exec": - attempts = int(state.read_text()) if state.exists() else 0 - state.write_text(str(attempts + 1)) - if mode == "hang": - time.sleep(60) - sys.exit(0 if mode == "ready" or (mode == "retry" and attempts > 0) else 1) -if command == "inspect": - print("false" if mode == "stopped" else "true") -if command == "logs": - print("database startup diagnostics") -''' - ) - docker.chmod(0o755) - env = dict(os.environ, PATH=f"{root}{os.pathsep}{os.environ['PATH']}", - PROBE_STATE=str(root / "attempts"), PROBE_BEHAVIOR=behavior) - result = subprocess.run( - ["bash", str(Path(__file__).with_name("wait-for-db.sh")), - "test-container", driver, wait_seconds], - env=env, text=True, capture_output=True, timeout=8, - ) - attempts = int((root / "attempts").read_text()) if (root / "attempts").exists() else 0 - return result, attempts - - def test_supported_drivers_succeed_when_ready(self): - for driver in ("postgres", "mysql", "mssql"): - with self.subTest(driver=driver): - result, attempts = self.run_wait("ready", driver) - self.assertEqual(result.returncode, 0, result.stderr) - self.assertEqual(attempts, 1) - - def test_retries_until_schema_is_ready(self): - result, attempts = self.run_wait("retry", wait_seconds="5") - self.assertEqual(result.returncode, 0, result.stderr) - self.assertGreater(attempts, 1) - - def test_stopped_container_fails_with_logs(self): - result, attempts = self.run_wait("stopped", wait_seconds="60") - self.assertNotEqual(result.returncode, 0) - self.assertEqual(attempts, 1) - self.assertIn("database startup diagnostics", result.stderr) - - def test_hung_probe_is_bounded_and_reports_logs(self): - result, _ = self.run_wait("hang", wait_seconds="1") - self.assertNotEqual(result.returncode, 0) - self.assertIn("database startup diagnostics", result.stderr) - - def test_invalid_arguments_never_probe_database(self): - for driver, wait in (("unknown", "2"), ("postgres", "0")): - with self.subTest(driver=driver, wait=wait): - result, attempts = self.run_wait("ready", driver, wait) - self.assertEqual(result.returncode, 2) - self.assertEqual(attempts, 0) - - -if __name__ == "__main__": - unittest.main() diff --git a/tests/wait-for-db.sh b/tests/wait-for-db.sh deleted file mode 100644 index 9a24db8f96..0000000000 --- a/tests/wait-for-db.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -container=${1:?usage: wait-for-db.sh CONTAINER postgres|mysql|mssql [TIMEOUT_SECONDS]} -driver=${2:?missing database driver} -wait_seconds=${3:-120} -if [[ ! $wait_seconds =~ ^[1-9][0-9]*$ ]]; then - echo "Timeout must be a positive number of seconds" >&2 - exit 2 -fi - -# Check the test schema over TCP, not just the server process or temporary -# initialization socket. SQL Server creates its schema in a background script. -case "$driver" in - postgres) - probe=(env PGPASSWORD=password psql -h 127.0.0.1 -U postgres -d sqlx - -v ON_ERROR_STOP=1 -c 'SELECT 1 FROM tweet LIMIT 0') - ;; - mysql) - probe=(sh -c ' - client=$(command -v mariadb || command -v mysql) - exec "$client" --protocol=TCP -h 127.0.0.1 -u root -ppassword -D sqlx \ - -e "SELECT 1 FROM tweet LIMIT 0" - ') - ;; - mssql) - probe=(bash -c ' - sqlcmd=/opt/mssql-tools18/bin/sqlcmd - if [[ ! -x "$sqlcmd" ]]; then sqlcmd=/opt/mssql-tools/bin/sqlcmd; fi - exec "$sqlcmd" -S tcp:127.0.0.1,1433 -U sa -P "$SA_PASSWORD" -d sqlx \ - -Q "SELECT TOP 0 * FROM dbo.tweet" -b -C -l 2 - ') - ;; - *) - echo "Unsupported database driver: $driver" >&2 - exit 2 - ;; -esac - -deadline=$((SECONDS + wait_seconds)) -while ((SECONDS < deadline)); do - remaining=$((deadline - SECONDS)) - if ((remaining <= 0)); then break; fi - if ((remaining > 5)); then remaining=5; fi - if timeout --foreground "${remaining}s" docker exec "$container" "${probe[@]}" >/dev/null 2>&1; then - echo "$container test schema is ready" - exit 0 - fi - if [[ $(timeout --foreground 5s docker inspect --format '{{.State.Running}}' "$container" 2>/dev/null) != true ]]; then - break - fi - sleep 1 -done - -echo "$container did not become ready within ${wait_seconds}s (or stopped)" >&2 -timeout --foreground 5s docker logs --tail 100 "$container" >&2 || true -exit 1