Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/sqlx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -514,3 +514,58 @@ jobs:
env:
DATABASE_URL: mysql://root@localhost:3306/sqlx?sslmode=verify_ca&ssl-ca=.%2Ftests%2Fcerts%2Fca.crt&ssl-key=.%2Ftests%2Fcerts%2Fkeys%2Fclient.key&ssl-cert=.%2Ftests%2Fcerts%2Fclient.crt
RUSTFLAGS: --cfg mariadb="${{ matrix.mariadb }}"

mixed:
name: Mixed in Postgres and MySQL
runs-on: ubuntu-24.04
strategy:
matrix:
postgres: [ 17, 13 ]
mysql: [ 8 ]
runtime: [ async-global-executor, smol, tokio ]
steps:
- uses: actions/checkout@v4

- name: Setup Rust
run: rustup show active-toolchain || rustup toolchain install

- uses: Swatinem/rust-cache@v2

- run: cargo build --features any,postgres,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }}

- run: |
docker compose -f tests/docker-compose.yml run -d -p 5432:5432 --name postgres_1_${{ matrix.postgres }} postgres_${{ matrix.postgres }}
docker exec postgres_1_${{ matrix.postgres }} bash -c "until pg_isready; do sleep 1; done"

- run: |
docker compose -f tests/docker-compose.yml run -d -p 5433:5432 --name postgres_2_${{ matrix.postgres }} postgres_${{ matrix.postgres }}
docker exec postgres_2_${{ matrix.postgres }} bash -c "until pg_isready; do sleep 1; done"

- run: |
docker compose -f tests/docker-compose.yml run -d -p 3306:3306 --name mysql_1_${{ matrix.mysql }} mysql_${{ matrix.mysql }}
docker exec mysql_1_${{ matrix.mysql }} bash -c "until mysqladmin ping; do sleep 1; done"

- run: |
docker compose -f tests/docker-compose.yml run -d -p 3307:3306 --name mysql_2_${{ matrix.mysql }} mysql_${{ matrix.mysql }}
docker exec mysql_2_${{ matrix.mysql }} bash -c "until mysqladmin ping; do sleep 1; done"

# Create data dir for offline mode
- run: mkdir .sqlx

# Run the `test-attr` test again to cover cleanup.
- run: >
cargo test
--test mixed-test-attr
--no-default-features
--features any,postgres,mysql,macros,migrate,_unstable-all-types,runtime-${{ matrix.runtime }}
env:
DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx
PG_1_DATABASE_URL: postgres://postgres:password@localhost:5432/sqlx
PG_2_DATABASE_URL: postgres://postgres:password@localhost:5433/sqlx
MYSQL_1_DATABASE_URL: mysql://root:password@localhost:3306/sqlx
MYSQL_2_DATABASE_URL: mysql://root:password@localhost:3307/sqlx
SQLX_OFFLINE_DIR: .sqlx
RUSTFLAGS: -D warnings --cfg postgres="${{ matrix.postgres }}" --cfg mysql_${{ matrix.mysql }}

# Remove test artifacts
- run: cargo clean -p sqlx
8 changes: 8 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -453,3 +453,11 @@ required-features = ["postgres"]
name = "postgres-rustsec"
path = "tests/postgres/rustsec.rs"
required-features = ["postgres", "macros", "migrate"]

#
# Mixed in Postgres and MySQL
#
[[test]]
name = "mixed-test-attr"
path = "tests/mixed/test-attr.rs"
required-features = ["postgres", "mysql", "macros", "migrate"]
245 changes: 243 additions & 2 deletions sqlx-core/src/testing/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub trait TestSupport: Database {
args: &TestArgs,
) -> impl Future<Output = Result<TestContext<Self>, Error>> + Send + '_;

fn cleanup_test(db_name: &str) -> impl Future<Output = Result<(), Error>> + Send + '_;
fn cleanup_test(args: &TestArgs) -> impl Future<Output = Result<(), Error>> + Send + '_;

