Skip to content

Commit 01aed7f

Browse files
authored
ci: wait for database test schemas with bounded probes
1 parent 0dad86d commit 01aed7f

5 files changed

Lines changed: 162 additions & 12 deletions

File tree

‎.github/workflows/sqlx.yml‎

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ on:
77
- main
88

99
jobs:
10+
test-infrastructure:
11+
name: Test infrastructure
12+
runs-on: ubuntu-24.04
13+
steps:
14+
- uses: actions/checkout@v6
15+
- run: python3 -m unittest discover -s tests -p 'test_wait_for_db.py'
16+
1017
format:
1118
name: Format
1219
runs-on: ubuntu-24.04
@@ -168,7 +175,7 @@ jobs:
168175

169176
- run: |
170177
docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_${{ matrix.postgres }} postgres_${{ matrix.postgres }}
171-
docker exec postgres_${{ matrix.postgres }} bash -c "until pg_isready; do sleep 1; done"
178+
bash tests/wait-for-db.sh postgres_${{ matrix.postgres }} postgres
172179
173180
- run: |
174181
cargo test --no-default-features --features any,postgres,macros,all-types,${{ matrix.tls }}
@@ -197,7 +204,7 @@ jobs:
197204
prefix-key: v1-sqlx
198205
shared-key: postgres-rustls
199206
save-if: ${{ false }}
200-
- run: docker compose up --wait postgres_16
207+
- run: docker compose up --wait --wait-timeout 120 postgres_16
201208
working-directory: tests
202209
- run: cargo test --no-default-features --features any,postgres,macros,all-types,rustls
203210
env:
@@ -230,8 +237,8 @@ jobs:
230237
--features mysql,all-types,${{ matrix.tls }},macros,migrate \
231238
-- -D warnings
232239
233-
- run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 mysql_${{ matrix.mysql }}
234-
- run: sleep 60
240+
- run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mysql_${{ matrix.mysql }} mysql_${{ matrix.mysql }}
241+
- run: bash tests/wait-for-db.sh mysql_${{ matrix.mysql }} mysql
235242

236243
- run: |
237244
cargo test --no-default-features --features any,mysql,macros,migrate,all-types,${{ matrix.tls }}
@@ -272,8 +279,8 @@ jobs:
272279
--features mysql,${{ matrix.tls }},macros,migrate \
273280
-- -D warnings
274281
275-
- run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 mariadb_${{ matrix.mariadb }}
276-
- run: sleep 30
282+
- run: docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mariadb_${{ matrix.mariadb }} mariadb_${{ matrix.mariadb }}
283+
- run: bash tests/wait-for-db.sh mariadb_${{ matrix.mariadb }} mysql
277284

278285
- run: |
279286
cargo test --no-default-features --features any,mysql,macros,migrate,${{ matrix.tls }}
@@ -307,8 +314,8 @@ jobs:
307314
--features mssql,all-types,${{ matrix.tls }},macros,migrate \
308315
-- -D warnings
309316
310-
- run: docker compose -f tests/docker-compose.yml run -d -p 1433:1433 mssql_${{ matrix.mssql }}
311-
- run: sleep 80 # MSSQL takes a "bit" to startup
317+
- run: docker compose -f tests/docker-compose.yml run -d -p 1433:1433 --name mssql_${{ matrix.mssql }} mssql_${{ matrix.mssql }}
318+
- run: bash tests/wait-for-db.sh mssql_${{ matrix.mssql }} mssql
312319

313320
- run: |
314321
cargo test --no-default-features --features any,mssql,macros,migrate,all-types,${{ matrix.tls }}
@@ -330,7 +337,7 @@ jobs:
330337
- name: Start Postgres (no SSL)
331338
run: |
332339
docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_16_no_ssl postgres_16_no_ssl
333-
docker exec postgres_16_no_ssl bash -c "until pg_isready; do sleep 1; done"
340+
bash tests/wait-for-db.sh postgres_16_no_ssl postgres
334341
- name: Install unixODBC and ODBC drivers (PostgreSQL, SQLite)
335342
run: |
336343
sudo apt-get update

‎tests/docker-compose.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ services:
8888
POSTGRES_HOST_AUTH_METHOD: trust
8989
POSTGRES_INITDB_ARGS: --auth-host=trust
9090
healthcheck:
91-
test: ["CMD-SHELL", "pg_isready -U postgres"]
91+
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U postgres"]
9292
volumes:
9393
- "./postgres/setup.sql:/docker-entrypoint-initdb.d/setup.sql"
9494
command: >

‎tests/mssql/configure-db.sh‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
#!/usr/bin/env bash
2+
set -euo pipefail
23

34
# Wait for SQL Server to be ready for connections
4-
until /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -Q "SELECT 1;" -No
5+
deadline=$((SECONDS + 120))
6+
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
57
do
8+
if ((SECONDS >= deadline)); then
9+
echo "SQL Server did not become ready within 120 seconds" >&2
10+
exit 1
11+
fi
612
echo "Waiting for SQL Server to be ready..."
713
sleep 1
814
done
915

1016
# Run the setup script to create the DB and the schema in the DB
11-
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -i setup.sql -No
17+
/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

