use super::*;
use std::time::Duration;
use tempfile::tempdir;
const REAP_TRIES: u32 = 200;
const REAP_STEP: Duration = Duration::from_millis(25);
fn sink_in(dir: &Path) -> PathBuf {
dir.join("sink").join("child.err")
}
fn make_fifo(path: &Path) {
let status = crate::git_env::status(
crate::git_env::command(Path::new("mkfifo"))
.args(["-m", "600"])
.arg(path),
)
.expect("spawn mkfifo");
assert!(status.success(), "mkfifo");
}
#[cfg(target_os = "linux")]
const GROUP_DRIVER: &str = "#!/bin/sh\nread _ _ _ _ pgid _ < /proc/self/stat\nprintf '%s %s\\n' \"$$\" \"$pgid\" > \"$1\"\n";
#[cfg(not(target_os = "linux"))]
const GROUP_DRIVER: &str =
"#!/bin/sh\nprintf '%s %s\\n' \"$$\" \"$(ps -o pgid= -p $$)\" > \"$1\"\n";
#[test]
fn detached_child_survives_and_leads_own_group() {
let dir = tempdir().unwrap();
let fifo = dir.path().join("report");
make_fifo(&fifo);
let bin = write_script(dir.path(), "driver", GROUP_DRIVER);
let fifo_str = fifo.to_str().unwrap();
let pid = Cli::new(bin)
.spawn_detached(None, &sink_in(dir.path()), &[fifo_str])
.unwrap();
let recorded = std::fs::read_to_string(&fifo).unwrap();
let mut fields = recorded.split_whitespace();
let child_pid: u32 = fields.next().unwrap().parse().unwrap();
let child_leader: u32 = fields.next().unwrap().parse().unwrap();
assert_eq!(
child_pid, pid,
"returned pid is the detached child's own pid"
);
assert_eq!(
child_leader, pid,
"process_group(0): detached child leads its own group"
);
}
const EXITING_DRIVER: &str = "#!/bin/sh\nprintf done > \"$1\"\n";
#[test]
fn the_detached_child_is_reaped_and_leaves_no_zombie() {
let dir = tempdir().unwrap();
let fifo = dir.path().join("report");
make_fifo(&fifo);
let bin = write_script(dir.path(), "driver", EXITING_DRIVER);
let fifo_str = fifo.to_str().unwrap();
let pid = Cli::new(bin)
.spawn_detached(None, &sink_in(dir.path()), &[fifo_str])
.unwrap();
assert_eq!(std::fs::read_to_string(&fifo).unwrap(), "done");
let reaped = (0..REAP_TRIES).any(|_| {
std::thread::sleep(REAP_STEP);
!process_exists(pid)
});
assert!(
reaped,
"detached child {pid} never left the process table: yog is still its \
parent and nothing waited on it — one zombie per fire"
);
}
#[test]
fn spawn_detached_propagates_cwd() {
let dir = tempdir().unwrap();
let fifo = dir.path().join("report");
make_fifo(&fifo);
let bin = write_script(dir.path(), "driver", "#!/bin/sh\npwd -P > \"$1\"\n");
let fifo_str = fifo.to_str().unwrap();
Cli::new(bin)
.spawn_detached(Some(dir.path()), &sink_in(dir.path()), &[fifo_str])
.unwrap();
let recorded = std::fs::read_to_string(&fifo).unwrap();
let cwd = recorded.trim();
assert_eq!(
std::fs::canonicalize(cwd).unwrap(),
std::fs::canonicalize(dir.path()).unwrap(),
);
}
#[test]
fn spawn_detached_errors_on_missing_binary() {
let dir = tempdir().unwrap();
let cli = Cli::new("/definitely/not/a/real/binary/litany-detach");
let err = cli
.spawn_detached(None, &sink_in(dir.path()), &[])
.unwrap_err();
assert!(err.to_string().contains("failed to spawn"), "{err}");
}
const DYING_DRIVER: &str =
"#!/bin/sh\nprintf 'version skew: refusing\\n' >&2\nprintf done > \"$1\"\n";
#[test]
fn detached_child_stderr_lands_in_the_sink_file() {
let dir = tempdir().unwrap();
let fifo = dir.path().join("report");
make_fifo(&fifo);
let bin = write_script(dir.path(), "driver", DYING_DRIVER);
let sink = sink_in(dir.path());
let fifo_str = fifo.to_str().unwrap();
assert!(!sink.parent().unwrap().exists());
Cli::new(bin)
.spawn_detached(None, &sink, &[fifo_str])
.unwrap();
assert_eq!(std::fs::read_to_string(&fifo).unwrap(), "done");
assert_eq!(
std::fs::read_to_string(&sink).unwrap(),
"version skew: refusing\n",
"the detached child's stderr is captured, not dropped (§8.1)"
);
}
#[test]
fn an_unopenable_sink_degrades_to_null_and_still_launches() {
let dir = tempdir().unwrap();
let fifo = dir.path().join("report");
make_fifo(&fifo);
let bin = write_script(dir.path(), "driver", DYING_DRIVER);
let blocker = dir.path().join("blocker");
fs::write(&blocker, "not a directory").unwrap();
let fifo_str = fifo.to_str().unwrap();
Cli::new(bin)
.spawn_detached(None, &blocker.join("child.err"), &[fifo_str])
.unwrap();
assert_eq!(std::fs::read_to_string(&fifo).unwrap(), "done");
}