pg-embed-setup-unpriv 0.5.1

Initialises postgresql_embedded clusters as root while handing off filesystem work to nobody
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
//! RAII wrapper that boots an embedded `PostgreSQL` instance for tests.
//!
//! The cluster starts during [`TestCluster::new`] and shuts down automatically when the
//! value drops out of scope.
//!
//! # Synchronous API
//!
//! Use [`TestCluster::new`] from synchronous contexts or when you want the cluster to
//! own its own Tokio runtime:
//!
//! ```no_run
//! use pg_embedded_setup_unpriv::TestCluster;
//!
//! # fn main() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
//! let cluster = TestCluster::new()?;
//! let url = cluster.settings().url("my_database");
//! // Perform test database work here.
//! drop(cluster); // `PostgreSQL` stops automatically.
//! # Ok(())
//! # }
//! ```
//!
//! # Async API
//!
//! When running within an existing async runtime (e.g., `#[tokio::test]`), use
//! [`TestCluster::start_async`] to avoid the "Cannot start a runtime from within a
//! runtime" panic that occurs when nesting Tokio runtimes:
//!
//! ```ignore
//! use pg_embedded_setup_unpriv::TestCluster;
//!
//! #[tokio::test]
//! async fn test_with_embedded_postgres() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
//!     let cluster = TestCluster::start_async().await?;
//!     let url = cluster.settings().url("my_database");
//!     // ... async database operations ...
//!     cluster.stop_async().await?;
//!     Ok(())
//! }
//! ```
//!
//! The async API requires the `async-api` feature flag:
//!
//! ```toml
//! [dependencies]
//! pg-embedded-setup-unpriv = { version = "...", features = ["async-api"] }
//! ```

mod cache_integration;
mod cleanup;
mod connection;
mod delegation;
mod guard;
mod handle;
mod installation;
mod lifecycle;
pub(crate) mod panic_utils;
mod runtime;
mod runtime_mode;
mod shutdown;
#[cfg(unix)]
mod shutdown_hook;
#[cfg(all(
    unix,
    any(doc, test, feature = "cluster-unit-tests", feature = "dev-worker")
))]
pub use self::shutdown_hook::{process_is_running, read_postmaster_pid};
mod startup;
mod temporary_database;
mod worker_invoker;
mod worker_operation;

pub use self::connection::{ConnectionMetadata, TestClusterConnection};
pub use self::guard::ClusterGuard;
pub use self::handle::ClusterHandle;
pub use self::lifecycle::DatabaseName;
pub use self::temporary_database::TemporaryDatabase;
#[cfg(any(doc, test, feature = "cluster-unit-tests", feature = "dev-worker"))]
pub use self::worker_invoker::WorkerInvoker;
#[doc(hidden)]
pub use self::worker_operation::WorkerOperation;

use self::runtime::build_runtime;
use self::runtime_mode::ClusterRuntime;
pub(crate) use self::startup::setup_postgres_only;
#[cfg(feature = "async-api")]
use self::startup::start_postgres_async;
use self::startup::{cache_config_from_bootstrap, start_postgres};
use crate::bootstrap_for_tests;
use crate::env::ScopedEnv;
use crate::error::BootstrapResult;
use crate::observability::LOG_TARGET;
use std::ops::Deref;
use tracing::info_span;

/// Embedded `PostgreSQL` instance whose lifecycle follows Rust's drop semantics.
///
/// `TestCluster` combines a [`ClusterHandle`] (for cluster access) with a
/// [`ClusterGuard`] (for lifecycle management). For most use cases, this
/// combined type is the simplest option.
///
/// # Send-Safe Patterns
///
/// `TestCluster` is `!Send` because it contains environment guards that must
/// be dropped on the creating thread. For patterns requiring `Send` (such as
/// `OnceLock` or rstest timeouts), use [`new_split()`](Self::new_split) to
/// obtain a `Send`-safe [`ClusterHandle`]:
///
/// ```no_run
/// use std::sync::OnceLock;
/// use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
///
/// static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
///
/// fn shared_cluster() -> &'static ClusterHandle {
///     SHARED.get_or_init(|| {
///         let (handle, guard) = TestCluster::new_split()
///             .expect("cluster bootstrap failed");
///         handle.register_shutdown_on_exit()
///             .expect("shutdown hook registration failed");
///         std::mem::forget(guard);
///         handle
///     })
/// }
/// ```
#[derive(Debug)]
pub struct TestCluster {
    /// Send-safe handle providing cluster access.
    pub(crate) handle: ClusterHandle,
    /// Lifecycle guard managing shutdown and environment restoration.
    pub(crate) guard: ClusterGuard,
}

#[cfg(feature = "async-api")]
enum StopAsyncPath {
    WorkerManaged,
    InProcess(Box<postgresql_embedded::PostgreSQL>),
    Noop,
}

impl TestCluster {
    /// Boots a `PostgreSQL` instance configured by [`bootstrap_for_tests`].
    ///
    /// The constructor blocks until the underlying server process is running and returns an
    /// error when startup fails.
    ///
    /// # Errors
    /// Returns an error if the bootstrap configuration cannot be prepared or if starting the
    /// embedded cluster fails.
    pub fn new() -> BootstrapResult<Self> {
        let (handle, guard) = Self::new_split()?;
        Ok(Self { handle, guard })
    }

