From 8f7070dd1588d9f2449f511c0072448124256cb9 Mon Sep 17 00:00:00 2001 From: Vasili Pascal Date: Wed, 28 Oct 2026 15:21:36 +0200 Subject: [PATCH] tests: a test process drops the databases it created Every spawned test app creates its own database (~11 MB with the migrations) and nothing dropped them. A full CI run filled the runner's disk until Postgres panicked ("could not write to file pg_wal/...: No space left on device") and the last few hundred tests failed with "Failed to start test app", on every branch. configure_database (integration and BDD harnesses) now registers each database it creates, and an atexit handler drops them all with DROP DATABASE ... WITH (FORCE) when the process exits. Most apps live in a static shared by a test file, so a Rust Drop would never run; process exit is the end of each test under nextest and of each binary under cargo test. The drop runs on a fresh thread: at exit the main thread's thread-locals, which tokio needs, are gone. Regression test: tests/test_databases_are_dropped.rs runs a child test process that spawns an app, then checks its database is gone. Co-Authored-By: Claude Opus 5.5 --- tests/bdd.rs | 2 + tests/common/mod.rs | 6 +++ tests/common/test_databases.rs | 62 +++++++++++++++++++++++++++++ tests/steps/common.rs | 4 ++ tests/test_databases_are_dropped.rs | 56 ++++++++++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 tests/common/test_databases.rs create mode 100644 tests/test_databases_are_dropped.rs 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"); +}