/// Cleanup any test databases that are no longer in-use.
///
Expand All @@ -38,6 +38,13 @@ pub trait TestSupport: Database {
/// The user credentials it contains must have the privilege to create and drop databases.
fn cleanup_test_dbs() -> impl Future<Output = Result<Option<usize>, Error>> + Send + 'static;

/// Cleanup any test databases that are no longer in-use.
///
/// Returns a count of the databases deleted, if possible.
fn cleanup_test_dbs_by_url(
url: &str,
) -> impl Future<Output = Result<Option<usize>, Error>> + Send + '_;

/// Take a snapshot of the current state of the database (data only).
///
/// This snapshot can then be used to generate test fixtures.
Expand Down Expand Up @@ -66,6 +73,7 @@ pub struct TestArgs {
pub test_path: &'static str,
pub migrator: Option<&'static Migrator>,
pub fixtures: &'static [TestFixture],
pub database_url_var: &'static str,
}

pub trait TestFn {
Expand Down Expand Up @@ -158,16 +166,25 @@ impl TestArgs {
test_path,
migrator: None,
fixtures: &[],
database_url_var: "DATABASE_URL",
}
}

pub fn migrator(&mut self, migrator: &'static Migrator) {
self.migrator = Some(migrator);
}

pub fn no_migrator(&mut self) {
self.migrator = None;
}

pub fn fixtures(&mut self, fixtures: &'static [TestFixture]) {
self.fixtures = fixtures;
}

pub fn database_url_var(&mut self, database_url_var: &'static str) {
self.database_url_var = database_url_var;
}
}