    /// Boots a `PostgreSQL` instance and returns a separate handle and guard.
    ///
    /// This constructor is useful for patterns requiring `Send`, such as shared
    /// cluster fixtures with [`OnceLock`](std::sync::OnceLock) or rstest fixtures
    /// with timeouts.
    ///
    /// # Returns
    ///
    /// A tuple of:
    /// - [`ClusterHandle`]: `Send + Sync` handle for accessing the cluster
    /// - [`ClusterGuard`]: `!Send` guard managing shutdown and environment
    ///
    /// # Errors
    ///
    /// Returns an error if the bootstrap configuration cannot be prepared or if
    /// starting the embedded cluster fails.
    ///
    /// # Examples
    ///
    /// ## Shared Cluster with `OnceLock`
    ///
    /// For shared clusters that run for the entire process lifetime, register
    /// the shutdown hook and forget the guard to prevent shutdown on drop:
    ///
    /// ```no_run
    /// use std::sync::OnceLock;
    /// use pg_embedded_setup_unpriv::{ClusterHandle, TestCluster};
    ///
    /// static SHARED: OnceLock<ClusterHandle> = OnceLock::new();
    ///
    /// fn shared_cluster() -> &'static ClusterHandle {
    ///     SHARED.get_or_init(|| {
    ///         let (handle, guard) = TestCluster::new_split()
    ///             .expect("cluster bootstrap failed");
    ///         handle.register_shutdown_on_exit()
    ///             .expect("shutdown hook registration failed");
    ///         std::mem::forget(guard);
    ///         handle
    ///     })
    /// }
    /// ```
    ///
    /// **Warning**: Dropping the guard shuts down the cluster. Do not use the
    /// handle after the guard has been dropped unless the guard was forgotten.
    pub fn new_split() -> BootstrapResult<(ClusterHandle, ClusterGuard)> {
        let span = info_span!(target: LOG_TARGET, "test_cluster");
        // Resolve cache directory BEFORE applying test environment.
        // Otherwise, the test sandbox's XDG_CACHE_HOME would be used.
        let (runtime, env_vars, env_guard, outcome) = {
            let _entered = span.enter();
            let initial_bootstrap = bootstrap_for_tests()?;
            let cache_config = cache_config_from_bootstrap(&initial_bootstrap);
            let runtime = build_runtime()?;
            let env_vars = initial_bootstrap.environment.to_env();
            let env_guard = ScopedEnv::apply(&env_vars);
            let outcome = start_postgres(&runtime, initial_bootstrap, &env_vars, &cache_config)?;
            (runtime, env_vars, env_guard, outcome)
        };

        let handle = ClusterHandle::new(outcome.bootstrap.clone());
        let guard = ClusterGuard {
            runtime: ClusterRuntime::Sync(runtime),
            postgres: outcome.postgres,
            bootstrap: outcome.bootstrap,
            is_managed_via_worker: outcome.is_managed_via_worker,
            env_vars,
            worker_guard: None,
            _env_guard: env_guard,
            _cluster_span: span,
        };

        Ok((handle, guard))
    }

    /// Boots a `PostgreSQL` instance asynchronously for use in `#[tokio::test]` contexts.
    ///
    /// Unlike [`TestCluster::new`], this constructor does not create its own Tokio runtime.
    /// Instead, it runs on the caller's async runtime, making it safe to call from within
    /// `#[tokio::test]` and other async contexts.
    ///
    /// **Important:** Clusters created with `start_async()` should be shut down explicitly
    /// using [`stop_async()`](Self::stop_async). The `Drop` implementation will attempt
    /// best-effort cleanup but may not succeed if the runtime is no longer available.
    ///
    /// # Errors
    ///
    /// Returns an error if the bootstrap configuration cannot be prepared or if starting
    /// the embedded cluster fails.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use pg_embedded_setup_unpriv::TestCluster;
    ///
    /// #[tokio::test]
    /// async fn test_with_embedded_postgres() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
    ///     let cluster = TestCluster::start_async().await?;
    ///     let url = cluster.settings().url("my_database");
    ///     // ... async database operations ...
    ///     cluster.stop_async().await?;
    ///     Ok(())
    /// }
    /// ```
    #[cfg(feature = "async-api")]
    pub async fn start_async() -> BootstrapResult<Self> {
        let (handle, guard) = Self::start_async_split().await?;
        Ok(Self { handle, guard })
    }

