use super::*;
use crate::process_use::ProcessUser;
use crate::test_support::{fixture, remove};
use crate::transaction_owner::{current_transaction_owner, encode_transaction_owner};
use std::time::Duration;
pub(crate) const ID: &str = "0b6c2c1e-6f5b-4d0e-9a55-3f5d8e2f6a10";
pub(crate) const DEAD: i64 = 4242;
fn missing(_pid: i64) -> ProcessStart {
return ProcessStart::Missing;
}
fn running(_pid: i64) -> ProcessStart {
return ProcessStart::Running { started_at_ms: 1.0, resolution_ms: 1 };
}
fn nobody(_target: &UseTarget<'_>) -> UseScan {
return UseScan::default();
}
fn a_holder(_target: &UseTarget<'_>) -> UseScan {
return UseScan { users: vec![ProcessUser { pid: 77, command: Some(String::from("git")) }], ..UseScan::default() };
}
fn a_blind_scan(_target: &UseTarget<'_>) -> UseScan {
return UseScan { unreadable: vec![String::from("PID 9 fd 3: Permission denied")], ..UseScan::default() };
}
fn no_process_table(_target: &UseTarget<'_>) -> UseScan {
return UseScan { unavailable: Some(String::from("/proc: No such file or directory")), ..UseScan::default() };
}
fn date(path: &Path, seconds: u64) {
std::fs::File::open(path)
.expect("open")
.set_modified(SystemTime::UNIX_EPOCH + Duration::from_secs(seconds))
.expect("date");
}
pub(crate) struct Scene {
pub(crate) root: PathBuf,
pub(crate) directory: PathBuf,
pub(crate) index: PathBuf,
}
pub(crate) fn scene(name: &str) -> Scene {
let root: PathBuf = fixture(name);
let directory: PathBuf = root.join("cli-git-transactions").join(ID);
std::fs::create_dir_all(&directory).expect("transaction");
let mut owner = current_transaction_owner(ID, "t").expect("owner");
owner.owner_pid = DEAD;
std::fs::write(directory.join(OWNER_FILENAME), encode_transaction_owner(&owner)).expect("owner record");
let index: PathBuf = root.join("index");
std::fs::write(real_lock_path(index.as_path()), b"").expect("lock");
std::fs::write(lock_pid_path(index.as_path()), format!("pid {DEAD}\n")).expect("PID file");
date(directory.join(OWNER_FILENAME).as_path(), 100);
date(real_lock_path(index.as_path()).as_path(), 200);
date(lock_pid_path(index.as_path()).as_path(), 300);
return Scene { root, directory, index };
}
fn release(
scene: &Scene,
pid: i64,
probe: fn(i64) -> ProcessStart,
scan: fn(&UseTarget<'_>) -> UseScan,
) -> UnrecordedLockRelease {
return release_announced_lock_with(scene.directory.as_path(), scene.index.as_path(), pid, probe, scan).expect("judged");
}
#[test]
fn a_lock_announced_by_a_dead_owner_with_every_proof_is_released() {
let scene: Scene = scene("unrecorded-released");
assert_eq!(release(&scene, DEAD, missing, nobody), UnrecordedLockRelease::Released);
assert!(!real_lock_path(scene.index.as_path()).exists());
remove(scene.root.as_path());
}
#[test]
fn an_unreadable_process_does_not_keep_the_lock() {
let scene: Scene = scene("unrecorded-blind");
assert_eq!(release(&scene, DEAD, missing, a_blind_scan), UnrecordedLockRelease::Released);
remove(scene.root.as_path());
}
#[test]
fn an_absent_lock_is_absent() {
let scene: Scene = scene("unrecorded-absent");
std::fs::remove_file(real_lock_path(scene.index.as_path())).expect("remove");
assert_eq!(release(&scene, DEAD, missing, nobody), UnrecordedLockRelease::Absent);
remove(scene.root.as_path());
}
#[test]
fn a_missing_proof_keeps_the_lock_and_names_the_proof() {
let lock = |scene: &Scene| { return real_lock_path(scene.index.as_path()); };
let cases: [(&str, &str, fn(&Scene), fn(i64) -> ProcessStart, fn(&UseTarget<'_>) -> UseScan); 8] = [
("a reused number", "may have been reused", |_| {}, running, nobody),
("a holder", "process 77 holds it open", |_| {}, missing, a_holder),
("no process table", "process table could not be read", |_| {}, missing, no_process_table),
("a PID file of another owner", "no PID file names the dead owner", |scene| {
std::fs::write(lock_pid_path(scene.index.as_path()), "pid 99\n").expect("other owner");
date(lock_pid_path(scene.index.as_path()).as_path(), 300);
}, missing, nobody),
("no PID file", "no PID file names the dead owner", |scene| {
std::fs::remove_file(lock_pid_path(scene.index.as_path())).expect("remove");
}, missing, nobody),
("a lock newer than the PID file", "another process took it", |scene| {
date(real_lock_path(scene.index.as_path()).as_path(), 400);
}, missing, nobody),
("a lock older than the transaction", "before the transaction was published", |scene| {
date(real_lock_path(scene.index.as_path()).as_path(), 50);
}, missing, nobody),
("a lock with bytes", "something wrote to it", |scene| {
std::fs::write(real_lock_path(scene.index.as_path()), b"x").expect("bytes");
date(real_lock_path(scene.index.as_path()).as_path(), 200);
}, missing, nobody),
];
for (name, expected, damage, probe, scan) in cases {
let scene: Scene = scene(format!("unrecorded-kept-{}", name.replace(' ', "-")).as_str());
damage(&scene);
let outcome: UnrecordedLockRelease = release(&scene, DEAD, probe, scan);
assert!(
matches!(&outcome, UnrecordedLockRelease::Kept(reason) if reason.contains(expected)),
"{name}: {outcome:?}"
);
assert!(lock(&scene).exists(), "{name}: the lock stays");
remove(scene.root.as_path());
}
}
#[test]
fn an_owner_without_a_trustworthy_number_keeps_the_lock() {
let scene: Scene = scene("unrecorded-foreign");
let mut owner = current_transaction_owner(ID, "t").expect("owner");
owner.owner_pid = DEAD;
owner.owner_pid_namespace = Some(String::from("pid:[1]"));
std::fs::write(scene.directory.join(OWNER_FILENAME), encode_transaction_owner(&owner)).expect("foreign owner");
date(scene.directory.join(OWNER_FILENAME).as_path(), 100);
let outcome: UnrecordedLockRelease = release(&scene, DEAD, missing, nobody);
assert!(matches!(&outcome, UnrecordedLockRelease::Kept(reason) if reason.contains("another PID namespace")), "{outcome:?}");
std::fs::remove_file(scene.directory.join(OWNER_FILENAME)).expect("lose the owner record");
let lost: UnrecordedLockRelease = release(&scene, 0, missing, nobody);
assert!(matches!(&lost, UnrecordedLockRelease::Kept(reason) if reason.contains("no PID file names")), "{lost:?}");
assert!(real_lock_path(scene.index.as_path()).exists());
remove(scene.root.as_path());
}