#![cfg(unix)]
use super::{
ResetsShuttingDown, capture_with_timeout, kill_in_flight_git_ops, kill_process_group,
register_killable_pid,
};
#[test]
fn process_group_kill_takes_grandchildren() {
use std::io::BufRead;
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
let mut child = Command::new("sh")
.arg("-c")
.arg("sleep 60 2>/dev/null & echo $!; wait")
.process_group(0)
.stdout(Stdio::piped())
.spawn()
.expect("spawn sh");
let mut line = String::new();
std::io::BufReader::new(child.stdout.take().expect("sh stdout"))
.read_line(&mut line)
.expect("read grandchild pid");
let grandchild: i32 = line.trim().parse().expect("grandchild pid");
assert_eq!(
unsafe { libc::kill(grandchild, 0) },
0,
"grandchild should be alive before the kill"
);
kill_process_group(&mut child);
let _ = child.wait();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
let rc = unsafe { libc::kill(grandchild, 0) };
if rc == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
break;
}
assert!(
std::time::Instant::now() < deadline,
"grandchild still alive or unreaped 5s after the process-group kill"
);
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[test]
fn kill_in_flight_git_ops_takes_registered_groups_down() {
use std::io::BufRead;
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
let _lock = super::TEST_KILL_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let _reset = ResetsShuttingDown::new();
let mut child = Command::new("sh")
.arg("-c")
.arg("sleep 60 2>/dev/null & echo $!; wait")
.process_group(0)
.stdout(Stdio::piped())
.spawn()
.expect("spawn sh");
let mut line = String::new();
std::io::BufReader::new(child.stdout.take().expect("sh stdout"))
.read_line(&mut line)
.expect("read grandchild pid");
let grandchild: i32 = line.trim().parse().expect("grandchild pid");
assert_eq!(
unsafe { libc::kill(grandchild, 0) },
0,
"grandchild should be alive before the exit-kill"
);
register_killable_pid(child.id() as i32);
kill_in_flight_git_ops();
let _ = child.wait();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
loop {
let rc = unsafe { libc::kill(grandchild, 0) };
if rc == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
break;
}
assert!(
std::time::Instant::now() < deadline,
"grandchild still alive 5s after the exit-kill"
);
std::thread::sleep(std::time::Duration::from_millis(50));
}
}
#[test]
fn capture_with_timeout_kills_the_group() {
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use std::time::Duration;
let pidfile = tempfile::NamedTempFile::new().expect("temp pid file");
let pidfile_path = pidfile.path().to_str().unwrap().to_string();
let script = format!(
"echo $$ > '{pidfile_path}'; sleep 60 2>/dev/null & echo $! >> '{pidfile_path}'; wait",
);
let mut child = Command::new("sh")
.arg("-c")
.arg(&script)
.process_group(0)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn sh");
let res = capture_with_timeout(&mut child, Duration::from_millis(1000));
assert!(
res.is_err(),
"a sleeping command should time out, got {:?}",
res.as_ref().map(|o| o.status.code())
);
let contents = std::fs::read_to_string(&pidfile_path).expect("read pid file");
let mut lines = contents.lines();
let leader: i32 = lines
.next()
.expect("leader pid")
.parse()
.expect("leader int");
let grandchild: i32 = lines
.next()
.expect("grandchild pid")
.parse()
.expect("grandchild int");
let deadline = std::time::Instant::now() + Duration::from_secs(5);
for pid in [leader, grandchild] {
loop {
let rc = unsafe { libc::kill(pid, 0) };
if rc == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
break;
}
assert!(
std::time::Instant::now() < deadline,
"pid {pid} still alive 5s after the timeout kill"
);
std::thread::sleep(Duration::from_millis(50));
}
}
}
#[test]
fn capture_with_timeout_success_does_not_kill_descendant() {
use std::os::unix::process::CommandExt;
use std::process::{Command, Stdio};
use std::time::Duration;
let mut child = Command::new("sh")
.arg("-c")
.arg("echo hi; sleep 60 2>/dev/null & echo $!; exit 0")
.process_group(0)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn sh");
let res = capture_with_timeout(&mut child, Duration::from_secs(5));
assert!(
res.is_ok(),
"expected Ok from a prompt exit, got {:?}",
res.as_ref().err().map(|e| e.kind())
);
let out = res.unwrap();
assert_eq!(out.status.code(), Some(0));
let stdout = String::from_utf8_lossy(&out.stdout);
let mut lines = stdout.lines();
assert_eq!(
lines.next(),
Some("hi"),
"stdout should start with 'hi', got {stdout:?}"
);
let sleep_pid: i32 = lines
.next()
.expect("sleep pid")
.parse()
.expect("sleep pid int");
assert_eq!(
unsafe { libc::kill(sleep_pid, 0) },
0,
"sleep grandchild should still be alive after the success path",
);
unsafe { libc::kill(sleep_pid, libc::SIGKILL) };
}
#[test]
fn run_git_op_capturing_registers_and_unregisters() {
use std::time::Duration;
if !crate::git::git_test_available() {
return;
}
let _lock = super::TEST_KILL_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
let _reset = ResetsShuttingDown::new();
let tmp = tempfile::TempDir::new().unwrap();
let path = tmp.path().to_path_buf();
let init = super::git_command(&path)
.arg("init")
.status()
.expect("run git init");
assert!(init.success(), "git init failed: {init}");
let pidfile = tempfile::NamedTempFile::new().expect("temp pid file");
let pidfile_path = pidfile.path().to_str().unwrap().to_string();
let args = vec![
"-c".to_string(),
format!("alias.slow=!sleep 30 2>/dev/null & echo $! > '{pidfile_path}'; wait"),
"slow".to_string(),
];
let worker = std::thread::spawn(move || super::run_git_op_capturing(&path, &args));
let deadline = std::time::Instant::now() + Duration::from_secs(5);
let sleep_pid: i32 = loop {
if let Some(pid) = std::fs::read_to_string(&pidfile_path)
.ok()
.and_then(|s| s.trim().parse().ok())
{
break pid;
}
assert!(
std::time::Instant::now() < deadline,
"git alias never reported its sleep grandchild"
);
std::thread::sleep(Duration::from_millis(20));
};
assert!(
!super::killable_pids().lock().unwrap().is_empty(),
"run_git_op_capturing did not register its child"
);
let killed_at = std::time::Instant::now();
kill_in_flight_git_ops();
let out = worker
.join()
.expect("worker thread")
.expect("a killed op still yields its captured Output");
assert!(
killed_at.elapsed() < Duration::from_secs(10),
"wrapper did not return promptly after the shutdown kill"
);
assert!(
!out.status.success(),
"a SIGKILLed git op must not report success"
);
let deadline = std::time::Instant::now() + Duration::from_secs(5);
loop {
let rc = unsafe { libc::kill(sleep_pid, 0) };
if rc == -1 && std::io::Error::last_os_error().raw_os_error() == Some(libc::ESRCH) {
break;
}
assert!(
std::time::Instant::now() < deadline,
"sleep grandchild {sleep_pid} still alive after the shutdown kill"
);
std::thread::sleep(Duration::from_millis(50));
}
assert!(
super::killable_pids().lock().unwrap().is_empty(),
"killable registry not empty after run_git_op_capturing"
);
}
#[test]
fn failed_git_queries_report_stderr_instead_of_empty_content() {
if !crate::git::git_test_available() {
return;
}
let tmp = tempfile::TempDir::new().unwrap();
git2::Repository::init(tmp.path()).unwrap();
let output = super::git_command(tmp.path())
.args(["log", "missing-reference"])
.output();
let error = super::output_text(output).unwrap_err().to_string();
assert!(error.contains("missing-reference"));
assert!(error.contains("failed"));
let output = super::git_command(tmp.path())
.args(["status", "--short"])
.output();
assert!(super::output_text(output).unwrap().is_empty());
}