use std::process::Command;
fn bin() -> &'static str {
env!("CARGO_BIN_EXE_powdb-cli")
}
fn tmp(tag: &str) -> std::path::PathBuf {
let p = std::env::temp_dir().join(format!(
"powdb_liveguard_{tag}_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let _ = std::fs::remove_dir_all(&p);
p
}
fn run(args: &[&str]) -> std::process::Output {
Command::new(bin())
.args(args)
.output()
.expect("failed to run powdb-cli")
}
fn seed(data_s: &str) {
assert!(
run(&["--data-dir", data_s, "-c", "type T { required id: int }"])
.status
.success()
);
assert!(run(&[
"--data-dir",
data_s,
"-c",
"insert T { id := 1 }; insert T { id := 2 }; insert T { id := 3 }"
])
.status
.success());
}
fn plant_live_writer(data_dir: &std::path::Path) -> std::process::Child {
let child = Command::new("sleep")
.arg("60")
.spawn()
.expect("failed to spawn sleeper process");
std::fs::write(data_dir.join("LOCK"), child.id().to_string()).expect("failed to write LOCK");
child
}
#[test]
fn backup_refuses_data_dir_held_by_live_writer() {
let data = tmp("bk_data");
let dest = tmp("bk_dest");
let data_s = data.to_str().unwrap();
seed(data_s);
let mut sleeper = plant_live_writer(&data);
let wal_len_before = std::fs::metadata(data.join("wal.log"))
.map(|m| m.len())
.ok();
let out = run(&["--data-dir", data_s, "backup", dest.to_str().unwrap()]);
let _ = sleeper.kill();
let _ = sleeper.wait();
assert!(
!out.status.success(),
"backup against a data dir held by a live writer must be refused, \
but it exited 0 with stdout: {:?}",
String::from_utf8_lossy(&out.stdout)
);
let wal_len_after = std::fs::metadata(data.join("wal.log"))
.map(|m| m.len())
.ok();
assert_eq!(
wal_len_before, wal_len_after,
"backup mutated the live directory's wal.log despite being refused"
);
assert!(
!dest.join("manifest.json").exists(),
"a refused backup must not leave a manifest at the destination"
);
}
#[test]
fn sweep_refuses_data_dir_held_by_live_writer() {
let data = tmp("sw_data");
let data_s = data.to_str().unwrap();
seed(data_s);
let mut sleeper = plant_live_writer(&data);
let out = run(&["--data-dir", data_s, "sweep", "T"]);
let _ = sleeper.kill();
let _ = sleeper.wait();
assert!(
!out.status.success(),
"sweep against a data dir held by a live writer must be refused, \
but it exited 0 with stdout: {:?}",
String::from_utf8_lossy(&out.stdout)
);
}
#[test]
fn backup_takes_over_stale_lock_from_dead_process() {
let data = tmp("stale_data");
let dest = tmp("stale_dest");
let data_s = data.to_str().unwrap();
seed(data_s);
let mut child = Command::new("true").spawn().expect("spawn");
let dead_pid = child.id();
child.wait().expect("wait");
std::fs::write(data.join("LOCK"), dead_pid.to_string()).expect("write LOCK");
let out = run(&["--data-dir", data_s, "backup", dest.to_str().unwrap()]);
assert!(
out.status.success(),
"backup must take over a stale LOCK from a dead process: {}",
String::from_utf8_lossy(&out.stderr)
);
assert!(dest.join("manifest.json").exists());
}