impl TestTermination for () {
Expand Down Expand Up @@ -231,7 +248,7 @@ where
let res = test_fn(test_context.pool_opts, test_context.connect_opts).await;

if res.is_success() {
if let Err(e) = DB::cleanup_test(&DB::db_name(&args)).await {
if let Err(e) = DB::cleanup_test(&args).await {
eprintln!(
"failed to delete database {:?}: {}",
test_context.db_name, e
Expand Down Expand Up @@ -273,3 +290,227 @@ async fn setup_test_db<DB: Database>(
.await
.expect("failed to close setup connection");
}

macro_rules! impl_test_fn {
(
$name:ident;
$run_fn:ident;
$run_with_pool_fn:ident;
$(
(
$lt:lifetime $db:ident,
$args:ident, $testctx:ident, $testpath:ident,
$poolopts:ident, $connopts:ident,
$pool:ident, $conn:ident
),
)*;
) => {
pub trait $name {
type Output;

fn run_test(self, $($args: TestArgs,)*) -> Self::Output;
}

impl<$($db,)* Fut> $name for fn($(Pool<$db>,)*) -> Fut
where
$(
$db: TestSupport + Database,
$db::Connection: Migrate,
for<$lt> &$lt mut $db::Connection: Executor<$lt, Database = $db>,
)*
Fut: Future,
Fut::Output: TestTermination,
{
type Output = Fut::Output;

fn run_test(self, $($args: TestArgs,)*) -> Self::Output {
$run_with_pool_fn($($args,)* self)
}
}

impl<$($db,)* Fut> $name for fn($(PoolConnection<$db>,)*) -> Fut
where
$(
$db: TestSupport + Database,
$db::Connection: Migrate,
for<$lt> &$lt mut $db::Connection: Executor<$lt, Database = $db>,
)*
Fut: Future,
Fut::Output: TestTermination,
{
type Output = Fut::Output;

fn run_test(self, $($args: TestArgs,)*) -> Self::Output {
$run_with_pool_fn($($args,)* |$($pool,)*| async move {
$(
let $conn = $pool
.acquire()
.await
.expect("failed to acquire test pool connection");
)*


let res = (self)($($conn,)*).await;

$(
$pool.close().await;
)*

res
})
}
}

impl<$($db,)* Fut> $name
for fn(
$(
(PoolOptions<$db>, <$db::Connection as Connection>::Options),
)*
) -> Fut
where
$(
$db: TestSupport + Database,
$db::Connection: Migrate,
for<$lt> &$lt mut $db::Connection: Executor<$lt, Database = $db>,
)*
Fut: Future,
Fut::Output: TestTermination,
{
type Output = Fut::Output;

fn run_test(self, $($args: TestArgs,)*) -> Self::Output {
$run_fn($($args,)* self)
}
}

impl<Fut> $name for fn() -> Fut
where
Fut: Future,
{
type Output = Fut::Output;

fn run_test(self, $($args: TestArgs,)*) -> Self::Output {
$(
assert!(
$args.fixtures.is_empty(),
"fixtures cannot be applied for a bare function",
);
)*
crate::rt::test_block_on(self())
}
}

fn $run_with_pool_fn<$($db,)* F, Fut>($($args: TestArgs,)* test_fn: F) -> Fut::Output
where
$(
$db: TestSupport,
$db::Connection: Migrate,
for<$lt> &$lt mut $db::Connection: Executor<$lt, Database = $db>,
)*
F: FnOnce($(Pool<$db>,)*) -> Fut,
Fut: Future,
Fut::Output: TestTermination,
{
$(
let $testpath: &'static str = $args.test_path;
)*

$run_fn::<$($db,)* _, _>(
$($args,)*
|$(($poolopts, $connopts),)*| async move {
$(
let $pool = $poolopts
.connect_with($connopts)
.await
.expect("failed to connect test pool");
)*

let res = test_fn($($pool.clone(),)*).await;

$(
let close_timed_out = crate::rt::timeout(Duration::from_secs(10), $pool.close())
.await
.is_err();
if close_timed_out {
eprintln!("test {} held onto Pool after exiting", $testpath);
}
)*

res
},
)
}

fn $run_fn<$($db,)* F, Fut>($($args: TestArgs,)* test_fn: F) -> Fut::Output
where
$(
$db: TestSupport,
$db::Connection: Migrate,
for<$lt> &$lt mut $db::Connection: Executor<$lt, Database = $db>,
)*
F: FnOnce(
$((PoolOptions<$db>, <$db::Connection as Connection>::Options),)*
) -> Fut,
Fut: Future,
Fut::Output: TestTermination,
{
crate::rt::test_block_on(async move {
$(
let $testctx = $db::test_context(&$args)
.await
.expect("failed to connect to setup test database");
setup_test_db::<$db>(&$testctx.connect_opts, &$args).await;
)*

let res = test_fn(
$(($testctx.pool_opts, $testctx.connect_opts),)*
)
.await;

if res.is_success() {
$(
if let Err(e) = $db::cleanup_test(&$args).await {
eprintln!(
"failed to delete database {:?}: {}",
$testctx.db_name, e
);
}
)*
}

res
})
}

};
}

impl_test_fn!(
TestFn2;
run_test2;
run_test_with_pool2;
('c DB1, args1, tc1, tp1, po1, co1, p1, c1),
('d DB2, args2, tc2, tp2, po2, co2, p2, c2),
;
);

impl_test_fn!(
TestFn3;
run_test3;
run_test_with_pool3;
('c DB1, args1, tc1, tp1, po1, co1, p1, c1),
('d DB2, args2, tc2, tp2, po2, co2, p2, c2),
('d DB3, args3, tc3, tp3, po3, co3, p3, c3),
;
);

impl_test_fn!(
TestFn4;
run_test4;
run_test_with_pool4;
('c DB1, args1, tc1, tp1, po1, co1, p1, c1),
('d DB2, args2, tc2, tp2, po2, co2, p2, c2),
('d DB3, args3, tc3, tp3, po3, co3, p3, c3),
('e DB4, args4, tc4, tp4, po4, co4, p4, c4),
;
);
Loading
Loading