use super::*;
use crate::test_support::{fixture, remove};
fn index_in(name: &str) -> (PathBuf, PathBuf) {
let root: PathBuf = fixture(name);
let index: PathBuf = root.join("index");
return (root, index);
}
#[test]
#[cfg(unix)]
fn a_free_index_is_locked_and_released() {
use std::os::unix::fs::MetadataExt;
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-free");
let lock: RealIndexLock = match acquire_real_index_lock(index.as_path(), 100, "did nothing") {
Ok(found) => found,
Err(failure) => panic!("a free index is lockable: {failure:?}"),
};
let metadata: std::fs::Metadata = std::fs::symlink_metadata(real_lock_path(index.as_path())).expect("the lock exists");
assert_eq!(lock.identity().file.device, metadata.dev().to_string());
assert_eq!(lock.identity().file.inode, metadata.ino().to_string());
assert!(!lock.identity().fs_id.is_empty());
let pid: String = std::fs::read_to_string(real_pid_path(index.as_path())).expect("PID file");
assert_eq!(pid, format!("pid {}\n", std::process::id()));
lock.release().expect("release");
assert!(!real_lock_path(index.as_path()).exists());
assert!(!real_pid_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_replaced_lock_is_never_released() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-replaced");
let lock: RealIndexLock = match acquire_real_index_lock(index.as_path(), 100, "did nothing") {
Ok(found) => found,
Err(failure) => panic!("a free index is lockable: {failure:?}"),
};
let path: PathBuf = real_lock_path(index.as_path());
std::fs::remove_file(&path).expect("remove the created lock");
std::fs::write(&path, b"another process's lock").expect("replacement");
let refusal: String = lock.release().expect_err("a replaced lock must not be released");
assert!(refusal.contains("identity changed"), "{refusal}");
assert_eq!(std::fs::read(&path).expect("replacement kept"), b"another process's lock");
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_dead_foreign_owner_exhausts_the_budget() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-dead");
std::fs::write(real_lock_path(index.as_path()), b"").expect("foreign lock");
std::fs::write(real_pid_path(index.as_path()), "pid 2147483646\n").expect("dead PID file");
let failure: CoordinationFailure = match acquire_real_index_lock(index.as_path(), 50, "landed nothing") {
Ok(_) => panic!("a dead owner must not be overtaken"),
Err(found) => found,
};
assert!(matches!(failure, CoordinationFailure::UnprovenOwner(_)));
assert!(real_lock_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_lock_with_one_mismatched_identity_field_is_refused() {
use std::os::unix::fs::MetadataExt;
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-fields");
let lock: RealIndexLock = match acquire_real_index_lock(index.as_path(), 100, "did nothing") {
Ok(found) => found,
Err(failure) => panic!("a free index is lockable: {failure:?}"),
};
let path: PathBuf = real_lock_path(index.as_path());
let metadata: std::fs::Metadata = std::fs::symlink_metadata(&path).expect("the lock is stat-ed");
let fs_id: String = lock.identity().fs_id.clone();
let wrong_device: LockIdentity = LockIdentity {
file: FileIdentity { device: (metadata.dev() + 1).to_string(), inode: metadata.ino().to_string() },
fs_id: fs_id.clone(),
};
let wrong_inode: LockIdentity = LockIdentity {
file: FileIdentity { device: metadata.dev().to_string(), inode: (metadata.ino() + 1).to_string() },
fs_id,
};
assert!(same_file_identity(path.as_path(), &wrong_device).is_err(), "a wrong device is refused");
assert!(same_file_identity(path.as_path(), &wrong_inode).is_err(), "a wrong inode is refused");
assert!(same_file_identity(path.as_path(), lock.identity()).is_ok(), "the own identity is accepted");
lock.release().expect("the real lock releases");
remove(root.as_path());
}
fn transaction_in(root: &Path) -> PathBuf {
let directory: PathBuf = root.join("transaction");
std::fs::create_dir(&directory).expect("transaction directory");
return directory;
}
fn acquire_bounded(index: &Path, directory: &Path) -> Result<RealIndexLock, CoordinationFailure> {
let (sender, receiver) = std::sync::mpsc::channel();
let owned_index: PathBuf = index.to_path_buf();
let owned_directory: PathBuf = directory.to_path_buf();
std::thread::spawn(move || {
let _ = sender.send(acquire_landing_index_lock(owned_index.as_path(), owned_directory.as_path(), 1, 100, "landed nothing"));
});
return receiver
.recv_timeout(std::time::Duration::from_secs(30))
.expect("the acquisition ended instead of waiting for ever");
}
#[test]
#[cfg(unix)]
fn a_landing_lock_journals_its_identity_and_announces_the_process() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-journal");
let directory: PathBuf = transaction_in(root.as_path());
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 3, 100, "landed nothing")
.expect("a free index is lockable");
let bytes: Vec<u8> = std::fs::read(directory.join("index-lock-3.json")).expect("the record exists");
let expected: String = encode_index_lock(&IndexLockRecord { attempt: 3, lock: lock.identity().clone() });
assert_eq!(String::from_utf8(bytes).expect("UTF-8 record"), expected);
assert_eq!(
std::fs::read_to_string(real_pid_path(index.as_path())).expect("PID file"),
format!("pid {}\n", std::process::id())
);
lock.release_after_landing().expect("release");
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_failed_journal_write_leaves_no_lock_and_no_pid_file() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-journal-fails");
let directory: PathBuf = transaction_in(root.as_path());
std::fs::write(directory.join("index-lock-1.json"), b"an earlier record").expect("blocking record");
let failure: CoordinationFailure =
match acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing") {
Ok(_) => panic!("the record cannot be written twice"),
Err(found) => found,
};
assert!(matches!(&failure, CoordinationFailure::Stop(text) if text.contains("index-lock-1.json")), "{failure:?}");
assert!(!real_lock_path(index.as_path()).exists());
assert!(!real_pid_path(index.as_path()).exists());
assert_eq!(std::fs::read(directory.join("index-lock-1.json")).expect("kept"), b"an earlier record");
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn an_existing_pid_file_is_left_alone() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-pid-exists");
let directory: PathBuf = transaction_in(root.as_path());
std::fs::write(real_pid_path(index.as_path()), "pid 2147483646\n").expect("foreign PID file");
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing")
.expect("the lock is free");
assert_eq!(std::fs::read_to_string(real_pid_path(index.as_path())).expect("PID file"), "pid 2147483646\n");
assert!(real_lock_path(index.as_path()).exists());
lock.release_after_landing().expect("release");
assert!(real_pid_path(index.as_path()).exists(), "a PID file that names another process stays");
assert!(!real_lock_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_landing_lock_behind_a_dead_owner_writes_no_record() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-landing-dead");
let directory: PathBuf = transaction_in(root.as_path());
std::fs::write(real_lock_path(index.as_path()), b"").expect("foreign lock");
std::fs::write(real_pid_path(index.as_path()), "pid 2147483646\n").expect("dead PID file");
let failure: CoordinationFailure =
match acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 50, "landed nothing") {
Ok(_) => panic!("a dead owner must not be overtaken"),
Err(found) => found,
};
assert!(matches!(failure, CoordinationFailure::UnprovenOwner(_)));
assert!(!directory.join("index-lock-1.json").exists());
assert!(real_lock_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn an_install_replaces_the_index_with_the_lock_file_and_releases_everything() {
use std::os::unix::fs::MetadataExt;
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-install");
let directory: PathBuf = transaction_in(root.as_path());
std::fs::write(index.as_path(), b"old index").expect("old index");
let post: PathBuf = directory.join("post-1.index");
std::fs::write(post.as_path(), b"post index").expect("post index");
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing")
.expect("a free index is lockable");
let lock_inode: u64 = std::fs::symlink_metadata(real_lock_path(index.as_path())).expect("lock").ino();
lock.install(post.as_path()).expect("install");
assert_eq!(std::fs::read(index.as_path()).expect("installed index"), b"post index");
assert_eq!(std::fs::symlink_metadata(index.as_path()).expect("index").ino(), lock_inode);
assert!(!real_lock_path(index.as_path()).exists());
assert!(!real_pid_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn an_install_through_a_replaced_lock_is_refused() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-install-replaced");
let directory: PathBuf = transaction_in(root.as_path());
std::fs::write(index.as_path(), b"old index").expect("old index");
let post: PathBuf = directory.join("post-1.index");
std::fs::write(post.as_path(), b"post index").expect("post index");
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing")
.expect("a free index is lockable");
let path: PathBuf = real_lock_path(index.as_path());
std::fs::remove_file(&path).expect("remove the created lock");
std::fs::write(&path, b"another process's lock").expect("replacement");
let refusal: RecoveryError = lock.install(post.as_path()).expect_err("a replaced lock must not be written");
assert!(refusal.0.contains("identity changed"), "{refusal:?}");
assert_eq!(std::fs::read(index.as_path()).expect("old index kept"), b"old index");
assert_eq!(std::fs::read(&path).expect("replacement kept"), b"another process's lock");
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn releasing_after_a_landing_removes_the_lock_and_the_pid_file() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-release-after");
let directory: PathBuf = transaction_in(root.as_path());
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing")
.expect("a free index is lockable");
lock.release_after_landing().expect("release");
assert!(!real_lock_path(index.as_path()).exists());
assert!(!real_pid_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn releasing_after_a_landing_leaves_a_replaced_lock() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-release-replaced");
let directory: PathBuf = transaction_in(root.as_path());
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing")
.expect("a free index is lockable");
let path: PathBuf = real_lock_path(index.as_path());
std::fs::remove_file(&path).expect("remove the created lock");
std::fs::write(&path, b"another process's lock").expect("replacement");
lock.release_after_landing().expect("a replaced lock only warns");
assert_eq!(std::fs::read(&path).expect("replacement kept"), b"another process's lock");
assert!(!real_pid_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn releasing_after_a_landing_tolerates_a_vanished_lock() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-release-vanished");
let directory: PathBuf = transaction_in(root.as_path());
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing")
.expect("a free index is lockable");
std::fs::remove_file(real_lock_path(index.as_path())).expect("remove the created lock");
lock.release_after_landing().expect("an absent lock only warns");
assert!(!real_pid_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn releasing_after_a_landing_tolerates_a_vanished_pid_file() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-release-no-pid");
let directory: PathBuf = transaction_in(root.as_path());
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing")
.expect("a free index is lockable");
std::fs::remove_file(real_pid_path(index.as_path())).expect("remove the PID file");
lock.release_after_landing().expect("an absent PID file is not an error");
assert!(!real_lock_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn releasing_after_a_landing_reports_an_unreadable_pid_file() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-release-bad-pid");
let directory: PathBuf = transaction_in(root.as_path());
let lock: RealIndexLock = acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing")
.expect("a free index is lockable");
std::fs::remove_file(real_pid_path(index.as_path())).expect("remove the PID file");
std::fs::create_dir(real_pid_path(index.as_path())).expect("a directory where the PID file was");
let failure: String = lock.release_after_landing().expect_err("a directory cannot be read as the PID file");
assert!(!failure.is_empty());
assert!(!real_lock_path(index.as_path()).exists());
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_pid_file_that_cannot_be_created_fails_the_acquisition() {
let root: PathBuf = crate::test_support::fixture("real-lock-pid-too-long");
let directory: PathBuf = transaction_in(root.as_path());
let index: PathBuf = root.join("i".repeat(250));
let failure: CoordinationFailure =
match acquire_landing_index_lock(index.as_path(), directory.as_path(), 1, 100, "landed nothing") {
Ok(_) => panic!("the PID file name is too long"),
Err(found) => found,
};
assert!(matches!(&failure, CoordinationFailure::Stop(text) if text.contains("~pid.lock")), "{failure:?}");
assert!(!real_lock_path(index.as_path()).exists(), "the lock is removed again");
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_free_index_is_locked_without_waiting() {
let (root, index): (PathBuf, PathBuf) = index_in("real-lock-no-wait");
let directory: PathBuf = transaction_in(root.as_path());
let lock: RealIndexLock = acquire_bounded(index.as_path(), directory.as_path()).expect("a free index is lockable");
assert!(real_lock_path(index.as_path()).exists());
lock.release_after_landing().expect("release");
remove(root.as_path());
}
#[test]
#[cfg(unix)]
fn a_lock_that_cannot_be_created_is_a_stop_and_not_a_wait() {
let root: PathBuf = crate::test_support::fixture("real-lock-no-directory");
let directory: PathBuf = transaction_in(root.as_path());
let index: PathBuf = root.join("missing").join("index");
let failure: CoordinationFailure = match acquire_bounded(index.as_path(), directory.as_path()) {
Ok(_) => panic!("there is no directory for the lock"),
Err(found) => found,
};
assert!(matches!(failure, CoordinationFailure::Stop(_)), "{failure:?}");
remove(root.as_path());
}