Skip to content

Commit 5149976

Browse files
authored
test: isolate local database runs and preserve user configuration
1 parent 01aed7f commit 5149976

4 files changed

Lines changed: 192 additions & 31 deletions

File tree

‎.github/workflows/sqlx.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
runs-on: ubuntu-24.04
1313
steps:
1414
- uses: actions/checkout@v6
15-
- run: python3 -m unittest discover -s tests -p 'test_wait_for_db.py'
15+
- run: python3 -m unittest discover -s tests -p 'test_*.py'
1616

1717
format:
1818
name: Format

‎test.sh‎

Lines changed: 59 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,64 @@
1-
#!/bin/sh
2-
docker compose -f tests/docker-compose.yml run -it -p 5432:5432 --name postgres_16 postgres_16
3-
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" cargo test --features any,postgres,macros,all-types,rustls --
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
cd -- "$(dirname -- "${BASH_SOURCE[0]}")"
44

5-
docker compose -f tests/docker-compose.yml run -it -p 1433:1433 --rm --name mssql_2022 mssql_2022
6-
DATABASE_URL='mssql://sa:Password123!@localhost/sqlx' cargo test --features any,mssql,macros,all-types,rustls --
5+
backend=${1:-all}
6+
if [[ $backend == all ]]; then
7+
for backend in postgres mssql mysql sqlite odbc; do
8+
bash ./test.sh "$backend"
9+
done
10+
exit 0
11+
fi
712

8-
docker compose -f tests/docker-compose.yml run -it -p 3306:3306 --name mysql_8 mysql_8
9-
DATABASE_URL='mysql://root:password@localhost/sqlx' cargo test --features any,mysql,macros,all-types,rustls --
13+
features="any,macros,all-types,rustls"
14+
case "$backend" in
15+
sqlite)
16+
sqlite_dir=$(mktemp -d "${TMPDIR:-/tmp}/sqlx-local-sqlite.XXXXXX")
17+
trap 'rm -rf -- "$sqlite_dir"' EXIT
18+
cp tests/sqlite/sqlite.db "$sqlite_dir/sqlite.db"
19+
export DATABASE_URL="sqlite://$sqlite_dir/sqlite.db"
20+
cargo test --locked --no-default-features --features "$features,sqlite,migrate"
21+
exit
22+
;;
23+
postgres) service=postgres_14; port=5432; driver=postgres ;;
24+
mysql) service=mysql_8; port=3306; driver=mysql ;;
25+
mssql) service=mssql_2022; port=1433; driver=mssql ;;
26+
odbc) service=postgres_16_no_ssl; port=5432; driver=postgres ;;
27+
*)
28+
echo "Usage: $0 [all|sqlite|postgres|mysql|mssql|odbc]" >&2
29+
exit 2
30+
;;
31+
esac
1032

11-
DATABASE_URL='sqlite://./tests/sqlite/sqlite.db' cargo test --features any,sqlite,macros,all-types,rustls --
33+
project="sqlx-local-test-$$-$RANDOM"
34+
cleanup() {
35+
status=$?
36+
docker compose -p "$project" -f tests/docker-compose.yml \
37+
down --remove-orphans >/dev/null 2>&1 || true
38+
exit "$status"
39+
}
40+
trap cleanup EXIT
1241

