use std::time::{Duration, Instant};
use super::exec_signal_delay;
use crate::unix::{ExitStatusExt, JobExt, PipelineExt};
use crate::{Exec, ExecExt, ExitStatus, Redirection};
const ISOLATED_TEST_PREFIX: &str = "SUBPROCESS_ISOLATED_FD_TEST_";
fn run_isolated(name: &str, body: impl FnOnce()) {
let parent_var = format!("{ISOLATED_TEST_PREFIX}{}", unsafe { libc::getppid() });
if std::env::var_os(&parent_var).is_some() {
body();
return;
}
let exe = std::env::current_exe().expect("current_exe");
let test_path = format!("tests::posix::{name}");
let child_var = format!("{ISOLATED_TEST_PREFIX}{}", std::process::id());
let output = std::process::Command::new(&exe)
.args(["--exact", &test_path])
.env(&child_var, "1")
.output()
.expect("spawning isolated test child");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"isolated child for {test_path} failed: {status}\n\
--- child stdout ---\n{stdout}\
--- child stderr ---\n{stderr}",
status = output.status,
);
assert!(
stdout.contains("1 passed"),
"isolated child for {test_path} matched no tests (typo in name?):\n\
--- child stdout ---\n{stdout}",
);
}
#[test]
fn err_terminate() {
let job = Exec::cmd("sleep").arg("5").start().unwrap();
exec_signal_delay();
assert!(job.poll().is_none());
job.terminate().unwrap();
assert!(job.wait().unwrap().is_killed_by(libc::SIGTERM));
}
#[test]
fn waitpid_echild() {
let job = Exec::cmd("true").start().unwrap();
let pid = job.pid() as i32;
let mut status = 0 as libc::c_int;
let wpid = unsafe { libc::waitpid(pid, &mut status, 0) };
assert_eq!(wpid, pid);
assert_eq!(status, 0);
let exit = job.wait().unwrap();
assert!(exit.code().is_none() && exit.signal().is_none());
}
#[test]
fn send_signal() {
let job = Exec::cmd("sleep").arg("5").start().unwrap();
exec_signal_delay();
job.send_signal(libc::SIGUSR1).unwrap();
assert_eq!(job.wait().unwrap().signal(), Some(libc::SIGUSR1));
}
#[test]
fn env_set_all_1() {
let out = Exec::cmd("env")
.env_clear()
.stdout(Redirection::Pipe)
.capture()
.unwrap()
.stdout_str();
assert_eq!(out, "");
}
#[test]
fn env_set_all_2() {
let out = Exec::cmd("env")
.env_clear()
.env("FOO", "bar")
.stdout(Redirection::Pipe)
.capture()
.unwrap()
.stdout_str();
assert_eq!(out.trim_end(), "FOO=bar");
}
#[test]
fn exec_setpgid() {
let job = Exec::cmd("sh")
.args(["-c", "sleep 10 & wait"])
.setpgid()
.start()
.unwrap();
exec_signal_delay();
job.send_signal_group(libc::SIGTERM).unwrap();
assert!(job.wait().unwrap().is_killed_by(libc::SIGTERM));
}
#[test]
fn send_signal_group() {
let job = Exec::cmd("sh")
.args(["-c", "sleep 10 & wait"])
.setpgid()
.start()
.unwrap();
exec_signal_delay();
job.send_signal_group(libc::SIGTERM).unwrap();
assert!(job.wait().unwrap().is_killed_by(libc::SIGTERM));
}
#[test]
fn send_signal_group_after_finish() {
let job = Exec::cmd("true").setpgid().start().unwrap();
job.wait().unwrap();
job.send_signal_group(libc::SIGTERM).unwrap();
}
#[test]
fn kill_process() {
let job = Exec::cmd("sleep").arg("10").start().unwrap();
exec_signal_delay();
job.kill().unwrap();
assert!(job.wait().unwrap().is_killed_by(libc::SIGKILL));
}
#[test]
fn kill_vs_terminate() {
let j1 = Exec::cmd("sleep").arg("10").start().unwrap();
exec_signal_delay();
j1.terminate().unwrap();
let status1 = j1.wait().unwrap();
let j2 = Exec::cmd("sleep").arg("10").start().unwrap();
exec_signal_delay();
j2.kill().unwrap();
let status2 = j2.wait().unwrap();
assert!(status1.is_killed_by(libc::SIGTERM));
assert!(status2.is_killed_by(libc::SIGKILL));
assert_ne!(status1, status2);
}
#[test]
fn exit_status_code() {
assert_eq!(ExitStatus::from_raw(0 << 8).code(), Some(0));
assert_eq!(ExitStatus::from_raw(1 << 8).code(), Some(1));
assert_eq!(ExitStatus::from_raw(42 << 8).code(), Some(42));
assert_eq!(ExitStatus::from_raw(9).code(), None); }
#[test]
fn exit_status_signal() {
assert_eq!(ExitStatus::from_raw(9).signal(), Some(9)); assert_eq!(
ExitStatus::from_raw(libc::SIGTERM).signal(),
Some(libc::SIGTERM)
);
assert_eq!(ExitStatus::from_raw(0 << 8).signal(), None);
assert_eq!(ExitStatus::from_raw(1 << 8).signal(), None);
}
#[test]
fn exit_status_display() {
assert_eq!(ExitStatus::from_raw(0 << 8).to_string(), "exit code 0");
assert_eq!(ExitStatus::from_raw(1 << 8).to_string(), "exit code 1");
assert_eq!(ExitStatus::from_raw(9).to_string(), "signal 9");
}
#[test]
fn exit_status_ext_round_trip() {
let status = <ExitStatus as ExitStatusExt>::from_raw(42 << 8);
assert_eq!(status.into_raw(), Some(42 << 8));
}
#[test]
fn pre_exec_runs() {
let job = unsafe {
Exec::cmd("true")
.pre_exec(|| libc::_exit(42))
.start()
.unwrap()
};
let status = job.wait().unwrap();
assert_eq!(status.code(), Some(42));
}
#[test]
fn pre_exec_error_reported() {
let result = unsafe {
Exec::cmd("true")
.pre_exec(|| Err(std::io::Error::from_raw_os_error(libc::EACCES)))
.start()
};
let err = result.unwrap_err();
assert_eq!(err.raw_os_error(), Some(libc::EACCES));
}
#[test]
fn pre_exec_multiple() {
use std::io::Read;
use std::os::fd::AsRawFd;
let (mut read_end, write_end) = crate::posix::pipe().unwrap();
let fd = write_end.as_raw_fd();
let job = unsafe {
Exec::cmd("true")
.pre_exec(move || {
let n = libc::write(fd, b"1".as_ptr().cast(), 1);
if n != 1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
})
.pre_exec(move || {
let n = libc::write(fd, b"2".as_ptr().cast(), 1);
if n != 1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
})
.start()
.unwrap()
};
drop(write_end);
let mut buf = [0u8; 2];
read_end.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"12");
job.wait().unwrap();
}
#[test]
fn arg0_override() {
let out = Exec::cmd("sh")
.arg0("custom-name")
.args(["-c", "echo $0"])
.capture()
.unwrap()
.stdout_str();
assert_eq!(out.trim(), "custom-name");
}
#[test]
fn started_send_signal() {
let job = Exec::cmd("sleep").arg("5").start().unwrap();
exec_signal_delay();
job.send_signal(libc::SIGTERM).unwrap();
let status = job.wait().unwrap();
assert!(status.is_killed_by(libc::SIGTERM));
}
#[test]
fn started_send_signal_group() {
let job = Exec::cmd("sh")
.args(["-c", "sleep 10 & wait"])
.setpgid()
.start()
.unwrap();
exec_signal_delay();
job.send_signal_group(libc::SIGKILL).unwrap();
let status = job.wait().unwrap();
assert!(status.is_killed_by(libc::SIGKILL) || status.is_killed_by(libc::SIGTERM));
}
#[test]
fn pipeline_setpgid() {
let handle = (Exec::cmd("sleep").arg("5") | Exec::cmd("sleep").arg("5"))
.setpgid()
.start()
.unwrap();
assert_eq!(handle.processes.len(), 2);
exec_signal_delay();
handle.send_signal_group(libc::SIGTERM).unwrap();
for p in &handle.processes {
let status = p.wait().unwrap();
assert!(status.is_killed_by(libc::SIGTERM));
}
}
#[test]
fn pipeline_setpgid_rejects_exec_setpgid() {
let result = (Exec::cmd("true").setpgid() | Exec::cmd("true")).start();
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert!(err.to_string().contains("setpgid"));
}
#[test]
fn user_file_at_target_fd_survives_exec() {
run_isolated("user_file_at_target_fd_survives_exec", || {
use std::fs::File;
use std::os::fd::AsRawFd;
use tempfile::TempDir;
let tmpdir = TempDir::new().unwrap();
let tmpname = tmpdir.path().join("input");
std::fs::write(&tmpname, "stdin-payload").unwrap();
let saved = unsafe { libc::dup(0) };
assert!(saved >= 0);
let close_rc = unsafe { libc::close(0) };
assert_eq!(close_rc, 0);
let f = File::open(&tmpname).unwrap();
assert_eq!(f.as_raw_fd(), 0, "test setup: file did not land at fd 0");
let dup_rc = unsafe { libc::dup2(saved, 100) };
assert!(dup_rc >= 0);
unsafe {
libc::close(saved);
}
let result = Exec::cmd("cat")
.stdin(f)
.stdout(Redirection::Pipe)
.stderr(Redirection::Pipe)
.capture();
unsafe {
libc::dup2(100, 0);
libc::close(100);
}
let c = result.expect("capture failed");
assert_eq!(
c.stdout_str(),
"stdin-payload",
"stderr was: {:?}",
c.stderr_str()
);
assert!(c.exit_status.success());
});
}
#[test]
fn user_file_at_other_standard_fd_preserves_inherited_stream() {
run_isolated(
"user_file_at_other_standard_fd_preserves_inherited_stream",
|| {
use std::fs::File;
use std::io::Read;
use std::os::fd::AsRawFd;
use tempfile::TempDir;
let tmpdir = TempDir::new().unwrap();
let tmpname = tmpdir.path().join("output");
let saved = unsafe { libc::dup(2) };
assert!(saved >= 0);
let close_rc = unsafe { libc::close(2) };
assert_eq!(close_rc, 0);
let f = File::create(&tmpname).unwrap();
assert_eq!(f.as_raw_fd(), 2, "test setup: file did not land at fd 2");
let dup_rc = unsafe { libc::dup2(saved, 100) };
assert!(dup_rc >= 0);
unsafe {
libc::close(saved);
}
let (mut read_end, write_end) = crate::posix::pipe().unwrap();
let report_fd = write_end.as_raw_fd();
let result = unsafe {
Exec::cmd("true")
.stdout(f)
.pre_exec(move || {
let r = libc::fcntl(2, libc::F_GETFD);
let msg: &[u8] = if r >= 0 { b"open" } else { b"clsd" };
libc::write(report_fd, msg.as_ptr().cast(), msg.len());
Ok(())
})
.start()
};
unsafe {
libc::dup2(100, 2);
libc::close(100);
}
drop(write_end);
let job = result.expect("start failed");
let mut buf = [0u8; 4];
read_end.read_exact(&mut buf).unwrap();
let _ = job.wait();
assert_eq!(
&buf, b"open",
"fd 2 was closed in the child by install_child_fd"
);
},
);
}
#[test]
fn stdin_pipe_with_user_stdout_at_fd_0() {
run_isolated("stdin_pipe_with_user_stdout_at_fd_0", || {
use std::fs::File;
use std::os::fd::AsRawFd;
use tempfile::TempDir;
let tmpdir = TempDir::new().unwrap();
let outfile = tmpdir.path().join("output");
let saved = unsafe { libc::dup(0) };
assert!(saved >= 0);
assert_eq!(unsafe { libc::close(0) }, 0);
let f = File::create(&outfile).unwrap();
assert_eq!(f.as_raw_fd(), 0, "test setup: file did not land at fd 0");
assert!(unsafe { libc::dup2(saved, 100) } >= 0);
unsafe {
libc::close(saved);
}
let result = Exec::cmd("printf")
.args(["%s", "hello"])
.stdin(Redirection::Pipe)
.stdout(f)
.stderr(Redirection::Pipe)
.capture();
unsafe {
libc::dup2(100, 0);
libc::close(100);
}
let c = result.expect("capture failed");
assert!(
c.exit_status.success(),
"printf failed; stderr: {:?}",
c.stderr_str()
);
let content = std::fs::read_to_string(&outfile).unwrap();
assert_eq!(content, "hello");
});
}
#[test]
fn stdout_pipe_with_user_stderr_at_fd_1() {
run_isolated("stdout_pipe_with_user_stderr_at_fd_1", || {
use std::fs::File;
use std::os::fd::AsRawFd;
use tempfile::TempDir;
let tmpdir = TempDir::new().unwrap();
let errfile = tmpdir.path().join("err");
let saved = unsafe { libc::dup(1) };
assert!(saved >= 0);
assert_eq!(unsafe { libc::close(1) }, 0);
let f = File::create(&errfile).unwrap();
assert_eq!(f.as_raw_fd(), 1, "test setup: file did not land at fd 1");
assert!(unsafe { libc::dup2(saved, 100) } >= 0);
unsafe {
libc::close(saved);
}
let result = Exec::cmd("sh")
.args(["-c", "echo to-stdout; echo to-stderr >&2"])
.stdout(Redirection::Pipe)
.stderr(f)
.capture();
unsafe {
libc::dup2(100, 1);
libc::close(100);
}
let c = result.expect("capture failed");
assert!(c.exit_status.success());
assert_eq!(c.stdout_str().trim(), "to-stdout");
let stderr_content = std::fs::read_to_string(&errfile).unwrap();
assert_eq!(stderr_content.trim(), "to-stderr");
});
}
#[test]
fn stdin_pipe_with_user_stderr_at_fd_0() {
run_isolated("stdin_pipe_with_user_stderr_at_fd_0", || {
use std::fs::File;
use std::os::fd::AsRawFd;
use tempfile::TempDir;
let tmpdir = TempDir::new().unwrap();
let errfile = tmpdir.path().join("err");
let saved = unsafe { libc::dup(0) };
assert!(saved >= 0);
assert_eq!(unsafe { libc::close(0) }, 0);
let f = File::create(&errfile).unwrap();
assert_eq!(f.as_raw_fd(), 0, "test setup: file did not land at fd 0");
assert!(unsafe { libc::dup2(saved, 100) } >= 0);
unsafe {
libc::close(saved);
}
let result = Exec::cmd("sh")
.args(["-c", "echo to-stderr >&2"])
.stdin(Redirection::Pipe)
.stdout(Redirection::Pipe)
.stderr(f)
.capture();
unsafe {
libc::dup2(100, 0);
libc::close(100);
}
let c = result.expect("capture failed");
assert!(c.exit_status.success());
let stderr_content = std::fs::read_to_string(&errfile).unwrap();
assert_eq!(stderr_content.trim(), "to-stderr");
});
}
#[test]
fn user_files_with_swapped_fds_resolve_cycle() {
run_isolated("user_files_with_swapped_fds_resolve_cycle", || {
use std::fs::File;
use std::os::fd::AsRawFd;
use tempfile::TempDir;
let tmpdir = TempDir::new().unwrap();
let path_at_1 = tmpdir.path().join("at_fd_1");
let path_at_2 = tmpdir.path().join("at_fd_2");
let saved_1 = unsafe { libc::dup(1) };
let saved_2 = unsafe { libc::dup(2) };
assert!(saved_1 >= 0 && saved_2 >= 0);
assert_eq!(unsafe { libc::close(1) }, 0);
assert_eq!(unsafe { libc::close(2) }, 0);
let file_at_1 = File::create(&path_at_1).unwrap();
assert_eq!(file_at_1.as_raw_fd(), 1);
let file_at_2 = File::create(&path_at_2).unwrap();
assert_eq!(file_at_2.as_raw_fd(), 2);
assert!(unsafe { libc::dup2(saved_1, 100) } >= 0);
assert!(unsafe { libc::dup2(saved_2, 101) } >= 0);
unsafe {
libc::close(saved_1);
libc::close(saved_2);
}
let result = Exec::cmd("sh")
.args(["-c", "echo out; echo err >&2"])
.stdout(file_at_2)
.stderr(file_at_1)
.join();
unsafe {
libc::dup2(100, 1);
libc::dup2(101, 2);
libc::close(100);
libc::close(101);
}
let status = result.expect("spawn failed");
assert!(status.success());
let content_at_1 = std::fs::read_to_string(&path_at_1).unwrap();
let content_at_2 = std::fs::read_to_string(&path_at_2).unwrap();
assert_eq!(
content_at_1.trim(),
"err",
"stderr file should contain 'err'"
);
assert_eq!(
content_at_2.trim(),
"out",
"stdout file should contain 'out'"
);
});
}
#[cfg(target_os = "linux")]
#[test]
fn pipeline_stderr_all_non_cloexec_file_does_not_leak() {
use std::fs::File;
use std::os::fd::AsRawFd;
use tempfile::TempDir;
let tmpdir = TempDir::new().unwrap();
let errfile = tmpdir.path().join("err");
let report = tmpdir.path().join("report");
let f = File::create(&errfile).unwrap();
let raw = f.as_raw_fd();
unsafe {
let flags = libc::fcntl(raw, libc::F_GETFD);
assert!(flags >= 0);
let r = libc::fcntl(raw, libc::F_SETFD, flags & !libc::FD_CLOEXEC);
assert_eq!(r, 0);
}
let check_cmd = format!(
"if [ -e /proc/self/fd/{} ]; then echo LEAK > {}; \
else echo CLEAR > {}; fi",
raw,
report.display(),
report.display(),
);
let p = Exec::shell(check_cmd) | Exec::cmd("true");
p.stderr_all(f).join().unwrap();
let content = std::fs::read_to_string(&report).unwrap();
assert_eq!(
content.trim(),
"CLEAR",
"user File fd was leaked into a non-last pipeline child"
);
}
#[test]
fn null_redirect_does_not_leak_fd() {
let start = Instant::now();
let status = Exec::cmd("sh")
.args(["-c", "sleep 10 &"])
.stdout(Redirection::Null)
.stderr(Redirection::Null)
.join()
.unwrap();
assert!(status.success());
assert!(
start.elapsed() < Duration::from_secs(5),
"join() took too long, /dev/null fds may have leaked"
);
}
#[test]
fn poll_does_not_block_during_wait() {
use std::thread;
let job = Exec::cmd("sleep").arg("5").start().unwrap();
let process = job.processes[0].clone();
let waiter_proc = process.clone();
let waiter = thread::spawn(move || waiter_proc.wait().unwrap());
thread::sleep(Duration::from_millis(100));
let start = Instant::now();
let status = process.poll();
let elapsed = start.elapsed();
assert!(status.is_none(), "child should still be running");
assert!(
elapsed < Duration::from_millis(200),
"poll() took {:?}, expected to return immediately while wait() blocks",
elapsed
);
process.terminate().unwrap();
let _ = waiter.join().unwrap();
}
#[test]
fn terminate_during_wait() {
use std::thread;
let job = Exec::cmd("sleep").arg("10").start().unwrap();
let process = job.processes[0].clone();
let waiter_proc = process.clone();
let waiter = thread::spawn(move || waiter_proc.wait().unwrap());
thread::sleep(Duration::from_millis(100));
let start = Instant::now();
process.terminate().unwrap();
let term_elapsed = start.elapsed();
assert!(
term_elapsed < Duration::from_millis(200),
"terminate() took {:?}, expected to return immediately while wait() blocks",
term_elapsed
);
let status = waiter.join().unwrap();
assert!(status.is_killed_by(libc::SIGTERM));
}