use std::process::{Command, Stdio};
use std::sync::Arc;
use std::sync::atomic::Ordering;
use std::time::Duration;
use crate::runner::execution::process::{test_ctrlc_state, wait_for_child};
fn slow_exit_command(exit_delay_ms: u64, exit_code: i32) -> Command {
let script = format!(
r#"trap 'sleep 0.{exit_delay_ms}; exit {exit_code}' INT; while true; do sleep 1; done"#
);
let mut cmd = Command::new("sh");
cmd.arg("-c")
.arg(script)
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd
}
#[test]
fn test_process_exits_cleanly_after_timeout_interrupt() {
let mut cmd = slow_exit_command(1, 0);
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
let _ = libc::setpgid(0, 0);
Ok(())
});
}
let mut child = cmd.spawn().expect("Failed to spawn test process");
let ctrlc = test_ctrlc_state();
#[cfg(unix)]
{
let mut guard = ctrlc.active_pgid.lock().unwrap();
*guard = Some(child.id() as i32);
}
let timeout = Some(Duration::from_millis(200));
let result = wait_for_child(&mut child, &ctrlc, timeout);
assert!(
result.is_ok(),
"Process should exit successfully after timeout interrupt"
);
let status = result.unwrap();
assert!(status.success(), "Process should have exit code 0");
}
#[test]
fn test_process_times_out_and_is_killed() {
let script = r#"trap '' INT; while true; do sleep 1; done"#;
let mut cmd = Command::new("sh");
cmd.arg("-c")
.arg(script)
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
let _ = libc::setpgid(0, 0);
Ok(())
});
}
let mut child = cmd.spawn().expect("Failed to spawn test process");
let ctrlc = test_ctrlc_state();
#[cfg(unix)]
{
let mut guard = ctrlc.active_pgid.lock().unwrap();
*guard = Some(child.id() as i32);
}
let timeout = Some(Duration::from_millis(50));
let start = std::time::Instant::now();
let result = wait_for_child(&mut child, &ctrlc, timeout);
let elapsed = start.elapsed();
assert!(
elapsed >= Duration::from_secs(2),
"Should wait at least 2 seconds for grace period before kill"
);
assert!(
result.is_err(),
"wait_for_child should return Timeout error when process is killed"
);
match result {
Err(crate::runner::RunnerError::Timeout) => {}
Err(other) => panic!("Expected Timeout error, got {:?}", other),
Ok(status) => panic!(
"Expected Timeout error, got Ok with exit code {:?}",
status.code()
),
}
}
#[test]
fn test_process_exits_nonzero_after_timeout() {
let mut cmd = slow_exit_command(1, 42);
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
let _ = libc::setpgid(0, 0);
Ok(())
});
}
let mut child = cmd.spawn().expect("Failed to spawn test process");
let ctrlc = test_ctrlc_state();
#[cfg(unix)]
{
let mut guard = ctrlc.active_pgid.lock().unwrap();
*guard = Some(child.id() as i32);
}
let timeout = Some(Duration::from_millis(50));
let result = wait_for_child(&mut child, &ctrlc, timeout);
assert!(
result.is_err(),
"Should return Timeout error for process that exits non-zero after interrupt"
);
match result {
Err(crate::runner::RunnerError::Timeout) => {}
Err(other) => panic!("Expected Timeout error, got {:?}", other),
Ok(status) => panic!(
"Expected Timeout error, got Ok with exit code {:?}",
status.code()
),
}
}
#[test]
fn test_ctrl_c_interrupt_handling() {
let mut cmd = slow_exit_command(1, 0);
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
let _ = libc::setpgid(0, 0);
Ok(())
});
}
let mut child = cmd.spawn().expect("Failed to spawn test process");
let ctrlc = test_ctrlc_state();
#[cfg(unix)]
{
let mut guard = ctrlc.active_pgid.lock().unwrap();
*guard = Some(child.id() as i32);
}
let ctrlc_clone = Arc::clone(&ctrlc);
let (ready_tx, ready_rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
ready_rx.recv().expect("signal wait_for_child start");
std::thread::park_timeout(Duration::from_millis(100));
ctrlc_clone.interrupted.store(true, Ordering::SeqCst);
});
ready_tx.send(()).expect("notify interrupt thread");
let result = wait_for_child(&mut child, &ctrlc, None);
assert!(
result.is_ok(),
"Process should exit successfully after Ctrl-C interrupt"
);
let status = result.unwrap();
assert!(status.success(), "Process should have exit code 0");
}
#[test]
fn test_no_timeout_no_interrupt_process_completes_normally() {
let mut cmd = Command::new("sh");
cmd.arg("-c").arg("exit 0");
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
let _ = libc::setpgid(0, 0);
Ok(())
});
}
let mut child = cmd.spawn().expect("Failed to spawn test process");
let ctrlc = test_ctrlc_state();
#[cfg(unix)]
{
let mut guard = ctrlc.active_pgid.lock().unwrap();
*guard = Some(child.id() as i32);
}
let result = wait_for_child(&mut child, &ctrlc, None);
assert!(result.is_ok());
assert!(result.unwrap().success());
}
#[test]
fn test_cleanup_clears_active_pgid_after_timeout() {
let script = r#"trap '' INT; while true; do sleep 1; done"#;
let mut cmd = Command::new("sh");
cmd.arg("-c")
.arg(script)
.stdout(Stdio::null())
.stderr(Stdio::null());
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
let _ = libc::setpgid(0, 0);
Ok(())
});
}
let mut child = cmd.spawn().expect("Failed to spawn test process");
let ctrlc = test_ctrlc_state();
#[cfg(unix)]
{
let mut guard = ctrlc.active_pgid.lock().unwrap();
*guard = Some(child.id() as i32);
}
let timeout = Some(Duration::from_millis(50));
let _ = wait_for_child(&mut child, &ctrlc, timeout);
#[cfg(unix)]
{
let pgid = ctrlc.active_pgid.lock().unwrap();
assert!(
pgid.is_some(),
"wait_for_child doesn't clear pgid - that's caller's responsibility"
);
}
}
#[test]
fn test_pre_run_interrupt_returns_immediately() {
let ctrlc = test_ctrlc_state();
ctrlc.interrupted.store(true, Ordering::SeqCst);
let should_abort = ctrlc.interrupted.load(Ordering::SeqCst);
assert!(should_abort, "Should detect pre-run interrupt");
assert!(
ctrlc.interrupted.load(Ordering::SeqCst),
"Interrupted flag should remain set after detecting pre-run interrupt"
);
let pgid = ctrlc.active_pgid.lock().unwrap();
assert!(
pgid.is_none(),
"active_pgid should remain None when aborting before spawn"
);
}
fn slow_exit_command_with_delay(exit_delay_ms: u64, exit_code: i32) -> Command {
let script = format!(
r#"trap 'sleep 0.{exit_delay_ms}; exit {exit_code}' INT; while true; do sleep 1; done"#
);
let mut cmd = Command::new("sh");
cmd.arg("-c")
.arg(script)
.stdout(Stdio::null())
.stderr(Stdio::null());
cmd
}
#[test]
fn test_ctrl_c_during_timeout_grace_period() {
let mut cmd = slow_exit_command_with_delay(5, 0);
#[cfg(unix)]
unsafe {
use std::os::unix::process::CommandExt;
cmd.pre_exec(|| {
let _ = libc::setpgid(0, 0);
Ok(())
});
}
let mut child = cmd.spawn().expect("Failed to spawn test process");
let ctrlc = test_ctrlc_state();
#[cfg(unix)]
{
let mut guard = ctrlc.active_pgid.lock().unwrap();
*guard = Some(child.id() as i32);
}
let ctrlc_clone = Arc::clone(&ctrlc);
std::thread::spawn(move || {
std::thread::park_timeout(Duration::from_millis(200));
ctrlc_clone.interrupted.store(true, Ordering::SeqCst);
});
let timeout = Some(Duration::from_millis(50));
let result = wait_for_child(&mut child, &ctrlc, timeout);
assert!(
result.is_ok(),
"Process should exit successfully (code 0) after timeout interrupt, even with Ctrl-C during grace"
);
let status = result.unwrap();
assert!(status.success(), "Process should have exit code 0");
}
#[test]
fn test_ctrlc_state_isolation() {
let ctrlc1 = test_ctrlc_state();
let ctrlc2 = test_ctrlc_state();
ctrlc1.interrupted.store(true, Ordering::SeqCst);
assert!(
!ctrlc2.interrupted.load(Ordering::SeqCst),
"Isolated CtrlCState should not be affected by other state changes"
);
#[cfg(unix)]
{
let mut guard = ctrlc1.active_pgid.lock().unwrap();
*guard = Some(12345);
}
let pgid2 = ctrlc2.active_pgid.lock().unwrap();
assert!(
pgid2.is_none(),
"Isolated CtrlCState pgid should remain None"
);
}