#![cfg(unix)]
use super::*;
use crate::test_support::{fixture, remove};
use crate::process_tree::TreeMember;
use std::process::Child;
use std::time::Duration;
struct Repo {
root: PathBuf,
git_dir: PathBuf,
}
fn repo(name: &str) -> Repo {
let root: PathBuf = fixture(name);
let git_dir: PathBuf = root.join(".git");
std::fs::create_dir_all(git_dir.join("refs").join("remotes").join("origin")).expect("ref directory");
return Repo { root, git_dir };
}
fn lock_of(repo: &Repo, reference: &str) -> PathBuf {
return repo.git_dir.join("refs").join("remotes").join("origin").join(format!("{reference}.lock"));
}
fn pid_file_of(repo: &Repo, reference: &str) -> PathBuf {
return repo.git_dir.join("refs").join("remotes").join("origin").join(format!("{reference}{PID_LOCK_SUFFIX}"));
}
fn ledger_of(repo: &Repo) -> LockLedger {
let mut command: Command = Command::new("git");
command.current_dir(repo.root.as_path());
return LockLedger::before_start(&command).with_judge(crate::stale_ref_lock_killed_git::judge());
}
fn ended_member() -> TreeMember {
let mut child: Child = Command::new("sleep").arg("0.3").spawn().expect("start sleep");
let member: TreeMember = crate::process_tree::member_of(i32::try_from(child.id()).expect("pid")).expect("a running process");
child.wait().expect("reap sleep");
return member;
}
fn running_member() -> (TreeMember, Child) {
let child: Child = crate::test_support::spawn_owned(Command::new("sleep").arg("30")).expect("start sleep");
let member: TreeMember = crate::process_tree::member_of(i32::try_from(child.id()).expect("pid")).expect("a running process");
return (member, child);
}
fn write_pid_file(path: &Path, pid: i32) {
std::fs::write(path, format!("pid {pid}\n")).expect("pid file");
}
#[test]
fn a_lock_named_by_an_ended_member_goes_with_its_pid_file() {
let repo: Repo = repo("stopped-locks-named");
let ledger: LockLedger = ledger_of(&repo);
let member: TreeMember = ended_member();
std::fs::write(lock_of(&repo, "main"), "0123\n").expect("lock with content");
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), vec![lock_of(&repo, "main")]);
assert!(!lock_of(&repo, "main").exists());
assert!(!pid_file_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn a_lock_named_by_a_running_process_stays() {
let repo: Repo = repo("stopped-locks-running");
let ledger: LockLedger = ledger_of(&repo);
let (member, mut child): (TreeMember, Child) = running_member();
std::fs::write(lock_of(&repo, "main"), "0123\n").expect("lock with content");
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
child.kill().expect("end sleep");
child.wait().expect("reap sleep");
remove(repo.root.as_path());
}
#[test]
fn a_lock_with_content_and_no_pid_file_stays() {
let repo: Repo = repo("stopped-locks-content");
let ledger: LockLedger = ledger_of(&repo);
std::fs::write(lock_of(&repo, "main"), "0123456789012345678901234567890123456789\n").expect("lock with content");
assert_eq!(ledger.sweep(&[]), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn an_empty_lock_with_a_pid_file_is_decided_by_the_pid_file() {
let repo: Repo = repo("stopped-locks-empty-pid");
let ledger: LockLedger = ledger_of(&repo);
std::fs::write(lock_of(&repo, "main"), "").expect("empty lock");
write_pid_file(pid_file_of(&repo, "main").as_path(), i32::try_from(std::process::id()).expect("pid"));
assert_eq!(ledger.sweep(&[]), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn only_regular_lock_files_are_candidates() {
let repo: Repo = repo("stopped-locks-other-files");
let ledger: LockLedger = ledger_of(&repo);
let plain: PathBuf = repo.git_dir.join("refs").join("remotes").join("origin").join("main");
std::fs::write(plain.as_path(), "").expect("empty ref");
std::fs::create_dir(lock_of(&repo, "directory")).expect("directory named like a lock");
assert_eq!(ledger.sweep(&[]), Vec::<PathBuf>::new());
assert!(plain.exists());
assert!(lock_of(&repo, "directory").is_dir());
remove(repo.root.as_path());
}
#[test]
fn the_empty_ledger_removes_nothing() {
let repo: Repo = repo("stopped-locks-none");
let ledger: LockLedger = LockLedger::none();
let member: TreeMember = ended_member();
std::fs::write(lock_of(&repo, "main"), "0123\n").expect("lock with content");
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn the_repository_is_found_from_the_working_directory_or_the_environment() {
let repo: Repo = repo("stopped-locks-roots");
let nested: PathBuf = repo.root.join("a").join("b");
std::fs::create_dir_all(nested.as_path()).expect("nested");
let mut command: Command = Command::new("git");
command.current_dir(nested.as_path());
assert_eq!(LockLedger::before_start(&command).roots, vec![repo.git_dir.clone()]);
let linked: PathBuf = repo.root.join("linked");
std::fs::create_dir_all(linked.as_path()).expect("linked");
let admin: PathBuf = repo.git_dir.join("worktrees").join("linked");
std::fs::create_dir_all(admin.as_path()).expect("admin");
std::fs::write(admin.join("commondir"), "../..\n").expect("commondir");
std::fs::write(linked.join(".git"), format!("gitdir: {}\n", admin.display())).expect("gitdir file");
let mut in_linked: Command = Command::new("git");
in_linked.current_dir(linked.as_path());
let roots: Vec<PathBuf> = LockLedger::before_start(&in_linked).roots;
assert_eq!(roots.first(), Some(&admin));
assert_eq!(roots.len(), 2, "{roots:?}");
let mut named: Command = Command::new("git");
named.current_dir(linked.as_path()).env("GIT_DIR", repo.git_dir.as_os_str());
assert_eq!(LockLedger::before_start(&named).roots, vec![repo.git_dir.clone()]);
remove(repo.root.as_path());
}
#[test]
fn a_live_foreign_writers_empty_lock_stays() {
let repo: Repo = repo("stopped-locks-foreign-writer");
let ledger: LockLedger = ledger_of(&repo);
let member: TreeMember = ended_member();
std::fs::write(lock_of(&repo, "main"), "").expect("the foreign writer's empty lock");
let removed: Vec<PathBuf> = ledger.sweep(std::slice::from_ref(&member));
assert_eq!(removed, Vec::<PathBuf>::new(), "a lock nothing ties to the stopped child was removed");
assert!(lock_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn a_lock_made_after_a_pid_file_that_names_an_ended_member_stays() {
let repo: Repo = repo("stopped-locks-older-pid-file");
let ledger: LockLedger = ledger_of(&repo);
let member: TreeMember = ended_member();
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
std::thread::sleep(Duration::from_millis(50));
std::fs::write(lock_of(&repo, "main"), "").expect("the foreign writer's lock, made after the pid file");
let removed: Vec<PathBuf> = ledger.sweep(std::slice::from_ref(&member));
assert_eq!(removed, Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists() && pid_file_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn locks_that_are_not_reference_locks_stay() {
let repo: Repo = repo("stopped-locks-not-references");
let ledger: LockLedger = ledger_of(&repo);
let others: Vec<PathBuf> = vec![
repo.git_dir.join("index.lock"),
repo.git_dir.join("shallow.lock"),
repo.git_dir.join("objects").join("pack").join("pack.lock"),
];
for path in &others {
std::fs::create_dir_all(path.parent().expect("parent")).expect("directories");
std::fs::write(path, "").expect("empty lock");
}
assert_eq!(ledger.sweep(&[]), Vec::<PathBuf>::new());
for path in &others {
assert!(path.exists(), "{path:?}");
}
remove(repo.root.as_path());
}
fn container_judge() -> crate::stale_ref_lock_judge::Judge {
return crate::stale_ref_lock_judge::Judge { pid_view_complete: false, ..crate::stale_ref_lock_killed_git::judge() };
}
#[test]
fn a_lock_named_by_an_ended_child_of_the_wrapper_goes_in_a_container_view() {
let repo: Repo = repo("stopped-locks-container-view");
let mut command: Command = Command::new("git");
command.current_dir(repo.root.as_path());
let ledger: LockLedger = LockLedger::before_start(&command).with_judge(container_judge());
let member: TreeMember = ended_member();
std::fs::write(lock_of(&repo, "main"), "0123\n").expect("the lock the child held");
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), vec![lock_of(&repo, "main")]);
assert!(!lock_of(&repo, "main").exists() && !pid_file_of(&repo, "main").exists());
remove(repo.root.as_path());
}
fn container_ledger_of(repo: &Repo) -> LockLedger {
let mut command: Command = Command::new("git");
command.current_dir(repo.root.as_path());
return LockLedger::before_start(&command).with_judge(container_judge());
}
#[test]
fn in_a_container_view_a_pid_file_older_than_the_lock_vouches_for_nothing() {
let repo: Repo = repo("stopped-locks-container-older-pid");
let ledger: LockLedger = container_ledger_of(&repo);
let member: TreeMember = ended_member();
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
std::thread::sleep(Duration::from_millis(50));
std::fs::write(lock_of(&repo, "main"), "").expect("the foreign writer's lock, made after the pid file");
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn in_a_container_view_a_lock_older_than_the_child_stays() {
let repo: Repo = repo("stopped-locks-container-older-lock");
let member: TreeMember = ended_member();
std::fs::write(lock_of(&repo, "main"), "0123\n").expect("the lock");
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
std::thread::sleep(crate::stale_ref_lock_judge::COARSE_CLOCK_SLACK * 4);
let ledger: LockLedger = container_ledger_of(&repo);
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn in_a_container_view_a_number_outside_the_tree_proves_nothing() {
let repo: Repo = repo("stopped-locks-container-outside");
let ledger: LockLedger = container_ledger_of(&repo);
let member: TreeMember = ended_member();
let outsider: TreeMember = ended_member();
std::fs::write(lock_of(&repo, "main"), "0123\n").expect("the lock");
write_pid_file(pid_file_of(&repo, "main").as_path(), outsider.pid);
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn in_a_container_view_a_running_member_keeps_its_lock() {
let repo: Repo = repo("stopped-locks-container-running");
let ledger: LockLedger = container_ledger_of(&repo);
let (member, mut child): (TreeMember, Child) = running_member();
std::fs::write(lock_of(&repo, "main"), "0123\n").expect("the lock");
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
child.kill().expect("end sleep");
child.wait().expect("reap sleep");
remove(repo.root.as_path());
}
#[test]
fn in_a_container_view_a_lock_without_a_pid_file_stays() {
let repo: Repo = repo("stopped-locks-container-no-pid");
let ledger: LockLedger = container_ledger_of(&repo);
let member: TreeMember = ended_member();
std::fs::write(lock_of(&repo, "main"), "").expect("the lock");
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), Vec::<PathBuf>::new());
assert!(lock_of(&repo, "main").exists());
remove(repo.root.as_path());
}
#[test]
fn a_young_lock_of_an_ended_child_does_not_wait_out_the_margin() {
let repo: Repo = repo("stopped-locks-young");
let mut command: Command = Command::new("git");
command.current_dir(repo.root.as_path());
let patient = crate::stale_ref_lock_judge::Judge { margin: Duration::from_secs(3600), ..container_judge() };
let ledger: LockLedger = LockLedger::before_start(&command).with_judge(patient);
let member: TreeMember = ended_member();
let outsider: TreeMember = ended_member();
std::fs::write(lock_of(&repo, "main"), "0123\n").expect("the child's lock");
write_pid_file(pid_file_of(&repo, "main").as_path(), member.pid);
std::fs::write(lock_of(&repo, "other"), "0123\n").expect("another writer's lock");
write_pid_file(pid_file_of(&repo, "other").as_path(), outsider.pid);
assert_eq!(ledger.sweep(std::slice::from_ref(&member)), vec![lock_of(&repo, "main")]);
assert!(lock_of(&repo, "other").exists());
remove(repo.root.as_path());
}