42+
# Each invocation owns one disposable container and a random loopback-only port.
43+
container=$(docker compose -p "$project" -f tests/docker-compose.yml \
44+
run --rm -d -p "127.0.0.1::$port" "$service")
45+
bash tests/wait-for-db.sh "$container" "$driver"
46+
address=$(docker port "$container" "$port/tcp")
47+
host_port=${address##*:}
48+
if [[ ! $host_port =~ ^[0-9]+$ ]]; then
49+
echo "Could not determine the database's published port: $address" >&2
50+
exit 1
51+
fi
1352

14-
# Copy odbc config from tests/odbc.ini to ~/.odbc.ini and run ODBC tests against Postgres
15-
cp tests/odbc.ini ~/.odbc.ini
16-
docker compose -f tests/docker-compose.yml run -p 5432:5432 --name postgres_16_no_ssl -it postgres_16_no_ssl
17-
DATABASE_URL='DSN=SNOWFLAKE' cargo test --no-default-features --features any,odbc,all-types,macros,rustls
53+
case "$backend" in
54+
postgres) DATABASE_URL="postgres://postgres:password@127.0.0.1:$host_port/sqlx" ;;
55+
mysql) DATABASE_URL="mysql://root:password@127.0.0.1:$host_port/sqlx" ;;
56+
mssql) DATABASE_URL="mssql://sa:Password123!@127.0.0.1:$host_port/sqlx" ;;
57+
odbc)
58+
# Use only the container we started; do not overwrite ~/.odbc.ini or use a saved DSN.
59+
DATABASE_URL="Driver={PostgreSQL Unicode};Servername=127.0.0.1;Port=$host_port;Database=sqlx;Uid=postgres;Pwd=password"
60+
;;
61+
esac
62+
export DATABASE_URL
63+
if [[ $backend != odbc ]]; then features+=",migrate"; fi
64+
cargo test --locked --no-default-features --features "$features,$backend"

‎tests/README.md‎

