use super::*;
use crate::tests::support::temp_dir;
#[cfg(unix)]
use crate::tests::support::{
hold_at_acknowledged_barrier, kill_child_at_acknowledged_barrier, wait_for_child_path,
};
#[cfg(unix)]
use rustix::io::{FdFlags, fcntl_getfd};
#[cfg(unix)]
use std::{os::unix::fs::symlink, process::Command};
const CHILD_JOURNAL_ENV: &str = "IC_BACKUP_TEST_JOURNAL_LOCK_CHILD_PATH";
#[cfg(unix)]
#[test]
fn lock_excludes_a_second_owner_and_reacquires_after_drop() {
let root = temp_dir("ic-backup-journal-lock");
let journal_path = root.join("journal");
let lock_path = journal_lock_path(&journal_path);
let lock = JournalLock::acquire(&journal_path).expect("acquire lock");
assert!(lock_path.is_file());
assert!(
fcntl_getfd(&lock.file)
.expect("read lock descriptor flags")
.contains(FdFlags::CLOEXEC)
);
assert!(matches!(
JournalLock::acquire(&journal_path),
Err(JournalLockError::Locked { lock_path: actual })
if actual == lock_path.to_string_lossy()
));
drop(lock);
assert!(lock_path.is_file());
let reacquired = JournalLock::acquire(&journal_path).expect("reacquire lock");
drop(reacquired);
fs::remove_file(lock_path).expect("remove lock path");
fs::remove_dir(root).expect("remove fixture root");
}
#[cfg(unix)]
#[test]
fn lock_rejects_symlink_and_special_entry_substitution() {
let root = temp_dir("ic-backup-journal-lock-unsafe");
let journal_path = root.join("journal");
let lock_path = journal_lock_path(&journal_path);
let target = root.join("target");
symlink(&target, &lock_path).expect("create lock symlink");
assert!(matches!(
JournalLock::acquire(&journal_path),
Err(JournalLockError::UnsafeEntry { lock_path: actual, kind })
if actual == lock_path.to_string_lossy() && kind == "Symlink"
));
fs::remove_file(&lock_path).expect("remove lock symlink");
fs::create_dir(&lock_path).expect("create lock directory");
assert!(matches!(
JournalLock::acquire(&journal_path),
Err(JournalLockError::UnsafeEntry { lock_path: actual, kind })
if actual == lock_path.to_string_lossy() && kind == "Directory"
));
fs::remove_dir(lock_path).expect("remove lock directory");
fs::remove_dir(root).expect("remove fixture root");
}
#[cfg(unix)]
#[test]
fn process_death_releases_lock_without_sidecar_removal() {
let root = temp_dir("ic-backup-journal-lock-process-death");
let journal_path = root.join("journal");
let mut child = Command::new(std::env::current_exe().expect("resolve test executable"))
.args([
"--exact",
"ops::persistence::journal_lock::tests::process_death_child_holds_lock",
"--nocapture",
])
.env(CHILD_JOURNAL_ENV, &journal_path)
.spawn()
.expect("spawn lock owner");
wait_for_child_path(&mut child, &root.join("barrier-ready"), "lock owner");
let excluded = matches!(
JournalLock::acquire(&journal_path),
Err(JournalLockError::Locked { .. })
);
kill_child_at_acknowledged_barrier(&mut child, &root);
assert!(excluded, "live child retains exclusive lock custody");
assert!(journal_lock_path(&journal_path).is_file());
let lock = JournalLock::acquire(&journal_path).expect("reacquire after process death");
drop(lock);
fs::remove_dir_all(root).expect("remove successful fixture");
}
#[cfg(unix)]
#[test]
fn process_death_child_holds_lock() {
let Some(journal_path) = std::env::var_os(CHILD_JOURNAL_ENV) else {
return;
};
let journal_path = Path::new(&journal_path);
let _lock = JournalLock::acquire(journal_path).expect("child acquire lock");
hold_at_acknowledged_barrier(journal_path.parent().expect("fixture root"));
}