use std::{
fs,
path::{Path, PathBuf},
};
use postgresql_embedded::Settings;
use rstest::rstest;
use tempfile::tempdir;
use super::{
cleanup_in_process,
is_dangerous_cleanup_path,
plan_cleanup,
should_remove_install_root,
};
use crate::{CleanupMode, bootstrap::claim_guard_at};
#[rstest]
#[case::data_only(CleanupMode::DataOnly, false, true)]
#[case::full(CleanupMode::Full, false, false)]
#[case::none(CleanupMode::None, true, true)]
fn cleanup_in_process_respects_mode(
#[case] mode: CleanupMode,
#[case] expect_data_exists: bool,
#[case] expect_install_exists: bool,
) {
let sandbox = tempdir().expect("tempdir");
let data_dir = sandbox.path().join("data");
let install_dir = sandbox.path().join("install");
fs::create_dir_all(&data_dir).expect("create data dir");
fs::create_dir_all(&install_dir).expect("create install dir");
fs::write(data_dir.join("marker"), b"data").expect("write data marker");
fs::write(install_dir.join("marker"), b"install").expect("write install marker");
let settings = Settings {
data_dir,
installation_dir: install_dir,
..Settings::default()
};
cleanup_in_process(mode, &settings, "cleanup-test");
cleanup_in_process(mode, &settings, "cleanup-test");
assert_eq!(
settings.data_dir.exists(),
expect_data_exists,
"data directory presence should match cleanup mode",
);
assert_eq!(
settings.installation_dir.exists(),
expect_install_exists,
"installation directory presence should match cleanup mode",
);
}
#[rstest]
#[case::peer_running(true, true)]
#[case::alone(false, false)]
fn full_cleanup_keeps_an_install_tree_that_a_peer_runs_from(
#[case] has_peer: bool,
#[case] expect_install_exists: bool,
) {
let sandbox = tempdir().expect("tempdir");
let slots = sandbox.path().join("data");
let own = slots.join("1-1-0");
let install_dir = sandbox.path().join("install");
fs::create_dir_all(&own).expect("own slot");
fs::create_dir_all(&install_dir).expect("install dir");
fs::write(slots.join("1-1-0.lock"), b"").expect("own lock file");
let peer_lock = fs::File::create(slots.join("2-2-0.lock")).expect("peer lock file");
if has_peer {
fs4::FileExt::lock(&peer_lock).expect("hold the peer's lock");
}
let settings = Settings {
data_dir: own,
installation_dir: install_dir,
..Settings::default()
};
cleanup_in_process(CleanupMode::Full, &settings, "peer-test");
assert!(!settings.data_dir.exists(), "the cluster's own data goes");
assert_eq!(settings.installation_dir.exists(), expect_install_exists);
}
#[rstest]
#[case::slot_running(true, true)]
#[case::none_running(false, false)]
fn full_cleanup_of_an_explicit_data_dir_respects_slots_on_its_install_tree(
#[case] has_peer: bool,
#[case] expect_install_exists: bool,
) {
let root = tempdir().expect("tempdir");
let slots = root.path().join("data");
let install_dir = root.path().join("install");
let explicit = root.path().join("elsewhere");
fs::create_dir_all(&slots).expect("slot parent");
fs::create_dir_all(&install_dir).expect("install dir");
fs::create_dir_all(&explicit).expect("explicit data dir");
let peer_lock = fs::File::create(slots.join("2-2-0.lock")).expect("peer lock file");
if has_peer {
fs4::FileExt::lock(&peer_lock).expect("hold the peer's lock");
}
let settings = Settings {
data_dir: explicit,
installation_dir: install_dir,
..Settings::default()
};
cleanup_in_process(CleanupMode::Full, &settings, "explicit-test");
assert!(!settings.data_dir.exists(), "the explicit data goes");
assert_eq!(settings.installation_dir.exists(), expect_install_exists);
}
#[test]
fn full_cleanupguards_a_slot_parent_that_does_not_exist_yet() {
let root = tempdir().expect("tempdir");
let slots = root.path().join("data");
let install_dir = root.path().join("install");
fs::create_dir_all(&install_dir).expect("install dir");
let settings = Settings {
data_dir: root.path().join("elsewhere"),
installation_dir: install_dir,
..Settings::default()
};
assert!(!slots.exists(), "the slot parent starts absent");
let plan = plan_cleanup(CleanupMode::Full, &settings, "absent-test");
assert_eq!(plan.mode, CleanupMode::Full, "nothing shares the tree");
assert!(plan.guard.is_some(), "the cleanup holds the guard");
let (sender, receiver) = std::sync::mpsc::channel();
let claimant = std::thread::spawn(move || {
let guard = claim_guard_at(&slots);
sender.send(()).expect("report the claim");
guard
});
assert!(
receiver
.recv_timeout(std::time::Duration::from_millis(500))
.is_err(),
"a first claim must wait for the cleanup"
);
drop(plan);
receiver
.recv_timeout(std::time::Duration::from_secs(30))
.expect("the claim proceeds once the cleanup is done");
claimant
.join()
.expect("join the claimant")
.expect("take the guard");
}
#[test]
fn full_cleanup_keeps_the_install_tree_when_theguard_is_unavailable() {
let sandbox = tempdir().expect("tempdir");
let slots = sandbox.path().join("data");
let own = slots.join("1-1-0");
let install_dir = sandbox.path().join("install");
fs::create_dir_all(&own).expect("own slot");
fs::create_dir_all(&install_dir).expect("install dir");
fs::write(slots.join("1-1-0.lock"), b"").expect("own lock file");
fs::create_dir_all(slots.join(".claim-guard")).expect("obstruct the guard");
let settings = Settings {
data_dir: own,
installation_dir: install_dir,
..Settings::default()
};
cleanup_in_process(CleanupMode::Full, &settings, "guard-test");
assert!(!settings.data_dir.exists(), "the cluster's own data goes");
assert!(settings.installation_dir.exists(), "the install tree stays");
}
#[cfg(unix)]
#[test]
fn full_cleanup_keeps_the_install_tree_when_the_slot_lookup_is_denied() {
use std::os::unix::fs::PermissionsExt;
if nix::unistd::geteuid().is_root() {
return;
}
let sandbox = tempdir().expect("tempdir");
let slots = sandbox.path().join("data");
let own = slots.join("1-1-0");
let install_dir = sandbox.path().join("install");
fs::create_dir_all(&own).expect("own slot");
fs::create_dir_all(&install_dir).expect("install dir");
fs::write(slots.join("1-1-0.lock"), b"").expect("own lock file");
fs::set_permissions(&slots, fs::Permissions::from_mode(0o600)).expect("deny search");
let settings = Settings {
data_dir: own,
installation_dir: install_dir,
..Settings::default()
};
cleanup_in_process(CleanupMode::Full, &settings, "denied-test");
fs::set_permissions(&slots, fs::Permissions::from_mode(0o700)).expect("restore search");
assert!(settings.installation_dir.exists(), "the install tree stays");
}
#[rstest]
#[case::nested_under_install("/opt/pg/install", "/opt/pg/install/secrets", true)]
#[case::equal_to_install("/opt/pg/install", "/opt/pg/install", false)]
#[case::outside_install("/opt/pg/install", "/elsewhere/secrets", false)]
#[case::parent_dir_traversal("/opt/pg/install", "/opt/pg/install/../evil", false)]
fn should_remove_install_root_classifies_parent(
#[case] install: &str,
#[case] parent: &str,
#[case] expected: bool,
) {
let settings = Settings {
installation_dir: PathBuf::from(install),
..Settings::default()
};
assert_eq!(
should_remove_install_root(Path::new(parent), &settings),
expected,
"unexpected installation-root removal decision",
);
}
#[test]
fn is_dangerous_cleanup_path_flags_root_and_empty() {
assert!(
is_dangerous_cleanup_path(Path::new("")),
"an empty path must be flagged as dangerous"
);
let root = std::env::current_dir()
.expect("resolve current dir")
.ancestors()
.last()
.expect("an absolute directory has a root ancestor")
.to_path_buf();
assert!(
is_dangerous_cleanup_path(&root),
"filesystem root {root:?} must be flagged as dangerous"
);
assert!(
!is_dangerous_cleanup_path(Path::new("data/pg-embed")),
"an ordinary relative path must not be flagged"
);
}