diff --git a/tests/bdd.rs b/tests/bdd.rs index 6c747164..045fcb30 100644 --- a/tests/bdd.rs +++ b/tests/bdd.rs @@ -2,6 +2,8 @@ mod vault_kv_mock; mod steps; +#[path = "common/test_databases.rs"] +mod test_databases; use cucumber::World as _; use steps::StepWorld; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index ed9fe341..51422907 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -16,6 +16,8 @@ use std::net::TcpListener; use std::path::{Path, PathBuf}; use std::sync::OnceLock; use wiremock::MockServer; +mod test_databases; +pub use test_databases::drop_at_exit; static ACCESS_CONTROL_CONF_READY: OnceLock<()> = OnceLock::new(); /// Long-lived runtime handle for test infrastructure (server + pool). @@ -768,6 +770,10 @@ pub async fn configure_database(config: &DatabaseSettings) -> Result> = Mutex::new(Vec::new()); + +extern "C" { + fn atexit(callback: extern "C" fn()) -> std::os::raw::c_int; +} + +/// Drop `name` when this process exits. Call right after `CREATE DATABASE`. +pub fn drop_at_exit(server: &str, name: &str) { + let mut created = CREATED.lock().unwrap_or_else(|e| e.into_inner()); + if created.is_empty() { + // SAFETY: registers a plain `extern "C"` function, as atexit expects. + unsafe { + atexit(drop_created); + } + } + created.push((server.to_string(), name.to_string())); +} + +extern "C" fn drop_created() { + let created = std::mem::take(&mut *CREATED.lock().unwrap_or_else(|e| e.into_inner())); + // A fresh thread: at exit the main thread's thread-locals, which tokio + // needs, are already destroyed. + let _ = std::thread::spawn(move || drop_all(created)).join(); +} + +fn drop_all(created: Vec<(String, String)>) { + let Ok(runtime) = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + else { + return; + }; + runtime.block_on(async { + for (server, name) in created { + // FORCE: the app's server and pools still hold connections. + let sql = format!(r#"DROP DATABASE IF EXISTS "{name}" WITH (FORCE)"#); + let result = match PgConnection::connect(&server).await { + Ok(mut conn) => conn.execute(sql.as_str()).await.map(|_| ()), + Err(e) => Err(e), + }; + if let Err(e) = result { + eprintln!("test database {name} not dropped: {e}"); + } + } + }); +} diff --git a/tests/steps/common.rs b/tests/steps/common.rs index 9d2a0a7f..de916cee 100644 --- a/tests/steps/common.rs +++ b/tests/steps/common.rs @@ -123,6 +123,10 @@ async fn configure_database(config: &DatabaseSettings) -> Result("SELECT count(*) FROM pg_database WHERE datname = $1") + .bind(name) + .fetch_one(&mut conn) + .await + .expect("query pg_database") + }); + assert_eq!(left, 0, "database {name} outlived the test process"); +}