use std::{future::Future, pin::Pin};
use anyhow::Result;
use sqlx::{Connection, Executor, PgConnection, PgPool, postgres::PgPoolOptions};
use url::Url;
use uuid::Uuid;
pub async fn with_test_db<F, T>(test_name: &str, f: F) -> Result<T>
where
F: for<'a> FnOnce(&'a PgPool) -> Pin<Box<dyn Future<Output = Result<T>> + 'a>>,
{
dotenvy::from_filename(".env").ok();
let admin_url = std::env::var("TEST_ADMIN_DATABASE_URL")
.expect("TEST_ADMIN_DATABASE_URL must be set for DB tests");
let mut admin_conn = PgConnection::connect(&admin_url).await?;
let db_name = make_db_name(test_name);
admin_conn
.execute(format!(r#"CREATE DATABASE "{}""#, db_name).as_str())
.await?;
let mut db_url = Url::parse(&admin_url)?;
db_url.set_path(&format!("/{}", db_name));
let pool = PgPoolOptions::new()
.max_connections(5)
.connect(db_url.as_str())
.await?;
sqlx::migrate!().run(&pool).await?;
let result = f(&pool).await;
let keep = std::env::var("TEST_KEEP_DB").is_ok();
if result.is_ok() && !keep {
pool.close().await;
if let Err(e) = admin_conn
.execute(format!(r#"DROP DATABASE IF EXISTS "{}" WITH (FORCE);"#, db_name).as_str())
.await
{
eprintln!(
"[with_test_db] Failed to drop database '{}': {}",
db_name, e
);
} else {
eprintln!("[with_test_db] Dropped database '{}'", db_name);
}
} else {
eprintln!(
"[with_test_db] Keeping database '{}' (error or TEST_KEEP_DB set)",
db_name
);
}
result
}
fn make_db_name(test_name: &str) -> String {
let mut safe: String = test_name
.to_lowercase()
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
while safe.starts_with('_') {
safe.remove(0);
}
while safe.ends_with('_') {
safe.pop();
}
let prefix = "test_";
let suffix_len = 1 + 32; let max_ident = 63usize;
let max_safe_len = max_ident
.saturating_sub(prefix.len())
.saturating_sub(suffix_len);
if safe.len() > max_safe_len {
safe.truncate(max_safe_len);
}
let uuid_part = Uuid::now_v7().simple(); format!("{prefix}{safe}_{uuid_part}")
}
#[macro_export]
macro_rules! db_test {
($name:ident, |$pool:ident| $body:block) => {
#[tokio::test(flavor = "multi_thread")]
async fn $name() -> anyhow::Result<()> {
let test_name = stringify!($name);
$crate::support::db_test::with_test_db(test_name, |$pool| {
let fut = async move { $body };
Box::pin(fut)
})
.await
}
};
}