Lines changed: 38 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,38 @@
1-
2-
3-
### Running Tests
4-
SQLx uses docker to run many compatible database systems for integration testing. You'll need to [install docker](https://docs.docker.com/engine/) to run the full suite. You can validate your docker installation with:
5-
6-
$ docker run hello-world
7-
8-
Start the databases with `docker-compose` before running tests:
9-
10-
$ docker-compose up
11-
12-
Run all tests against all supported databases using:
13-
14-
$ ./x.py
15-
16-
If you see test failures, or want to run a more specific set of tests against a specific database, you can specify both the features to be tests and the DATABASE_URL. e.g.
17-
18-
$ DATABASE_URL=mysql://root:password@127.0.0.1:49183/sqlx cargo test --no-default-features --features macros,offline,any,all-types,mysql,native-tls
1+
# Running tests
2+
3+
Install Rust through rustup and [Docker Compose](https://docs.docker.com/compose/install/).
4+
Run the local test script from any directory:
5+
6+
```sh
7+
./test.sh # all backends, stopping at the first failure
8+
./test.sh sqlite # SQLite only; no Docker needed
9+
./test.sh postgres # PostgreSQL 14
10+
./test.sh mysql # MySQL 8
11+
./test.sh mssql # SQL Server 2022
12+
./test.sh odbc # ODBC against local PostgreSQL 16
13+
```
14+
15+
Each database run starts a disposable container, waits for its test schema, and
16+
publishes a randomly assigned port on loopback. The script uses that container's
17+
connection string and removes it when the test command exits, including failures.
18+
SQLite runs against a temporary copy of its fixture database.
19+
The script does not use an existing `DATABASE_URL`, a saved ODBC DSN, or modify `~/.odbc.ini`.
20+
The ODBC run requires unixODBC and the **PostgreSQL Unicode** driver on the host.
21+
22+
The PostgreSQL client-certificate authentication configuration is tested separately
23+
by the **Postgres with SSL client cert** CI job.
24+
25+
For custom databases, set `DATABASE_URL` explicitly when invoking Cargo. These tests
26+
create and modify database objects: use a disposable test database.
27+
28+
```sh
29+
DATABASE_URL=mysql://root:password@127.0.0.1:3306/sqlx \
30+
cargo test --locked --no-default-features \
31+
--features macros,offline,any,all-types,mysql,native-tls
32+
```
33+
34+
The test-runner and readiness regression tests need only Python and Bash:
35+
36+
```sh
37+
python3 -m unittest discover -s tests -p 'test_*.py'
38+
```

‎tests/test_local_runner.py‎

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
"""Check local test isolation and cleanup without running databases or Cargo."""
2+
import json
3+
import os
4+
from pathlib import Path
5+
import subprocess
6+
import sys
7+
import tempfile
8+
import unittest
9+
10+
11+
class LocalRunnerTests(unittest.TestCase):
12+
def run_backend(self, backend, cargo_status=0, docker_status=0):
13+
with tempfile.TemporaryDirectory() as directory:
14+
root = Path(directory)
15+
log = root / "commands.jsonl"
16+
program = f"#!{sys.executable}\n" + '''
17+
import json
18+
import os
19+
from pathlib import Path
20+
import sys
21+
22+
name = Path(sys.argv[0]).name
23+
with open(os.environ["COMMAND_LOG"], "a") as log:
24+
log.write(json.dumps([name, sys.argv[1:], os.environ.get("DATABASE_URL")]) + "\\n")
25+
if name == "cargo":
26+
sys.exit(int(os.environ["CARGO_STATUS"]))
27+
if sys.argv[1] == "compose":
28+
if int(os.environ["DOCKER_STATUS"]):
29+
sys.exit(int(os.environ["DOCKER_STATUS"]))
30+
print("owned-container")
31+
elif sys.argv[1] == "port":
32+
print("127.0.0.1:49123")
33+
elif sys.argv[1] == "inspect":
34+
print("true")
35+
'''
36+
for name in ("docker", "cargo"):
37+
tool = root / name
38+
tool.write_text(program)
39+
tool.chmod(0o755)
40+
# A user's saved DSN must neither be overwritten nor selected.
41+
config = root / ".odbc.ini"
42+
config.write_text("[SNOWFLAKE]\nDatabase=production\n")
43+
env = dict(os.environ, PATH=f"{root}{os.pathsep}{os.environ['PATH']}",
44+
COMMAND_LOG=str(log), CARGO_STATUS=str(cargo_status),
45+
DOCKER_STATUS=str(docker_status), ODBCINI=str(config),
46+
DATABASE_URL="DSN=SNOWFLAKE")
47+
result = subprocess.run(
48+
["bash", str(Path(__file__).resolve().parent.parent / "test.sh"), backend],
49+
cwd=root, env=env, text=True, capture_output=True, timeout=10,
50+
)
51+
self.assertEqual(config.read_text(), "[SNOWFLAKE]\nDatabase=production\n")
52+
commands = [json.loads(line) for line in log.read_text().splitlines()] if log.exists() else []
53+
return result, commands
54+
55+
def test_database_backends_use_owned_containers_and_random_loopback_ports(self):
56+
for backend in ("postgres", "mysql", "mssql", "odbc"):
57+
with self.subTest(backend=backend):
58+
result, commands = self.run_backend(backend)
59+
self.assertEqual(result.returncode, 0, result.stderr)
60+
compose = next(args for name, args, _ in commands if name == "docker" and args[0] == "compose")
61+
self.assertIn("-d", compose)
62+
self.assertIn("--rm", compose)
63+
self.assertTrue(compose[compose.index("-p", compose.index("run")) + 1].startswith("127.0.0.1::"))
64+
cargo = next(command for command in commands if command[0] == "cargo")
65+
self.assertIn("--locked", cargo[1])
66+
self.assertIn("49123", cargo[2])
67+
self.assertNotIn("DSN=", cargo[2])
68+
self.assertEqual(commands[-1][1][-2:], ["down", "--remove-orphans"])
69+
70+
def test_test_failure_is_preserved_after_cleanup(self):
71+
result, commands = self.run_backend("odbc", cargo_status=17)
72+
self.assertEqual(result.returncode, 17)
73+
self.assertEqual(commands[-1][1][-2:], ["down", "--remove-orphans"])
74+
75+
def test_failed_container_start_never_runs_tests(self):
76+
result, commands = self.run_backend("postgres", docker_status=19)
77+
self.assertEqual(result.returncode, 19)
78+
self.assertFalse(any(name == "cargo" for name, _, _ in commands))
79+
80+
def test_sqlite_does_not_start_docker(self):
81+
result, commands = self.run_backend("sqlite")
82+
self.assertEqual(result.returncode, 0, result.stderr)
83+
self.assertEqual([name for name, _, _ in commands], ["cargo"])
84+
self.assertTrue(commands[0][2].startswith("sqlite:"))
85+
self.assertFalse(Path(commands[0][2].removeprefix("sqlite://")).exists())
86+
87+
def test_invalid_backend_does_not_start_anything(self):
88+
result, commands = self.run_backend("production")
89+
self.assertEqual(result.returncode, 2)
90+
self.assertEqual(commands, [])
91+
92+
93+
if __name__ == "__main__":
94+
unittest.main()

0 commit comments

Comments
 (0)