use std::ffi::CString;
use std::os::unix::ffi::OsStrExt as _;
use std::os::unix::fs::OpenOptionsExt as _;
use std::path::{Path, PathBuf};
use std::sync::mpsc;
use std::time::Duration;
use super::decommission_force::is_provisioning_entry;
use super::provisioning_ledger::{ProvisioningLedger, snapshot};
const BOUND: Duration = Duration::from_secs(10);
fn mkfifo(path: &Path) {
let c = CString::new(path.as_os_str().as_bytes()).expect("path has no NUL");
let rc = unsafe { libc::mkfifo(c.as_ptr(), 0o600) };
assert_eq!(rc, 0, "mkfifo {}", path.display());
}
fn answers_in_time<T: Send + 'static>(
fifos: &[PathBuf],
f: impl FnOnce() -> T + Send + 'static,
) -> Option<T> {
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(f());
});
let answer = rx.recv_timeout(BOUND).ok();
if answer.is_none() {
for _ in 0..16 {
for fifo in fifos {
let _ = std::fs::OpenOptions::new()
.write(true)
.custom_flags(libc::O_NONBLOCK)
.open(fifo);
}
}
}
answer
}
#[test]
fn a_fifo_claude_md_or_gitignore_does_not_block_snapshot() {
let dir = tempfile::tempdir().expect("tempdir");
let ws = dir.path().to_path_buf();
let fifos = vec![ws.join("CLAUDE.md"), ws.join(".gitignore")];
for fifo in &fifos {
mkfifo(fifo);
}
let answered = answers_in_time(&fifos, move || {
snapshot(&ws);
});
assert!(answered.is_some(), "snapshot blocked on a FIFO");
}
#[test]
fn a_fifo_untracked_gitignore_is_not_excused_and_does_not_block() {
let dir = tempfile::tempdir().expect("tempdir");
let ws = dir.path().to_path_buf();
let fifo = ws.join(".gitignore");
mkfifo(&fifo);
let ledger = ProvisioningLedger {
version: 1,
gitignore_appended: vec![".trusty-mpm/sessions/".to_string()],
..ProvisioningLedger::default()
};
let fifos = vec![fifo];
let ws_ledger = ws.clone();
let by_ledger = answers_in_time(&fifos, move || ledger.excuses(&ws_ledger, "?? .gitignore"));
assert_eq!(by_ledger, Some(false), "ledger check: hung or excused");
let by_force = answers_in_time(&fifos, move || {
is_provisioning_entry(&ws, None, "?? .gitignore")
});
assert_eq!(by_force, Some(false), "--force check: hung or excused");
}
#[test]
fn a_symlinked_untracked_gitignore_is_not_excused() {
let line = ".trusty-mpm/sessions/";
let ledger = ProvisioningLedger {
version: 1,
gitignore_appended: vec![line.to_string()],
..ProvisioningLedger::default()
};
let excused = |ws: &Path| {
(
ledger.excuses(ws, "?? .gitignore"),
is_provisioning_entry(ws, None, "?? .gitignore"),
)
};
let elsewhere = tempfile::tempdir().expect("tempdir");
let target = elsewhere.path().join("gitignore");
std::fs::write(&target, format!("{line}\n")).expect("write target");
let control = tempfile::tempdir().expect("tempdir");
std::fs::copy(&target, control.path().join(".gitignore")).expect("copy");
assert_eq!(
excused(control.path()),
(true, true),
"regular-file control"
);
let ws = tempfile::tempdir().expect("tempdir");
std::os::unix::fs::symlink(&target, ws.path().join(".gitignore")).expect("symlink");
assert_eq!(excused(ws.path()), (false, false), "a symlink was excused");
}