use coreshift_core::inotify::decode_events;
use coreshift_core::spawn::{
CancelPolicy, ExitStatus, ProcessGroup, SpawnBackend, SpawnFdPolicy, SpawnOptions, spawn_start,
};
use std::fs::File;
use std::os::unix::io::AsRawFd;
use std::time::Instant;
#[test]
fn test_spawn_timeout_no_capture_returns_quickly() {
let start = Instant::now();
let output = SpawnOptions::builder(
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"sleep 30".to_string(),
],
SpawnBackend::PosixSpawn,
)
.timeout_ms(200)
.build()
.unwrap()
.run()
.unwrap();
assert!(
output.timed_out,
"a deadline must be reported as timed out, not block past it"
);
assert!(
start.elapsed().as_millis() < 2000,
"wait_loop escaped the deadline via wait_blocking (took {}ms)",
start.elapsed().as_millis()
);
assert_eq!(output.status, Some(ExitStatus::Signaled(9)));
}
#[test]
fn test_spawn_echo_capture() {
let output = SpawnOptions::builder(
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"echo hello world".to_string(),
],
SpawnBackend::PosixSpawn,
)
.capture_stdout()
.build()
.unwrap()
.run()
.unwrap();
assert_eq!(output.status, Some(ExitStatus::Exited(0)));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"hello world"
);
}
#[test]
fn test_spawn_large_stdout() {
let script = "for i in $(seq 1 10000); do echo \"line $i\"; done";
let output = SpawnOptions::builder(
vec!["/bin/sh".to_string(), "-c".to_string(), script.to_string()],
SpawnBackend::PosixSpawn,
)
.capture_stdout()
.max_output(200_000)
.build()
.unwrap()
.run()
.unwrap();
assert_eq!(output.status, Some(ExitStatus::Exited(0)));
assert!(output.stdout.len() > 65536);
}
#[test]
fn test_inotify_decode_with_name() {
let mut buf = Vec::new();
let name = "test_file.txt";
let name_bytes = name.as_bytes();
let mut name_with_padding = name_bytes.to_vec();
name_with_padding.push(0); while !name_with_padding.len().is_multiple_of(8) {
name_with_padding.push(0); }
let name_len = name_with_padding.len() as u32;
buf.extend_from_slice(&1i32.to_ne_bytes());
buf.extend_from_slice(&libc::IN_MODIFY.to_ne_bytes());
buf.extend_from_slice(&0u32.to_ne_bytes());
buf.extend_from_slice(&name_len.to_ne_bytes());
buf.extend_from_slice(&name_with_padding);
let events = decode_events(&buf).unwrap();
assert_eq!(events.len(), 1);
assert_eq!(events[0].wd, 1);
assert_eq!(events[0].name, Some(name.as_bytes().to_vec()));
}
#[test]
fn test_exec_context_many_args() {
let mut args = vec![
"/bin/sh".to_string(),
"-c".to_string(),
"echo $@".to_string(),
"--".to_string(),
];
for i in 0..100 {
args.push(format!("arg{}", i));
}
let output = SpawnOptions::builder(args, SpawnBackend::PosixSpawn)
.capture_stdout()
.build()
.unwrap()
.run()
.unwrap();
assert_eq!(output.status, Some(ExitStatus::Exited(0)));
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("arg0"));
assert!(stdout.contains("arg99"));
}
#[test]
fn test_spawn_timeout_wedged_pipe_partial_output() {
let start = Instant::now();
let output = SpawnOptions::builder(
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"(sleep 1; echo late) & echo immediate; exit 0".to_string(),
],
SpawnBackend::PosixSpawn,
)
.capture_stdout()
.timeout_ms(200)
.build()
.unwrap()
.run()
.unwrap();
assert!(
output.timed_out,
"wedged pipe with an elapsed deadline must report timed_out"
);
assert!(
start.elapsed().as_millis() < 900,
"wait_loop hung on the wedged pipe (took {}ms)",
start.elapsed().as_millis()
);
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("immediate"),
"partial output must contain the data drained before the kill"
);
assert!(
!stdout.contains("late"),
"must return partial output, not wait for the wedged pipe's late data"
);
}
#[test]
fn test_vfork_echo_capture() {
let output = SpawnOptions::builder(
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"echo vfork hello".to_string(),
],
SpawnBackend::Vfork,
)
.capture_stdout()
.build()
.unwrap()
.run()
.unwrap();
assert_eq!(output.status, Some(ExitStatus::Exited(0)));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"vfork hello"
);
}
#[test]
fn test_clone3_echo_capture() {
let output = SpawnOptions::builder(
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"echo clone3 hello".to_string(),
],
SpawnBackend::Clone3,
)
.capture_stdout()
.build()
.unwrap()
.run()
.unwrap();
assert_eq!(output.status, Some(ExitStatus::Exited(0)));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"clone3 hello"
);
}
#[test]
fn test_clone3_pidfd_echo_capture() {
let output = SpawnOptions::builder(
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"echo pidfd hello".to_string(),
],
SpawnBackend::Clone3Pidfd,
)
.capture_stdout()
.build()
.unwrap()
.run()
.unwrap();
assert_eq!(output.status, Some(ExitStatus::Exited(0)));
assert_eq!(
String::from_utf8_lossy(&output.stdout).trim(),
"pidfd hello"
);
}
#[test]
fn test_clone3_pidfd_exposes_pidfd() {
let running = spawn_start(
SpawnOptions::builder(vec!["/bin/true".to_string()], SpawnBackend::Clone3Pidfd)
.build()
.unwrap(),
)
.unwrap();
assert!(
running.process.pidfd().is_some(),
"Clone3Pidfd must attach a pidfd to the Process handle"
);
assert_eq!(
running.process.wait_blocking().unwrap(),
ExitStatus::Exited(0)
);
}
#[test]
fn test_vfork_stdin_capture() {
let output = SpawnOptions::builder(
vec!["/bin/sh".to_string(), "-c".to_string(), "cat".to_string()],
SpawnBackend::Vfork,
)
.stdin(b"vfork stdin data".to_vec())
.capture_stdout()
.build()
.unwrap()
.run()
.unwrap();
assert_eq!(output.status, Some(ExitStatus::Exited(0)));
assert_eq!(String::from_utf8_lossy(&output.stdout), "vfork stdin data");
}
#[test]
fn test_vfork_isolated_pgroup_closefrom3_timeout_kills_group() {
let output = SpawnOptions::builder(
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"trap '' TERM; while true; do sleep 1; done".to_string(),
],
SpawnBackend::Vfork,
)
.pgroup(ProcessGroup::new(None, true))
.fd_policy(SpawnFdPolicy::CloseFrom3)
.cancel(CancelPolicy::Graceful)
.kill_grace_ms(100)
.timeout_ms(200)
.build()
.unwrap()
.run()
.unwrap();
assert!(output.timed_out, "vfork child must be timed out");
assert_eq!(output.status, Some(ExitStatus::Signaled(9)));
}
#[test]
fn test_vfork_close_from3_closes_inherited_fd() {
let file = File::open("/dev/null").unwrap();
let fd = file.as_raw_fd();
let flags = unsafe { libc::fcntl(fd, libc::F_GETFD) };
assert!(flags >= 0);
let ret = unsafe { libc::fcntl(fd, libc::F_SETFD, flags & !libc::FD_CLOEXEC) };
assert_eq!(ret, 0);
let script = format!("if [ -e /proc/$$/fd/{fd} ]; then echo open; else echo closed; fi");
let output = SpawnOptions::builder(
vec!["/bin/sh".to_string(), "-c".to_string(), script],
SpawnBackend::Vfork,
)
.fd_policy(SpawnFdPolicy::CloseFrom3)
.capture_stdout()
.build()
.unwrap()
.run()
.unwrap();
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "closed");
}
#[test]
fn test_vfork_exec_failure_reports_error() {
let err = SpawnOptions::builder(
vec!["/definitely/missing/coreshift-core-bin".to_string()],
SpawnBackend::Vfork,
)
.build()
.unwrap()
.run()
.unwrap_err();
assert_eq!(err.raw_os_error(), Some(libc::ENOENT));
}
#[test]
fn test_clone3_exec_failure_reports_error() {
let err = SpawnOptions::builder(
vec!["/definitely/missing/coreshift-core-bin".to_string()],
SpawnBackend::Clone3,
)
.build()
.unwrap()
.run()
.unwrap_err();
assert_eq!(err.raw_os_error(), Some(libc::ENOENT));
}
#[test]
fn test_clone3_pidfd_timeout_graceful_kills() {
let output = SpawnOptions::builder(
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"trap '' TERM; while true; do sleep 1; done".to_string(),
],
SpawnBackend::Clone3Pidfd,
)
.cancel(CancelPolicy::Graceful)
.kill_grace_ms(100)
.timeout_ms(200)
.build()
.unwrap()
.run()
.unwrap();
assert!(output.timed_out, "pidfd child must be timed out");
assert_eq!(output.status, Some(ExitStatus::Signaled(9)));
}