‎tests/test_wait_for_db.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Exercise readiness retries and failure reporting without a Docker daemon."""
2+
import os
3+
from pathlib import Path
4+
import subprocess
5+
import sys
6+
import tempfile
7+
import unittest
8+
9+
10+
class DatabaseReadinessTests(unittest.TestCase):
11+
def run_wait(self, behavior, driver="postgres", wait_seconds="2"):
12+
with tempfile.TemporaryDirectory() as directory:
13+
root = Path(directory)
14+
docker = root / "docker"
15+
docker.write_text(
16+
f"#!{sys.executable}\n" + '''
17+
import os
18+
from pathlib import Path
19+
import sys
20+
import time
21+
22+
state = Path(os.environ["PROBE_STATE"])
23+
mode = os.environ["PROBE_BEHAVIOR"]
24+
command = sys.argv[1]
25+
if command == "exec":
26+
attempts = int(state.read_text()) if state.exists() else 0
27+
state.write_text(str(attempts + 1))
28+
if mode == "hang":
29+
time.sleep(60)
30+
sys.exit(0 if mode == "ready" or (mode == "retry" and attempts > 0) else 1)
31+
if command == "inspect":
32+
print("false" if mode == "stopped" else "true")
33+
if command == "logs":
34+
print("database startup diagnostics")
35+
'''
36+
)
37+
docker.chmod(0o755)
38+
env = dict(os.environ, PATH=f"{root}{os.pathsep}{os.environ['PATH']}",
39+
PROBE_STATE=str(root / "attempts"), PROBE_BEHAVIOR=behavior)
40+
result = subprocess.run(
41+
["bash", str(Path(__file__).with_name("wait-for-db.sh")),
42+
"test-container", driver, wait_seconds],
43+
env=env, text=True, capture_output=True, timeout=8,
44+
)
45+
attempts = int((root / "attempts").read_text()) if (root / "attempts").exists() else 0
46+
return result, attempts
47+
48+
def test_supported_drivers_succeed_when_ready(self):
49+
for driver in ("postgres", "mysql", "mssql"):
50+
with self.subTest(driver=driver):
51+
result, attempts = self.run_wait("ready", driver)
52+
self.assertEqual(result.returncode, 0, result.stderr)
53+
self.assertEqual(attempts, 1)
54+
55+
def test_retries_until_schema_is_ready(self):
56+
result, attempts = self.run_wait("retry", wait_seconds="5")
57+
self.assertEqual(result.returncode, 0, result.stderr)
58+
self.assertGreater(attempts, 1)
59+
60+
def test_stopped_container_fails_with_logs(self):
61+
result, attempts = self.run_wait("stopped", wait_seconds="60")
62+
self.assertNotEqual(result.returncode, 0)
63+
self.assertEqual(attempts, 1)
64+
self.assertIn("database startup diagnostics", result.stderr)
65+
66+
def test_hung_probe_is_bounded_and_reports_logs(self):
67+
result, _ = self.run_wait("hang", wait_seconds="1")
68+
self.assertNotEqual(result.returncode, 0)
69+
self.assertIn("database startup diagnostics", result.stderr)
70+
71+
def test_invalid_arguments_never_probe_database(self):
72+
for driver, wait in (("unknown", "2"), ("postgres", "0")):
73+
with self.subTest(driver=driver, wait=wait):
74+
result, attempts = self.run_wait("ready", driver, wait)
75+
self.assertEqual(result.returncode, 2)
76+
self.assertEqual(attempts, 0)
77+
78+
79+
if __name__ == "__main__":
80+
unittest.main()

‎tests/wait-for-db.sh‎

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
container=${1:?usage: wait-for-db.sh CONTAINER postgres|mysql|mssql [TIMEOUT_SECONDS]}
5+
driver=${2:?missing database driver}
6+
wait_seconds=${3:-120}
7+
if [[ ! $wait_seconds =~ ^[1-9][0-9]*$ ]]; then
8+
echo "Timeout must be a positive number of seconds" >&2
9+
exit 2
10+
fi
11+
12+
# Check the test schema over TCP, not just the server process or temporary
13+
# initialization socket. SQL Server creates its schema in a background script.
14+
case "$driver" in
15+
postgres)
16+
probe=(env PGPASSWORD=password psql -h 127.0.0.1 -U postgres -d sqlx
17+
-v ON_ERROR_STOP=1 -c 'SELECT 1 FROM tweet LIMIT 0')
18+
;;
19+
mysql)
20+
probe=(sh -c '
21+
client=$(command -v mariadb || command -v mysql)
22+
exec "$client" --protocol=TCP -h 127.0.0.1 -u root -ppassword -D sqlx \
23+
-e "SELECT 1 FROM tweet LIMIT 0"
24+
')
25+
;;
26+
mssql)
27+
probe=(bash -c '
28+
sqlcmd=/opt/mssql-tools18/bin/sqlcmd
29+
if [[ ! -x "$sqlcmd" ]]; then sqlcmd=/opt/mssql-tools/bin/sqlcmd; fi
30+
exec "$sqlcmd" -S tcp:127.0.0.1,1433 -U sa -P "$SA_PASSWORD" -d sqlx \
31+
-Q "SELECT TOP 0 * FROM dbo.tweet" -b -C -l 2
32+
')
33+
;;
34+
*)
35+
echo "Unsupported database driver: $driver" >&2
36+
exit 2
37+
;;
38+
esac
39+
40+
deadline=$((SECONDS + wait_seconds))
41+
while ((SECONDS < deadline)); do
42+
remaining=$((deadline - SECONDS))
43+
if ((remaining <= 0)); then break; fi
44+
if ((remaining > 5)); then remaining=5; fi
45+
if timeout --foreground "${remaining}s" docker exec "$container" "${probe[@]}" >/dev/null 2>&1; then
46+
echo "$container test schema is ready"
47+
exit 0
48+
fi
49+
if [[ $(timeout --foreground 5s docker inspect --format '{{.State.Running}}' "$container" 2>/dev/null) != true ]]; then
50+
break
51+
fi
52+
sleep 1
53+
done
54+
55+
echo "$container did not become ready within ${wait_seconds}s (or stopped)" >&2
56+
timeout --foreground 5s docker logs --tail 100 "$container" >&2 || true
57+
exit 1

0 commit comments

Comments
 (0)