#![cfg(unix)]
use std::{thread, time::Duration};
use camino::Utf8PathBuf;
use color_eyre::eyre::{Context, Result, ensure, eyre};
use pg_embedded_setup_unpriv::TestCluster;
use rstest::rstest;
#[path = "support/cap_fs_bootstrap.rs"]
mod cap_fs;
#[path = "support/cluster_skip.rs"]
mod cluster_skip;
#[path = "support/env.rs"]
mod env;
#[path = "support/env_snapshot.rs"]
mod env_snapshot;
#[path = "support/sandbox.rs"]
mod sandbox;
#[path = "support/serial.rs"]
mod serial;
#[path = "support/skip.rs"]
mod skip;
use cluster_skip::cluster_skip_message;
use env_snapshot::EnvSnapshot;
use sandbox::TestSandbox;
use serial::{ScenarioSerialGuard, serial_guard};
#[rstest]
fn new_split_creates_working_handle_and_guard(serial_guard: ScenarioSerialGuard) -> Result<()> {
let sandbox = TestSandbox::new("split-constructor").context("create test sandbox")?;
sandbox.reset()?;
let env_before = EnvSnapshot::capture();
let result = sandbox.with_env(sandbox.env_without_timezone(), run_split_lifecycle_test);
if should_skip_on_error(&result) {
return Ok(());
}
let data_dir = result?;
let env_after = EnvSnapshot::capture();
ensure!(
env_before == env_after,
"environment should be restored after guard drops"
);
wait_for_postmaster_shutdown(&data_dir)?;
drop(serial_guard);
Ok(())
}
fn run_split_lifecycle_test() -> std::result::Result<Utf8PathBuf, color_eyre::Report> {
let (handle, guard) = TestCluster::new_split().map_err(color_eyre::Report::from)?;
let data_dir = Utf8PathBuf::from_path_buf(handle.settings().data_dir.clone())
.map_err(|_| eyre!("data_dir is not valid UTF-8"))?;
ensure!(
data_dir.join("postmaster.pid").exists(),
"postmaster.pid should exist while cluster runs"
);
ensure!(
handle
.database_exists("postgres")
.map_err(color_eyre::Report::from)?,
"postgres database should exist"
);
drop(guard);
Ok(data_dir)
}
#[rstest]
fn test_cluster_derefs_to_cluster_handle(serial_guard: ScenarioSerialGuard) -> Result<()> {
let sandbox = TestSandbox::new("deref-test").context("create test sandbox")?;
sandbox.reset()?;
let result = sandbox.with_env(sandbox.env_without_timezone(), run_deref_test);
if should_skip_on_error(&result) {
return Ok(());
}
result?;
drop(serial_guard);
Ok(())
}
fn run_deref_test() -> std::result::Result<(), color_eyre::Report> {
let cluster = TestCluster::new().map_err(color_eyre::Report::from)?;
let _settings = cluster.settings();
let _environment = cluster.environment();
let _bootstrap = cluster.bootstrap();
ensure!(
cluster
.database_exists("postgres")
.map_err(color_eyre::Report::from)?,
"should access database_exists through Deref"
);
Ok(())
}
fn should_skip_on_error<T>(result: &std::result::Result<T, color_eyre::Report>) -> bool {
let Err(err) = result else {
return false;
};
let message = err.to_string();
let debug = format!("{err:?}");
cluster_skip_message(&message, Some(&debug))
.map(|reason| {
tracing::warn!("{reason}");
})
.is_some()
}
#[cfg(feature = "async-api")]
#[rstest]
fn start_async_split_creates_working_handle_and_guard(
serial_guard: ScenarioSerialGuard,
) -> Result<()> {
let sandbox = TestSandbox::new("async-split").context("create test sandbox")?;
sandbox.reset()?;
let env_before = EnvSnapshot::capture();
let result = sandbox.with_env(sandbox.env_without_timezone(), || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|e| eyre!("failed to build runtime: {e}"))?
.block_on(run_async_split_lifecycle_test())
});
if should_skip_on_error(&result) {
return Ok(());
}
result?;
let env_after = EnvSnapshot::capture();
ensure!(
env_before == env_after,
"environment should be restored after guard drops"
);
drop(serial_guard);
Ok(())
}
#[cfg(feature = "async-api")]
async fn run_async_split_lifecycle_test() -> std::result::Result<(), color_eyre::Report> {
let (handle, guard) = TestCluster::start_async_split()
.await
.map_err(color_eyre::Report::from)?;
let data_dir = Utf8PathBuf::from_path_buf(handle.settings().data_dir.clone())
.map_err(|_| eyre!("data_dir is not valid UTF-8"))?;
ensure!(
data_dir.join("postmaster.pid").exists(),
"postmaster.pid should exist while cluster runs"
);
drop(guard);
wait_for_postmaster_shutdown_async(&data_dir).await?;
Ok(())
}
const POSTMASTER_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(10);
const POSTMASTER_POLL_INTERVAL: Duration = Duration::from_millis(50);
const POSTMASTER_SHUTDOWN_ERROR: &str =
"postmaster.pid should be removed once cluster stops (waited 10s)";
fn wait_for_postmaster_shutdown(data_dir: &Utf8PathBuf) -> Result<()> {
use std::time::Instant;
let pid = data_dir.join("postmaster.pid");
let deadline = Instant::now() + POSTMASTER_SHUTDOWN_TIMEOUT;
while pid.exists() && Instant::now() < deadline {
thread::sleep(POSTMASTER_POLL_INTERVAL);
}
ensure!(!pid.exists(), POSTMASTER_SHUTDOWN_ERROR);
Ok(())
}
#[cfg(feature = "async-api")]
async fn wait_for_postmaster_shutdown_async(
data_dir: &Utf8PathBuf,
) -> std::result::Result<(), color_eyre::Report> {
use std::time::Instant;
use tokio::time::sleep;
let pid = data_dir.join("postmaster.pid");
let deadline = Instant::now() + POSTMASTER_SHUTDOWN_TIMEOUT;
while pid.exists() && Instant::now() < deadline {
sleep(POSTMASTER_POLL_INTERVAL).await;
}
ensure!(!pid.exists(), POSTMASTER_SHUTDOWN_ERROR);
Ok(())
}