use std::future::Future;
use std::time::Duration;
use futures_core::future::BoxFuture;
pub use fixtures::FixtureSnapshot;
use crate::connection::{ConnectOptions, Connection};
use crate::database::Database;
use crate::error::Error;
use crate::executor::Executor;
use crate::migrate::{Migrate, Migrator};
use crate::pool::{Pool, PoolConnection, PoolOptions};
mod fixtures;
pub trait TestSupport: Database {
fn test_context(args: &TestArgs) -> BoxFuture<'_, Result<TestContext<Self>, Error>>;
fn cleanup_test(db_name: &str) -> BoxFuture<'_, Result<(), Error>>;
fn cleanup_test_dbs() -> BoxFuture<'static, Result<Option<usize>, Error>>;
fn snapshot(conn: &mut Self::Connection)
-> BoxFuture<'_, Result<FixtureSnapshot<Self>, Error>>;
}
pub struct TestFixture {
pub path: &'static str,
pub contents: &'static str,
}
pub struct TestArgs {
pub test_path: &'static str,
pub migrator: Option<&'static Migrator>,
pub fixtures: &'static [TestFixture],
}
pub trait TestFn {
type Output;
fn run_test(self, args: TestArgs) -> Self::Output;
}
pub trait TestTermination {
fn is_success(&self) -> bool;
}
pub struct TestContext<DB: Database> {
pub pool_opts: PoolOptions<DB>,
pub connect_opts: <DB::Connection as Connection>::Options,
pub db_name: String,
}
impl<DB, Fut> TestFn for fn(Pool<DB>) -> Fut
where
DB: TestSupport + Database,
DB::Connection: Migrate,
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
Fut: Future,
Fut::Output: TestTermination,
{
type Output = Fut::Output;
fn run_test(self, args: TestArgs) -> Self::Output {
run_test_with_pool(args, self)
}
}
impl<DB, Fut> TestFn for fn(PoolConnection<DB>) -> Fut
where
DB: TestSupport + Database,
DB::Connection: Migrate,
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
Fut: Future,
Fut::Output: TestTermination,
{
type Output = Fut::Output;
fn run_test(self, args: TestArgs) -> Self::Output {
run_test_with_pool(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> TestFn for fn(PoolOptions<DB>, <DB::Connection as Connection>::Options) -> Fut
where
DB: Database + TestSupport,
DB::Connection: Migrate,
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
Fut: Future,
Fut::Output: TestTermination,
{
type Output = Fut::Output;
fn run_test(self, args: TestArgs) -> Self::Output {
run_test(args, self)
}
}
impl<Fut> TestFn 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())
}
}
impl TestArgs {
pub fn new(test_path: &'static str) -> Self {
TestArgs {
test_path,
migrator: None,
fixtures: &[],
}
}
pub fn migrator(&mut self, migrator: &'static Migrator) {
self.migrator = Some(migrator);
}
pub fn fixtures(&mut self, fixtures: &'static [TestFixture]) {
self.fixtures = fixtures;
}
}
impl TestTermination for () {
fn is_success(&self) -> bool {
true
}
}
impl<T, E> TestTermination for Result<T, E> {
fn is_success(&self) -> bool {
self.is_ok()
}
}
fn run_test_with_pool<DB, F, Fut>(args: TestArgs, test_fn: F) -> Fut::Output
where
DB: TestSupport,
DB::Connection: Migrate,
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
F: FnOnce(Pool<DB>) -> Fut,
Fut: Future,
Fut::Output: TestTermination,
{
let test_path = args.test_path;
run_test::<DB, _, _>(args, |pool_opts, connect_opts| async move {
let pool = pool_opts
.connect_with(connect_opts)
.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 {test_path} held onto Pool after exiting");
}
res
})
}
fn run_test<DB, F, Fut>(args: TestArgs, test_fn: F) -> Fut::Output
where
DB: TestSupport,
DB::Connection: Migrate,
for<'c> &'c mut DB::Connection: Executor<'c, 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 test_context = DB::test_context(&args)
.await
.expect("failed to connect to setup test database");
setup_test_db::<DB>(&test_context.connect_opts, &args).await;
let res = test_fn(test_context.pool_opts, test_context.connect_opts).await;
if res.is_success() {
if let Err(e) = DB::cleanup_test(&test_context.db_name).await {
eprintln!(
"failed to delete database {:?}: {}",
test_context.db_name, e
);
}
}
res
})
}
async fn setup_test_db<DB: Database>(
copts: &<DB::Connection as Connection>::Options,
args: &TestArgs,
) where
DB::Connection: Migrate + Sized,
for<'c> &'c mut DB::Connection: Executor<'c, Database = DB>,
{
let mut conn = copts
.connect()
.await
.expect("failed to connect to test database");
if let Some(migrator) = args.migrator {
migrator
.run_direct(&mut conn)
.await
.expect("failed to apply migrations");
}
for fixture in args.fixtures {
(&mut conn)
.execute(fixture.contents)
.await
.unwrap_or_else(|e| panic!("failed to apply test fixture {:?}: {:?}", fixture.path, e));
}
conn.close()
.await
.expect("failed to close setup connection");
}