Resource Acquisition Is Initialization (RAII) test clusters

Version 0.5.2 Updated Jul 20, 2026

pg_embedded_setup_unpriv::TestCluster wraps bootstrap_for_tests() with a Resource Acquisition Is Initialization (RAII) lifecycle. Constructing the guard starts PostgreSQL using the discovered settings, applies the environment produced by the bootstrap helper, and exposes the configuration to callers. Dropping the guard stops the instance and restores the prior process environment, so subsequent tests start from a clean slate.

use pg_embedded_setup_unpriv::{TestCluster, error::BootstrapResult};

fn exercise_cluster() -> BootstrapResult<()> {
    let cluster = TestCluster::new()?;
    let url = cluster.settings().url("app_db");
    // Issue queries using any preferred client here.
drop(cluster); // PostgreSQL shuts down automatically.
    Ok(())
}

The guard keeps PGPASSFILE, TZ, TZDIR, and the XDG directories populated for the duration of its lifetime, making synchronous tests usable without extra setup.

By default the guard removes the PostgreSQL data directory when it drops. Use CleanupMode to control whether the installation directory is removed or to skip cleanup for debugging:

use pg_embedded_setup_unpriv::{CleanupMode, TestCluster};

# fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
let cluster = TestCluster::new()?.with_cleanup_mode(CleanupMode::Full);
drop(cluster);
# Ok(())
# }

Shared clusters created with test_support::shared_test_cluster() are intentionally leaked for the process lifetime and therefore do not perform cleanup on drop.

Async API for `#[tokio::test]` contexts

Tests within an async runtime (e.g. #[tokio::test]) must not use the standard TestCluster::new() constructor, which panics with "Cannot start a runtime from within a runtime" because it creates its own internal Tokio runtime. Async contexts require enabling the async-api feature and using the async constructor and shutdown methods.

Enable the feature in your Cargo.toml:

[dev-dependencies]
pg-embed-setup-unpriv = { version = "0.5.2", features = ["async-api"] }

Then use start_async() and stop_async() in your async tests:

use pg_embedded_setup_unpriv::{TestCluster, error::BootstrapResult};

#[tokio::test]
async fn test_async_database_operations() -> BootstrapResult<()> {
    let cluster = TestCluster::start_async().await?;

    // Access connection metadata as usual.
    let url = cluster.connection().database_url("app_db");
    // Issue async queries using sqlx or other async clients here.

    // Explicitly shut down to ensure clean resource release.
    cluster.stop_async().await?;
    Ok(())
}

Async clusters behave like the synchronous guard: the same accessors apply, and the environment overrides are restored on shutdown. stop_async() consumes the guard, so capture any required connection details before calling it.

Important: stop_async() must be called explicitly before the cluster goes out of scope. Unlike the synchronous API where Drop can reliably shut down PostgreSQL using its internal runtime, async-created clusters cannot guarantee cleanup in Drop because Drop cannot be async. When stop_async() is not called, the library will attempt best-effort cleanup and log a warning; if no async runtime handle is available (for example, after the runtime has shut down), resources may leak and the process may need to be stopped manually.

The async API runs PostgreSQL lifecycle operations on the caller's runtime rather than creating a separate one, avoiding the nested-runtime panic whilst maintaining the same zero-configuration experience as the synchronous API. When running as root, the async API still delegates to the worker helper, and those operations are executed with spawn_blocking so they do not block the async executor.