pg_embedded_setup_unpriv/cluster/mod.rs
1//! RAII wrapper that boots an embedded `PostgreSQL` instance for tests.
2//!
3//! The cluster starts during [`TestCluster::new`] and shuts down automatically when the
4//! value drops out of scope.
5//!
6//! # Synchronous API
7//!
8//! Use [`TestCluster::new`] from synchronous contexts or when you want the cluster to
9//! own its own Tokio runtime:
10//!
11//! ```no_run
12//! use pg_embedded_setup_unpriv::TestCluster;
13//!
14//! # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
15//! let cluster = TestCluster::new()?;
16//! let url = cluster.settings().url("my_database");
17//! // Perform test database work here.
18//! drop(cluster); // `PostgreSQL` stops automatically.
19//!
20//! # Ok(())
21//! # }
22//! ```
23//!
24//! # Async API
25//!
26//! When running within an existing async runtime (e.g., `#[tokio::test]`), use
27//! [`TestCluster::start_async`] to avoid the "Cannot start a runtime from within a
28//! runtime" panic that occurs when nesting Tokio runtimes:
29//!
30//! ```ignore
31//! use pg_embedded_setup_unpriv::TestCluster;
32//!
33//! #[tokio::test]
34//! async fn test_with_embedded_postgres() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
35//! let cluster = TestCluster::start_async().await?;
36//! let url = cluster.settings().url("my_database");
37//! // ... async database operations ...
38//! cluster.stop_async().await?;
39//! Ok(())
40//! }
41//! ```
42//!
43//! The async API requires the `async-api` feature flag:
44//!
45//! ```toml
46//! [dependencies]
47//! pg-embedded-setup-unpriv = { version = "...", features = ["async-api"] }
48//! ```
49
50#[cfg(feature = "async-api")]
51mod async_api;
52mod cache_integration;
53mod cleanup;
54mod connection;
55mod delegation;
56mod extension_hook;
57mod guard;
58mod handle;
59mod installation;
60mod lifecycle;
61mod lifecycle_template;
62pub(crate) mod panic_utils;
63mod runtime;
64mod runtime_mode;
65mod shutdown;
66#[cfg(any(unix, windows))]
67mod shutdown_hook;
68#[cfg(all(
69 any(unix, windows),
70 any(doc, test, feature = "cluster-unit-tests", feature = "dev-worker")
71))]
72pub use self::shutdown_hook::{
73 PostmasterPid,
74 PostmasterProcess,
75 postmaster_process_is_running,
76 process_is_running,
77 read_postmaster_pid,
78 read_postmaster_process,
79};
80#[cfg(all(test, feature = "loom-tests"))]
81mod lifecycle_loom_tests;
82mod startup;
83mod temporary_database;
84mod worker_invoker;
85mod worker_operation;
86
87use std::ops::Deref;
88
89use tracing::info_span;
90
91pub(crate) use self::startup::setup_postgres_only;
92#[cfg(any(doc, test, feature = "cluster-unit-tests", feature = "dev-worker"))]
93pub use self::worker_invoker::WorkerInvoker;
94#[doc(hidden)]
95pub use self::worker_operation::WorkerOperation;
96pub use self::{
97 connection::{ConnectionMetadata, TestClusterConnection},
98 guard::ClusterGuard,
99 handle::ClusterHandle,
100 lifecycle::DatabaseName,
101 temporary_database::TemporaryDatabase,
102};
103use self::{
104 runtime::build_runtime,
105 runtime_mode::ClusterRuntime,
106 startup::{cache_config_from_bootstrap, start_postgres},
107};
108use crate::{
109 bootstrap_for_tests,
110 env::ScopedEnv,
111 error::BootstrapResult,
112 observability::LOG_TARGET,
113};
114
115/// Embedded `PostgreSQL` instance whose lifecycle follows Rust's drop semantics.
116///
117/// `TestCluster` combines a [`ClusterHandle`] (for cluster access) with a
118/// [`ClusterGuard`] (for lifecycle management). For most use cases, this
119/// combined type is the simplest option.
120///
121/// # Send-Safe Patterns
122///
123/// `TestCluster` is `!Send` because it contains environment guards that must
124/// be dropped on the creating thread. For patterns requiring `Send` (such as
125/// `OnceLock` or rstest timeouts), use [`new_split()`](Self::new_split) to
126/// obtain a `Send`-safe [`ClusterHandle`]:
127///
128/// ```no_run
129/// use std::sync::OnceLock;
130///
131/// use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
132///
133/// static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
134///
135/// fn shared_cluster() -> &'static ClusterHandle {
136/// SHARED.get_or_init(|| {
137/// let (handle, guard) = TestCluster::new_split().expect("cluster bootstrap failed");
138/// handle
139/// .register_shutdown_on_exit()
140/// .expect("shutdown hook registration failed");
141/// std::mem::forget(guard);
142/// handle
143/// })
144/// }
145/// ```
146#[derive(Debug)]
147pub struct TestCluster {
148 /// Send-safe handle providing cluster access.
149 pub(crate) handle: ClusterHandle,
150 /// Lifecycle guard managing shutdown and environment restoration.
151 pub(crate) guard: ClusterGuard,
152}
153
154impl TestCluster {
155 /// Boots a `PostgreSQL` instance configured by [`bootstrap_for_tests`].
156 ///
157 /// The constructor blocks until the underlying server process is running and returns an
158 /// error when startup fails.
159 ///
160 /// # Errors
161 /// Returns an error if the bootstrap configuration cannot be prepared or if starting the
162 /// embedded cluster fails.
163 pub fn new() -> BootstrapResult<Self> {
164 let (handle, guard) = Self::new_split()?;
165 Ok(Self { handle, guard })
166 }
167
168 /// Boots a `PostgreSQL` instance and returns a separate handle and guard.
169 ///
170 /// This constructor is useful for patterns requiring `Send`, such as shared
171 /// cluster fixtures with [`OnceLock`](std::sync::OnceLock) or rstest fixtures
172 /// with timeouts.
173 ///
174 /// # Returns
175 ///
176 /// A tuple of:
177 /// - [`ClusterHandle`]: `Send + Sync` handle for accessing the cluster
178 /// - [`ClusterGuard`]: `!Send` guard managing shutdown and environment
179 ///
180 /// # Errors
181 ///
182 /// Returns an error if the bootstrap configuration cannot be prepared or if
183 /// starting the embedded cluster fails.
184 ///
185 /// # Examples
186 ///
187 /// ## Shared Cluster with `OnceLock`
188 ///
189 /// For shared clusters that run for the entire process lifetime, register
190 /// the shutdown hook and forget the guard to prevent shutdown on drop:
191 ///
192 /// ```no_run
193 /// use std::sync::OnceLock;
194 ///
195 /// use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
196 ///
197 /// static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
198 ///
199 /// fn shared_cluster() -> &'static ClusterHandle {
200 /// SHARED.get_or_init(|| {
201 /// let (handle, guard) = TestCluster::new_split().expect("cluster bootstrap failed");
202 /// handle
203 /// .register_shutdown_on_exit()
204 /// .expect("shutdown hook registration failed");
205 /// std::mem::forget(guard);
206 /// handle
207 /// })
208 /// }
209 /// ```
210 ///
211 /// **Warning**: Dropping the guard shuts down the cluster. Do not use the
212 /// handle after the guard has been dropped unless the guard was forgotten.
213 pub fn new_split() -> BootstrapResult<(ClusterHandle, ClusterGuard)> {
214 let span = info_span!(target: LOG_TARGET, "test_cluster");
215 // Resolve cache directory BEFORE applying test environment.
216 // Otherwise, the test sandbox's XDG_CACHE_HOME would be used.
217 let (runtime, env_vars, env_guard, outcome) = {
218 let _entered = span.enter();
219 let initial_bootstrap = bootstrap_for_tests()?;
220 let cache_config = cache_config_from_bootstrap(&initial_bootstrap);
221 let runtime = build_runtime()?;
222 let env_vars = initial_bootstrap.environment.to_env();
223 let env_guard = ScopedEnv::apply(&env_vars);
224 let outcome = start_postgres(&runtime, initial_bootstrap, &env_vars, &cache_config)?;
225 (runtime, env_vars, env_guard, outcome)
226 };
227
228 let handle = ClusterHandle::new(outcome.bootstrap.clone());
229 let guard = ClusterGuard {
230 runtime: ClusterRuntime::Sync(runtime),
231 postgres: outcome.postgres,
232 bootstrap: outcome.bootstrap,
233 is_managed_via_worker: outcome.is_managed_via_worker,
234 env_vars,
235 worker_guard: None,
236 _env_guard: env_guard,
237 _cluster_span: span,
238 };
239
240 Ok((handle, guard))
241 }
242
243 /// Extends the cluster lifetime to cover additional scoped environment guards.
244 ///
245 /// Primarily used by fixtures that need to ensure `PG_EMBEDDED_WORKER` remains set for the
246 /// duration of the cluster lifetime.
247 #[doc(hidden)]
248 #[must_use]
249 pub fn with_worker_guard(self, worker_guard: Option<ScopedEnv>) -> Self {
250 Self {
251 handle: self.handle,
252 guard: self.guard.with_worker_guard(worker_guard),
253 }
254 }
255}
256
257/// Provides transparent access to [`ClusterHandle`] methods.
258///
259/// This allows `TestCluster` to be used interchangeably with `ClusterHandle`
260/// for all read-only operations like `settings()`, `connection()`, etc.
261impl Deref for TestCluster {
262 type Target = ClusterHandle;
263
264 fn deref(&self) -> &Self::Target { &self.handle }
265}
266
267// Note: TestCluster does NOT implement Drop because the ClusterGuard handles shutdown.
268// When TestCluster drops, its _guard field drops, which triggers ClusterGuard::Drop.
269
270#[cfg(test)]
271mod mod_tests;
272
273#[cfg(all(test, feature = "cluster-unit-tests"))]
274mod drop_logging_tests;
275
276#[cfg(all(test, not(feature = "cluster-unit-tests")))]
277#[path = "../../tests/test_cluster.rs"]
278mod test_cluster_tests;