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.