use super::*;
use std::cell::Cell;
use std::path::PathBuf;
use tempfile::TempDir;
const INBOX: &str = "/ws/inbox/20260427T140000Z-aaaa";
const RESPONSE: &str = "/ws/steps/20260427T140000Z-aaaa/003/response.json";
struct FakeLsof {
output: Option<Vec<u8>>,
calls: Cell<usize>,
}
fn probe(out: Option<Vec<u8>>) -> LsofProbe<FakeLsof> {
LsofProbe::new(FakeLsof {
output: out,
calls: Cell::new(0),
})
}
impl LsofRunner for FakeLsof {
fn run(&self, _target: &Path) -> Option<Vec<u8>> {
self.calls.set(self.calls.get() + 1);
self.output.clone()
}
}
fn target(s: &str) -> PathBuf {
PathBuf::from(s)
}
#[test]
fn writer_open_for_write_is_holder_and_writer() {
let out = format!("p4321\nf3\ntREG\nau\nn{RESPONSE}\n");
let seen = parse(out.as_bytes(), &target(RESPONSE)).unwrap();
assert!(seen.any_holder && seen.any_writer);
}
#[test]
fn reader_only_is_a_holder_but_not_a_writer() {
let out = format!("p4321\nf3\nar\nn{INBOX}\n");
let seen = parse(out.as_bytes(), &target(INBOX)).unwrap();
assert!(seen.any_holder && !seen.any_writer);
}
#[test]
fn write_access_w_counts_as_a_writer() {
let out = format!("p9\naw\nn{RESPONSE}\n");
let seen = parse(out.as_bytes(), &target(RESPONSE)).unwrap();
assert!(seen.any_writer);
}
#[test]
fn a_non_matching_name_is_no_holder() {
let out = "p4321\nf3\naw\nn/some/other/file\n\nf4\nn/and/another\n";
let seen = parse(out.as_bytes(), &target(RESPONSE)).unwrap();
assert!(!seen.any_holder && !seen.any_writer);
}
#[test]
fn access_does_not_leak_across_files_without_an_f_delimiter() {
let out = format!("p7\naw\nn/other\nn{RESPONSE}\n");
let seen = parse(out.as_bytes(), &target(RESPONSE)).unwrap();
assert!(seen.any_holder && !seen.any_writer);
}
#[test]
fn empty_output_is_a_definite_no_holder() {
let seen = parse(b"", &target(RESPONSE)).unwrap();
assert!(!seen.any_holder && !seen.any_writer);
}
#[test]
fn non_field_output_is_unparseable() {
assert!(parse(b"lsof: WARNING: bad argument\n", &target(RESPONSE)).is_none());
}
#[test]
fn invalid_utf8_is_unparseable() {
assert!(parse(&[0xff, 0xfe, 0x00], &target(RESPONSE)).is_none());
}
struct Targets {
_dir: TempDir,
inbox: PathBuf,
response: PathBuf,
}
fn targets() -> Targets {
let dir = tempfile::tempdir().unwrap();
let inbox = dir.path().join("inbox");
std::fs::create_dir(&inbox).unwrap();
let response = dir.path().join("response.json");
std::fs::write(&response, b"{}").unwrap();
Targets {
inbox: inbox.canonicalize().unwrap(),
response: response.canonicalize().unwrap(),
_dir: dir,
}
}
#[test]
fn lock_state_reports_held_free_and_unknown() {
let t = targets();
let name = t.inbox.display();
let held = probe(Some(format!("p1\nf3\nar\nn{name}\n").into_bytes()));
assert_eq!(held.lock_state(&t.inbox), Probe::Held);
let free = probe(Some(Vec::new()));
assert_eq!(free.lock_state(&t.inbox), Probe::Free);
let absent = probe(None);
assert_eq!(absent.lock_state(&t.inbox), Probe::Unknown);
}
#[test]
fn writer_state_reports_held_free_and_unknown() {
let t = targets();
let name = t.response.display();
let held = probe(Some(format!("p1\nf3\naw\nn{name}\n").into_bytes()));
assert_eq!(held.writer_state(&t.response), Probe::Held);
let reader = probe(Some(format!("p1\nf3\nar\nn{name}\n").into_bytes()));
assert_eq!(reader.writer_state(&t.response), Probe::Free);
let garbled = probe(Some(b"garbage\n".to_vec()));
assert_eq!(garbled.writer_state(&t.response), Probe::Unknown);
assert_eq!(garbled.runner.calls.get(), 1);
}
#[test]
fn an_absent_target_is_a_definite_free_and_asks_lsof_nothing() {
let t = targets();
let gone = t.inbox.join("no-such-agent");
let unknown = probe(None);
assert_eq!(unknown.lock_state(&gone), Probe::Free);
assert_eq!(
unknown.writer_state(&gone.join("response.json")),
Probe::Free
);
assert_eq!(unknown.runner.calls.get(), 0, "nothing was spawned");
}
#[test]
fn a_target_reached_through_a_symlink_matches_its_resolved_name() {
let t = targets();
let link = t.inbox.parent().unwrap().join("link");
std::os::unix::fs::symlink(&t.inbox, &link).unwrap();
let name = t.inbox.display();
let held = probe(Some(format!("p1\nf3\nar\nn{name}\n").into_bytes()));
assert_eq!(held.lock_state(&link), Probe::Held);
}