use super::*;
fn workspace() -> tempfile::TempDir {
tempfile::tempdir().expect("a temporary workspace")
}
fn held_by(root: &Path, pid: u32, reason: &str) {
let holder = Holder {
command: "wsb test --on pve".into(),
argv: vec!["wsb".into(), "test".into()],
pid,
host: host_name(),
taken_at: "2026-10-04T21:00:00Z".into(),
reason: reason.into(),
protects: "the workspace".into(),
};
std::fs::create_dir_all(root.join(".wsb")).unwrap();
std::fs::write(path(root), serde_json::to_string(&holder).unwrap()).unwrap();
}
fn alive() -> u32 {
std::process::id()
}
#[test]
fn test12484_a_writer_waits_for_the_lock_and_proceeds_when_it_is_released() {
let root = workspace();
held_by(root.path(), alive(), "snapshotting for run-1");
let lock = path(root.path());
let release = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(2500));
std::fs::remove_file(lock).unwrap();
});
let started = Instant::now();
wait_until_free_with(root.path(), "checkpoint", false, Duration::from_secs(30)).expect("released");
assert!(started.elapsed() >= Duration::from_secs(2), "it did not wait for the lock");
release.join().unwrap();
}
#[test]
fn test12485_a_lock_held_past_the_cap_is_a_failure_naming_its_holder() {
let root = workspace();
held_by(root.path(), alive(), "snapshotting for run-2");
let refused = wait_until_free_with(root.path(), "checkpoint", false, Duration::from_secs(1))
.expect_err("still locked at the cap");
for said in ["wsb test --on pve", "snapshotting for run-2", FILE, "--ignore-workspace-lock"] {
assert!(refused.contains(said), "`{said}` missing from: {refused}");
}
}
#[test]
fn test12486_the_override_proceeds_through_a_held_lock() {
let root = workspace();
held_by(root.path(), alive(), "snapshotting for run-3");
let started = Instant::now();
wait_until_free_with(root.path(), "checkpoint", true, Duration::from_secs(30)).expect("ignored");
assert!(started.elapsed() < Duration::from_secs(2), "the override waited");
assert!(path(root.path()).exists(), "the override removed somebody else's lock");
}
#[test]
fn test12487_a_lock_whose_holder_is_gone_is_stale() {
let root = workspace();
let child = crate::git_backend::command_without_repo_scope(std::env::current_exe().unwrap())
.arg("--list")
.stdout(std::process::Stdio::null())
.spawn()
.unwrap();
let gone = child.id();
let mut child = child;
child.wait().unwrap();
held_by(root.path(), gone, "snapshotting for run-4");
let refused = wait_until_free_with(root.path(), "checkpoint", false, Duration::from_secs(30))
.expect_err("a stale lock is reported");
assert!(refused.contains("stale") && refused.contains(FILE), "{refused}");
}
#[test]
fn test12488_the_lock_is_exclusive_and_released_only_by_its_holder() {
let root = workspace();
let held = take_within(root.path(), "wsb test", "snapshotting for run-5", Duration::from_secs(1))
.expect("free, so taken");
assert!(path(root.path()).exists());
let second = take_within(root.path(), "wsb test", "another snapshot", Duration::from_secs(1));
assert!(second.is_err(), "a second holder took a held lock");
let before = std::fs::read_to_string(path(root.path())).unwrap();
assert!(!create_exclusively(&path(root.path()), "a racing snapshot").unwrap(), "the create was not exclusive");
assert_eq!(std::fs::read_to_string(path(root.path())).unwrap(), before, "the racing create overwrote the holder");
drop(held);
assert!(!path(root.path()).exists(), "letting the lock go did not release it");
let held = take_within(root.path(), "wsb test", "snapshotting for run-6", Duration::from_secs(1)).unwrap();
std::fs::remove_file(held.path()).unwrap();
held_by(root.path(), alive() + 1_000_000, "a later snapshot");
drop(held);
assert!(path(root.path()).exists(), "a holder released a lock that was no longer its own");
}
fn writing_as(root: &Path, pid: u32) -> PathBuf {
let record = Holder {
command: "checkpoint".into(),
argv: vec!["wsb".into(), "checkpoint".into()],
pid,
host: host_name(),
taken_at: "2026-10-07T07:00:00Z".into(),
reason: "checkpoint is writing into the tree".into(),
protects: "the workspace".into(),
};
let dir = root.join(".wsb").join(WRITERS);
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join(format!("{}-{pid}.json", host_name()));
std::fs::write(&file, serde_json::to_string(&record).unwrap()).unwrap();
file
}
#[test]
fn test12588_a_snapshot_waits_for_a_writer_under_way() {
let root = workspace();
let mut child = crate::git_backend::command_without_repo_scope(std::env::current_exe().unwrap())
.arg("--list")
.stdout(std::process::Stdio::null())
.spawn()
.unwrap();
let dead = child.id();
child.wait().unwrap();
let gone = writing_as(root.path(), dead);
let writing = writing_as(root.path(), alive());
let finish = std::thread::spawn(move || {
std::thread::sleep(Duration::from_millis(2500));
std::fs::remove_file(writing).unwrap();
});
let started = Instant::now();
let held = take_within(root.path(), "wsb test", "snapshotting the tree", Duration::from_secs(60))
.expect("the snapshot is taken once the writer is done");
assert!(
started.elapsed() >= Duration::from_secs(2),
"the snapshot was taken while a writer was still writing"
);
finish.join().unwrap();
assert!(!gone.exists(), "a dead writer's registration was left behind");
drop(held);
}
#[test]
fn test12589_a_writer_registers_for_its_run_and_its_children_do_not() {
let _env = crate::test_environment::Environment::set(&[(WRITING_ENV, ""), (IGNORE_ENV, "")]);
let root = workspace();
let dir = root.path().join(".wsb").join(WRITERS);
let writing = begin_writing(root.path(), "checkpoint").expect("registered").expect("a writer of its own");
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 1, "the writer is not registered");
assert!(std::env::var_os(WRITING_ENV).is_some(), "its children would not know they are covered");
assert!(begin_writing(root.path(), "update").expect("asked").is_none(), "a child registered beside its parent");
drop(writing);
assert_eq!(std::fs::read_dir(&dir).unwrap().count(), 0, "the registration outlived the writer");
}