Skip to content
Merged
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
2 changes: 2 additions & 0 deletions tests/bdd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -768,6 +770,10 @@ pub async fn configure_database(config: &DatabaseSettings) -> Result<PgPool, sql
connection
.execute(format!(r#"CREATE DATABASE "{}""#, config.database_name).as_str())
.await?;
drop_at_exit(
&config.connection_string_without_db(),
&config.database_name,
);

// Run migrations on a dedicated single-connection pool so the advisory lock
// and any DDL catalog locks are fully released before the server pool is
Expand Down
62 changes: 62 additions & 0 deletions tests/common/test_databases.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//! Drops the databases a test process created, when the process exits.
//!
//! Every spawned app gets its own database (about 11 MB with the migrations).
//! Left behind, a full run fills the CI runner's disk until Postgres panics
//! with "No space left on device". Most apps live in a `static` shared by a
//! test file, so nothing is dropped in Rust; the drop happens at process exit
//! instead. Under nextest that is the end of each test, under `cargo test`
//! the end of each test binary. A killed process still leaks its databases.
//!
//! Shared by the integration harness (`tests/common/mod.rs`) and the BDD
//! harness (`tests/bdd.rs`) via `#[path]` include.

use sqlx::{Connection, Executor, PgConnection};
use std::sync::Mutex;

/// (connection string without the database, database name)
static CREATED: Mutex<Vec<(String, String)>> = 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}");
}
}
});
}
4 changes: 4 additions & 0 deletions tests/steps/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ async fn configure_database(config: &DatabaseSettings) -> Result<PgPool, sqlx::E
connection
.execute(format!(r#"CREATE DATABASE "{}""#, config.database_name).as_str())
.await?;
crate::test_databases::drop_at_exit(
&config.connection_string_without_db(),
&config.database_name,
);
let pool = sqlx::postgres::PgPoolOptions::new()
.max_connections(32)
.acquire_timeout(std::time::Duration::from_secs(120))
Expand Down
56 changes: 56 additions & 0 deletions tests/test_databases_are_dropped.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//! Every spawned test app creates its own database. Left behind, they fill the
//! CI runner's disk (about 11 MB each, one or more per test process) until
//! Postgres panics with "No space left on device" and the rest of the run
//! fails to start. A test process must drop the databases it created.

mod common;

use sqlx::{Connection, PgConnection};

const PREFIX: &str = "TEST_DATABASE=";

/// Runs only as the child of `a_finished_test_process_leaves_no_database`.
#[tokio::test]
#[ignore = "child process of a_finished_test_process_leaves_no_database"]
async fn spawn_an_app_and_report_its_database() {
let Some(app) = common::spawn_app().await else {
return;
};
println!("{PREFIX}{}", app.connection_string);
}

#[test]
fn a_finished_test_process_leaves_no_database() {
let out = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"spawn_an_app_and_report_its_database",
"--exact",
"--ignored",
"--nocapture",
])
.output()
.expect("run the child test process");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(out.status.success(), "child failed: {stdout}");
let Some(connection_string) = stdout
.lines()
.find_map(|line| line.strip_prefix(PREFIX))
.map(str::to_string)
else {
println!("Skipping: the child could not reach postgres");
return;
};
let (server, name) = connection_string.rsplit_once('/').unwrap();

let left = tokio::runtime::Runtime::new().unwrap().block_on(async {
let mut conn = PgConnection::connect(&format!("{server}/postgres"))
.await
.expect("connect to postgres");
sqlx::query_scalar::<_, i64>("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");
}
Loading