use super::{descendants_in_table, kill_with_grace, parse_table, unchanged_pids};
use crate::test_subprocess::{self, is_live};
use std::os::unix::process::{CommandExt, ExitStatusExt};
use std::process::Command;
use std::time::Duration;
#[test]
fn kill_with_grace_escalates_for_term_ignoring_child() {
let _permit = test_subprocess::acquire();
let ready = tempfile::NamedTempFile::new().expect("ready file should be created");
let ready_path = ready.path().to_path_buf();
drop(ready);
let mut command = Command::new("/bin/sh");
command
.arg("-c")
.arg("trap '' TERM; touch \"$1\"; while :; do sleep 1; done")
.arg("sh")
.arg(&ready_path)
.process_group(0);
let mut child = command.spawn().expect("TERM-ignoring child should spawn");
let pgid = child.id() as i32;
for _ in 0..100 {
if ready_path.exists() {
break;
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(ready_path.exists());
kill_with_grace(pgid, Duration::from_millis(100));
let status = child.wait().expect("TERM-ignoring child should exit");
assert_eq!(status.signal(), Some(libc::SIGKILL));
}
#[test]
fn descendants_follow_ppid_chain_and_skip_unrelated_and_init() {
let table = parse_table(
&[
" 10 1 Sat Oct 3 00:00:01 2026",
" 11 10 Sat Oct 3 00:00:02 2026",
" 12 11 Sat Oct 3 00:00:03 2026",
" 13 12 Sat Oct 3 00:00:04 2026",
" 20 1 Sat Oct 3 00:00:05 2026",
" 21 20 Sat Oct 3 00:00:06 2026",
" bad row",
" 30 10",
]
.join("\n"),
);
let mut found = descendants_in_table(10, &table)
.iter()
.map(|e| e.pid)
.collect::<Vec<_>>();
found.sort_unstable();
assert_eq!(found, vec![11, 12, 13]);
let init = parse_table(" 1 0 Sat Oct 3 00:00:00 2026\n 2 1 Sat Oct 3 00:00:00 2026");
assert!(
descendants_in_table(1, &init)
.iter()
.all(|entry| entry.pid != 1)
);
}
#[test]
fn sigkill_pass_skips_descendants_whose_pid_was_reused_or_is_unverifiable() {
let snapshot = parse_table(" 11 10 Sat Oct 3 00:00:02 2026\n 12 11 Sat Oct 3 00:00:03 2026");
let now = parse_table(" 11 1 Sat Oct 3 00:00:02 2026\n 12 1 Sat Oct 3 00:09:59 2026");
assert_eq!(unchanged_pids(&snapshot, &now), vec![11]);
assert!(unchanged_pids(&snapshot, &[]).is_empty());
}
#[test]
fn kill_with_grace_reaches_descendant_in_its_own_group() {
assert_own_group_descendant_killed("perl -e 'setpgrp(0, 0); exec q(sleep), q(60)'");
}
#[test]
fn kill_with_grace_sigkills_term_ignoring_descendant_after_identity_check() {
assert_own_group_descendant_killed("perl -e '$SIG{TERM} = q(IGNORE); setpgrp(0, 0); sleep 60'");
}
fn assert_own_group_descendant_killed(descendant: &str) {
let _permit = test_subprocess::acquire();
let dir = tempfile::tempdir().expect("temp dir");
let pid_file = dir.path().join("grandchild.pid");
let mut command = Command::new("/bin/sh");
command
.arg("-c")
.arg(format!("{descendant} & echo $! > \"$1\"; wait"))
.arg("sh")
.arg(&pid_file)
.process_group(0);
let mut child = command.spawn().expect("agent stand-in should spawn");
let mut grandchild = 0;
for _ in 0..200 {
if let Some(pid) = std::fs::read_to_string(&pid_file)
.ok()
.and_then(|text| text.trim().parse::<i32>().ok())
.filter(|pid| unsafe { libc::getpgid(*pid) } == *pid)
{
grandchild = pid;
break;
}
std::thread::sleep(Duration::from_millis(10));
}
assert!(grandchild > 0, "grandchild must lead its own process group");
kill_with_grace(child.id() as i32, Duration::from_millis(200));
let _ = child.wait();
let gone = (0..100).any(|_| {
std::thread::sleep(Duration::from_millis(20));
!is_live(grandchild)
});
if !gone {
unsafe { libc::kill(grandchild, libc::SIGKILL) };
}
assert!(
gone,
"descendant in its own process group must be killed with the agent"
);
}