    /// Boots a `PostgreSQL` instance asynchronously and returns a separate handle and guard.
    ///
    /// This is the async equivalent of [`new_split()`](Self::new_split).
    ///
    /// # Returns
    ///
    /// A tuple of:
    /// - [`ClusterHandle`]: `Send + Sync` handle for accessing the cluster
    /// - [`ClusterGuard`]: `!Send` guard managing shutdown and environment
    ///
    /// # Errors
    ///
    /// Returns an error if the bootstrap configuration cannot be prepared or if
    /// starting the embedded cluster fails.
    #[cfg(feature = "async-api")]
    pub async fn start_async_split() -> BootstrapResult<(ClusterHandle, ClusterGuard)> {
        use tracing::Instrument;

        let span = info_span!(target: LOG_TARGET, "test_cluster", async_mode = true);

        // Sync bootstrap preparation (no await needed).
        // Resolve cache directory BEFORE applying test environment.
        // Otherwise, the test sandbox's XDG_CACHE_HOME would be used.
        let initial_bootstrap = bootstrap_for_tests()?;
        let cache_config = cache_config_from_bootstrap(&initial_bootstrap);
        let env_vars = initial_bootstrap.environment.to_env();
        let env_guard = ScopedEnv::apply(&env_vars);

        // Async postgres startup, instrumented with the span.
        // Box::pin to avoid large future on the stack.
        let outcome = Box::pin(start_postgres_async(
            initial_bootstrap,
            &env_vars,
            &cache_config,
        ))
        .instrument(span.clone())
        .await?;

        let handle = ClusterHandle::new(outcome.bootstrap.clone());
        let guard = ClusterGuard {
            runtime: ClusterRuntime::Async,
            postgres: outcome.postgres,
            bootstrap: outcome.bootstrap,
            is_managed_via_worker: outcome.is_managed_via_worker,
            env_vars,
            worker_guard: None,
            _env_guard: env_guard,
            _cluster_span: span,
        };

        Ok((handle, guard))
    }

    /// Extends the cluster lifetime to cover additional scoped environment guards.
    ///
    /// Primarily used by fixtures that need to ensure `PG_EMBEDDED_WORKER` remains set for the
    /// duration of the cluster lifetime.
    #[doc(hidden)]
    #[must_use]
    pub fn with_worker_guard(self, worker_guard: Option<ScopedEnv>) -> Self {
        Self {
            handle: self.handle,
            guard: self.guard.with_worker_guard(worker_guard),
        }
    }

    #[cfg(feature = "async-api")]
    fn stop_async_path(&mut self) -> StopAsyncPath {
        if self.guard.is_managed_via_worker {
            StopAsyncPath::WorkerManaged
        } else if let Some(postgres) = self.guard.postgres.take() {
            StopAsyncPath::InProcess(Box::new(postgres))
        } else {
            StopAsyncPath::Noop
        }
    }

    /// Explicitly shuts down an async cluster.
    ///
    /// This method should be called for clusters created with [`start_async()`](Self::start_async)
    /// to ensure proper cleanup. It consumes `self` to prevent the `Drop` implementation from
    /// attempting duplicate shutdown.
    ///
    /// For worker-managed clusters (root privileges), the worker subprocess is invoked
    /// synchronously via `spawn_blocking`.
    ///
    /// # Errors
    ///
    /// Returns an error if the shutdown operation fails. The cluster resources are released
    /// regardless of whether shutdown succeeds.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// use pg_embedded_setup_unpriv::TestCluster;
    ///
    /// #[tokio::test]
    /// async fn test_explicit_shutdown() -> pg_embedded_setup_unpriv::BootstrapResult<()> {
    ///     let cluster = TestCluster::start_async().await?;
    ///     // ... use cluster ...
    ///     cluster.stop_async().await?;
    ///     Ok(())
    /// }
    /// ```
    #[cfg(feature = "async-api")]
    pub async fn stop_async(mut self) -> BootstrapResult<()> {
        let context = shutdown::stop_context(self.handle.settings());
        shutdown::log_async_stop(&context, self.guard.is_managed_via_worker);

        match self.stop_async_path() {
            StopAsyncPath::WorkerManaged => {
                shutdown::stop_worker_managed_async(
                    &self.guard.bootstrap,
                    &self.guard.env_vars,
                    &context,
                )
                .await
            }
            StopAsyncPath::InProcess(postgres) => {
                let cleanup = shutdown::InProcessCleanup {
                    cleanup_mode: self.guard.bootstrap.cleanup_mode,
                    settings: &self.guard.bootstrap.settings,
                    context: &context,
                };
                shutdown::stop_in_process_async(
                    *postgres,
                    self.guard.bootstrap.shutdown_timeout,
                    cleanup,
                )
                .await
            }
            StopAsyncPath::Noop => Ok(()),
        }
    }
}

/// Provides transparent access to [`ClusterHandle`] methods.
///
/// This allows `TestCluster` to be used interchangeably with `ClusterHandle`
/// for all read-only operations like `settings()`, `connection()`, etc.
impl Deref for TestCluster {
    type Target = ClusterHandle;

    fn deref(&self) -> &Self::Target {
        &self.handle
    }
}

// Note: TestCluster does NOT implement Drop because the ClusterGuard handles shutdown.
// When TestCluster drops, its _guard field drops, which triggers ClusterGuard::Drop.

#[cfg(test)]
mod mod_tests;

#[cfg(all(test, feature = "cluster-unit-tests"))]
mod drop_logging_tests;

#[cfg(all(test, not(feature = "cluster-unit-tests")))]
#[path = "../../tests/test_cluster.rs"]
mod test_cluster_tests;