blob: 2191d0f3f398f84c29e517527ef530debf2a2646 [file] [log] [blame]
use std::time::Duration;
use tame_index::utils::flock::LockOptions;
mod utils;
enum LockKind {
Exclusive,
Shared,
}
impl LockKind {
fn as_str(&self) -> &'static str {
match self {
Self::Exclusive => "exclusive",
Self::Shared => "shared",
}
}
}
fn spawn(kind: LockKind, path: &tame_index::Path) -> std::process::Child {
let mut cmd = std::process::Command::new("cargo");
cmd.env("RUST_BACKTRACE", "1")
.args([
"run",
"-q",
"--manifest-path",
"tests/flock/Cargo.toml",
"--",
])
.stdout(std::process::Stdio::piped())
.arg(kind.as_str())
.arg(path);
let mut child = cmd.spawn().expect("failed to spawn flock");
// Wait for the child to actually take the lock
{
use std::io::Read;
let mut output = child.stdout.take().unwrap();
let mut buff = [0u8; 4];
output.read_exact(&mut buff).expect("failed to read output");
assert_eq!(
'🔒',
char::from_u32(u32::from_le_bytes(buff)).expect("invalid char")
);
}
child
}
fn kill(mut child: std::process::Child) {
child.kill().expect("failed to kill child");
child.wait().expect("failed to wait for child");
}
/// Validates we can take a lock we know is uncontended
#[test]
fn can_take_lock() {
let td = utils::tempdir();
let ctl = td.path().join("can-take-lock");
let lo = LockOptions::new(&ctl).exclusive(false);
let _lf = lo
.lock(|_p| unreachable!("lock is uncontested"))
.expect("failed to acquire lock");
}
/// Validates we can create parent directories for a lock file if they don't exist
#[test]
fn can_take_lock_in_non_existant_directory() {
let td = utils::tempdir();
let ctl = td.path().join("sub/dir/can-take-lock");
let lo = LockOptions::new(&ctl).exclusive(false);
let _lf = lo.try_lock().expect("failed to acquire lock");
}
/// Validates we can take multiple shared locks of the same file
#[test]
fn can_take_shared_lock() {
let td = utils::tempdir();
let ctl = td.path().join("can-take-shared-lock");
let _ = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(&ctl)
.expect("failed to create lock file");
let child = spawn(LockKind::Shared, &ctl);
let mut lo = LockOptions::new(&ctl);
lo = lo.shared();
lo.try_lock().expect("failed to acquire shared lock");
lo = lo.exclusive(false);
if lo.try_lock().is_ok() {
panic!("we acquired an exclusive lock but we shouldn't have been able to");
}
kill(child);
lo.try_lock().expect("failed to acquire exclusive lock");
}
/// Validates we can wait for a lock to be released
#[test]
fn waits_lock() {
let td = utils::tempdir();
let ctl = td.path().join("waits-lock");
let _ = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(&ctl)
.expect("failed to create lock file");
let child = spawn(LockKind::Exclusive, &ctl);
std::thread::scope(|s| {
s.spawn(|| {
LockOptions::new(&ctl)
.lock(|_p| {
println!("waiting on lock");
Some(Duration::from_millis(200))
})
.expect("failed to acquire shared lock");
});
s.spawn(|| {
std::thread::sleep(Duration::from_millis(100));
kill(child);
});
});
}
/// Ensures we can timeout if it takes too long to acquire the lock
#[test]
fn wait_lock_times_out() {
let td = utils::tempdir();
let ctl = td.path().join("wait-lock-times-out");
let _ = std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(&ctl)
.expect("failed to create lock file");
let child = spawn(LockKind::Exclusive, &ctl);
if let Err(err) = LockOptions::new(&ctl).shared().lock(|_p| {
println!("waiting on lock");
Some(Duration::from_millis(100))
}) {
let tame_index::Error::Lock(le) = err else {
panic!("unexpected error type {err:#?}");
};
assert!(matches!(
le.source,
tame_index::utils::flock::LockError::TimedOut
));
} else {
panic!("we should not be able to take the lock");
}
kill(child);
}