#[cfg(unix)]
use std::os::unix::process::ExitStatusExt;
use std::{io, path::Path, time::Duration};
#[cfg(unix)]
use std::{thread, time::Instant};
use futures::{AsyncRead, AsyncReadExt, AsyncWriteExt, future::join};
use ntest::timeout;
#[cfg(any(target_os = "linux", target_os = "android"))]
use rustix::process::{WaitOptions, waitpid};
#[cfg(unix)]
use rustix::{
io::Errno,
process::{Pid, test_kill_process},
};
use crate::{
Local, LocalRuntime, Runtime, SharedRuntime,
process::{Child, Command, Stdio},
};
macro_rules! in_both_modes {
($(#[$attr:meta])* fn $name:ident<$mode:ident>() $body:block) => {
$(#[$attr])*
mod $name {
use super::*;
#[test]
#[ntest::timeout(15000)]
fn local() {
run::<$crate::Local>();
}
#[test]
#[ntest::timeout(15000)]
fn shared() {
run::<$crate::Shared>();
}
fn run<$mode>()
where
$mode: $crate::Mode,
$body
}
};
}
#[test]
#[timeout(15000)]
fn the_builder_forwards_to_the_std_command() {
let mut command = Command::new("program");
command
.arg("one")
.args(["two", "three"])
.env("KEPT", "1")
.envs([("ALSO", "2")])
.env("DROPPED", "3")
.env_remove("DROPPED")
.current_dir("somewhere");
let std = command.as_std();
assert_eq!(std.get_program(), "program");
assert_eq!(std.get_args().collect::<Vec<_>>(), ["one", "two", "three"]);
assert_eq!(std.get_current_dir(), Some(Path::new("somewhere")));
let envs = std
.get_envs()
.map(|(key, value)| (key.to_owned(), value.map(ToOwned::to_owned)))
.collect::<Vec<_>>();
assert!(envs.contains(&("KEPT".into(), Some("1".into()))));
assert!(envs.contains(&("ALSO".into(), Some("2".into()))));
assert!(envs.contains(&("DROPPED".into(), None)));
command.env_clear();
assert_eq!(command.as_std().get_envs().count(), 0);
}
#[test]
#[timeout(15000)]
fn the_getters_read_back_the_kill_and_reap_settings() {
let mut command = Command::new("program");
assert!(!command.get_kill_on_drop());
assert!(command.get_reap_on_drop());
command.kill_on_drop(true);
assert!(command.get_kill_on_drop());
assert!(command.get_reap_on_drop());
command.reap_on_drop(false);
assert!(command.get_kill_on_drop());
assert!(!command.get_reap_on_drop());
command.kill_on_drop(false).reap_on_drop(true);
assert!(!command.get_kill_on_drop());
assert!(command.get_reap_on_drop());
let converted = Command::from(std::process::Command::new("program"));
assert!(!converted.get_kill_on_drop());
assert!(converted.get_reap_on_drop());
}
in_both_modes! {
fn as_std_mut_configures_the_child<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut command = script("echo $ZRUNTIME_TEST", "echo %ZRUNTIME_TEST%");
command.as_std_mut().env("ZRUNTIME_TEST", "set through std");
let output = runtime.block_on(command.output(&runtime)).unwrap();
assert_eq!(text(&output.stdout), "set through std");
}
}
in_both_modes! {
fn a_std_command_converts_with_its_streams_not_configured<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut std = std::process::Command::new(if cfg!(windows) { "cmd" } else { "sh" });
std.arg(if cfg!(windows) { "/C" } else { "-c" })
.arg("echo hello")
.stdout(Stdio::null());
let mut command = Command::from(std);
let output = runtime.block_on(command.output(&runtime)).unwrap();
assert_eq!(text(&output.stdout), "hello");
}
}
in_both_modes! {
fn output_collects_both_streams_and_the_exit_code<M>() {
let runtime = Runtime::<M>::new().unwrap();
let output = runtime
.block_on(
script(
"echo out; echo err >&2; exit 3",
"echo out& echo err 1>&2& exit 3",
)
.output(&runtime),
)
.unwrap();
assert_eq!(output.status.code(), Some(3));
assert_eq!(text(&output.stdout), "out");
assert_eq!(text(&output.stderr), "err");
}
}
in_both_modes! {
fn output_reports_success<M>() {
let runtime = Runtime::<M>::new().unwrap();
let output = runtime
.block_on(shell("exit 0").output(&runtime))
.unwrap();
assert!(output.status.success());
assert_eq!(output.status.code(), Some(0));
assert!(output.stdout.is_empty());
assert!(output.stderr.is_empty());
}
}
in_both_modes! {
fn output_leaves_out_what_is_not_piped<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut command = script("echo out; echo err >&2", "echo out& echo err 1>&2");
command.stdout(Stdio::null());
let output = runtime.block_on(command.output(&runtime)).unwrap();
assert!(output.stdout.is_empty());
assert_eq!(text(&output.stderr), "err");
}
}
in_both_modes! {
fn a_command_runs_again_with_fresh_defaults<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut command = shell("exit 0");
let first = runtime.block_on(command.output(&runtime)).unwrap();
assert!(first.status.success());
let mut child = command.spawn(&runtime).unwrap();
assert!(child.stdin.is_none());
assert!(child.stdout.is_none());
assert!(child.stderr.is_none());
assert!(runtime.block_on(child.status()).unwrap().success());
assert!(runtime.block_on(command.status(&runtime)).unwrap().success());
let mut echoing = shell("echo hello");
for _ in 0..2 {
let output = runtime.block_on(echoing.output(&runtime)).unwrap();
assert_eq!(text(&output.stdout), "hello");
}
}
}
in_both_modes! {
fn a_missing_program_fails_to_spawn<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut command = Command::new("zruntime-no-such-program");
let spawned = command.spawn(&runtime).unwrap_err();
let status = runtime.block_on(command.status(&runtime)).unwrap_err();
let output = runtime.block_on(command.output(&runtime)).unwrap_err();
assert_eq!(spawned.kind(), std::io::ErrorKind::NotFound);
assert_eq!(status.kind(), std::io::ErrorKind::NotFound);
assert_eq!(output.kind(), std::io::ErrorKind::NotFound);
}
}
in_both_modes! {
fn a_child_writes_back_what_it_reads<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut child = filter()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
runtime.block_on(async {
stdin.write_all(b"hello\n").await.unwrap();
drop(stdin);
assert_eq!(text(&read_all(stdout).await), "hello");
assert!(child.status().await.unwrap().success());
});
}
}
in_both_modes! {
fn a_read_that_waits_leaves_the_thread_to_the_other_tasks<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut child = filter()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let (read, ()) = runtime.block_on(join(read_all(stdout), async {
runtime.sleep(Duration::from_millis(50)).await;
stdin.write_all(b"late\n").await.unwrap();
drop(stdin);
}));
assert_eq!(text(&read), "late");
}
}
in_both_modes! {
fn closing_the_input_ends_it<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut child = filter()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
runtime.block_on(async {
stdin.write_all(b"alpha\nbravo\n").await.unwrap();
stdin.close().await.unwrap();
assert_eq!(text(&read_all(stdout).await), "alpha\nbravo");
assert!(child.status().await.unwrap().success());
let written = stdin.write_all(b"charlie\n").await.unwrap_err();
assert_eq!(written.kind(), io::ErrorKind::BrokenPipe);
stdin.flush().await.unwrap();
stdin.close().await.unwrap();
let handed = stdin.into_stdio().await.unwrap_err();
assert_eq!(handed.kind(), io::ErrorKind::BrokenPipe);
});
}
}
in_both_modes! {
#[cfg(unix)]
fn a_large_input_goes_through_and_comes_back<M>() {
let runtime = Runtime::<M>::new().unwrap();
let input = pattern(1_000_000);
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let stdout = child.stdout.take().unwrap();
let (output, ()) = runtime.block_on(join(read_all(stdout), async {
stdin.write_all(&input).await.unwrap();
drop(stdin);
}));
assert_eq!(output, input);
}
}
in_both_modes! {
#[cfg(unix)]
fn vectored_io_goes_through_the_pipes<M>() {
use std::io::{IoSlice, IoSliceMut};
let runtime = Runtime::<M>::new().unwrap();
let mut child = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let mut stdin = child.stdin.take().unwrap();
let mut stdout = child.stdout.take().unwrap();
runtime.block_on(async {
let written = stdin
.write_vectored(&[IoSlice::new(b"ab"), IoSlice::new(b"cd")])
.await
.unwrap();
assert_eq!(written, 4);
drop(stdin);
let (mut first, mut second) = ([0; 1], [0; 8]);
let read = stdout
.read_vectored(&mut [IoSliceMut::new(&mut first), IoSliceMut::new(&mut second)])
.await
.unwrap();
assert!(read >= 1);
assert_eq!(first, *b"a");
assert_eq!(&second[..read - 1], &b"bcd"[..read - 1]);
});
}
}
in_both_modes! {
#[cfg(unix)]
fn output_reads_both_pipes_at_once<M>() {
let runtime = Runtime::<M>::new().unwrap();
let output = runtime
.block_on(
shell("head -c 200000 /dev/zero; head -c 200000 /dev/zero >&2").output(&runtime),
)
.unwrap();
assert!(output.status.success());
assert_eq!(output.stdout.len(), 200_000);
assert_eq!(output.stderr.len(), 200_000);
assert!(output.stdout.iter().chain(&output.stderr).all(|&byte| byte == 0));
}
}
in_both_modes! {
fn status_closes_the_input<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut child = filter()
.stdin(Stdio::piped())
.stdout(Stdio::null())
.spawn(&runtime)
.unwrap();
assert!(child.stdin.is_some());
let status = runtime.block_on(child.status()).unwrap();
assert!(status.success());
assert!(child.stdin.is_none());
}
}
in_both_modes! {
fn output_of_a_child_closes_the_input<M>() {
let runtime = Runtime::<M>::new().unwrap();
let child = filter()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let output = runtime.block_on(child.output()).unwrap();
assert!(output.status.success());
assert!(output.stdout.is_empty());
}
}
in_both_modes! {
fn try_status_follows_the_child<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut child = filter()
.stdin(Stdio::piped())
.stdout(Stdio::null())
.spawn(&runtime)
.unwrap();
assert!(child.try_status().unwrap().is_none());
assert!(child.stdin.is_some());
drop(child.stdin.take());
let status = runtime.block_on(child.status()).unwrap();
assert!(status.success());
assert_eq!(child.try_status().unwrap(), Some(status));
assert_eq!(runtime.block_on(child.status()).unwrap(), status);
}
}
in_both_modes! {
fn a_status_given_up_on_can_be_asked_for_again<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut child = sleeper().spawn(&runtime).unwrap();
runtime.block_on(async {
for _ in 0..3 {
let waited = runtime
.timeout(Duration::from_millis(50), child.status())
.await;
assert!(waited.is_err(), "the child exited by itself");
assert!(child.try_status().unwrap().is_none());
}
child.kill().unwrap();
let status = child.status().await.unwrap();
assert!(!status.success());
});
}
}
in_both_modes! {
fn kill_ends_a_running_child<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut child = sleeper().spawn(&runtime).unwrap();
child.kill().unwrap();
let status = runtime.block_on(child.status()).unwrap();
assert!(!status.success());
#[cfg(unix)]
assert_eq!(status.signal(), Some(9));
child.kill().unwrap();
}
}
in_both_modes! {
#[cfg(unix)]
fn an_output_pipe_becomes_the_input_of_another_child<M>() {
let runtime = Runtime::<M>::new().unwrap();
runtime.block_on(async {
let mut first = shell("sleep 0.2; echo late")
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let pipe = first.stdout.take().unwrap().into_stdio().await.unwrap();
let second = Command::new("cat")
.stdin(pipe)
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let output = second.output().await.unwrap();
assert!(output.status.success());
assert_eq!(text(&output.stdout), "late");
assert!(first.status().await.unwrap().success());
});
}
}
in_both_modes! {
#[cfg(unix)]
fn an_error_pipe_becomes_the_input_of_another_child<M>() {
let runtime = Runtime::<M>::new().unwrap();
runtime.block_on(async {
let mut first = shell("echo oops >&2")
.stderr(Stdio::piped())
.spawn(&runtime)
.unwrap();
let pipe = first.stderr.take().unwrap().into_stdio().await.unwrap();
let second = Command::new("cat")
.stdin(pipe)
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let output = second.output().await.unwrap();
assert_eq!(text(&output.stdout), "oops");
assert!(first.status().await.unwrap().success());
});
}
}
in_both_modes! {
#[cfg(unix)]
fn an_input_pipe_becomes_the_output_of_another_child<M>() {
let runtime = Runtime::<M>::new().unwrap();
runtime.block_on(async {
let mut reader = Command::new("cat")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let pipe = reader.stdin.take().unwrap().into_stdio().await.unwrap();
let stdout = reader.stdout.take().unwrap();
let mut writer = shell("echo hello").stdout(pipe).spawn(&runtime).unwrap();
assert_eq!(text(&read_all(stdout).await), "hello");
assert!(writer.status().await.unwrap().success());
assert!(reader.status().await.unwrap().success());
});
}
}
in_both_modes! {
#[cfg(unix)]
fn kill_on_drop_kills_the_process<M>() {
let runtime = Runtime::<M>::new().unwrap();
let child = sleeper().kill_on_drop(true).spawn(&runtime).unwrap();
let id = child.id();
assert!(exists(id), "the child is not running");
drop(child);
assert!(disappears(id), "the child is still there after being dropped");
}
}
in_both_modes! {
#[cfg(unix)]
fn a_dropped_child_is_reaped_once_it_exits<M>() {
let runtime = Runtime::<M>::new().unwrap();
let child = shell("sleep 0.3").spawn(&runtime).unwrap();
let id = child.id();
drop(child);
assert!(disappears(id), "the child was never collected");
}
}
in_both_modes! {
#[cfg(unix)]
fn a_child_dropped_after_a_given_up_wait_is_reaped<M>() {
let runtime = Runtime::<M>::new().unwrap();
let mut child = shell("sleep 0.5").spawn(&runtime).unwrap();
let id = child.id();
let waited = runtime.block_on(runtime.timeout(Duration::from_millis(50), child.status()));
assert!(waited.is_err(), "the child exited by itself");
drop(child);
assert!(disappears(id), "the child was never collected");
}
}
#[test]
#[timeout(15000)]
#[cfg(any(target_os = "linux", target_os = "android"))]
fn reap_on_drop_off_leaves_the_process_for_someone_else() {
let runtime = LocalRuntime::new().unwrap();
let child = shell("exit 0").reap_on_drop(false).spawn(&runtime).unwrap();
let id = child.id();
let pid = Pid::from_raw(id as i32).expect("a child's process ID is not zero");
drop(child);
thread::sleep(Duration::from_millis(300));
assert!(test_kill_process(pid).is_ok(), "the child was collected");
waitpid(Some(pid), WaitOptions::empty()).unwrap();
assert_eq!(test_kill_process(pid), Err(Errno::SRCH));
}
#[test]
#[timeout(15000)]
fn a_shared_runtime_waits_for_a_child_on_a_task() {
let runtime = SharedRuntime::new().unwrap();
let mut child = script("exit 3", "exit 3").spawn(&runtime).unwrap();
let waiting = runtime.spawn("a task that waits for a child", async move {
child.status().await
});
let status = runtime.block_on(waiting).unwrap().unwrap();
assert_eq!(status.code(), Some(3));
let output = script("echo out; exit 4", "echo out& exit 4").output(&runtime);
let collecting = runtime.spawn("a task that collects the output of a child", output);
let output = runtime.block_on(collecting).unwrap().unwrap();
assert_eq!(output.status.code(), Some(4));
assert_eq!(text(&output.stdout), "out");
}
#[test]
#[timeout(15000)]
fn a_shared_child_goes_to_another_thread() {
let runtime = SharedRuntime::new().unwrap();
let mut child = filter()
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.spawn(&runtime)
.unwrap();
let echoed = std::thread::scope(|scope| {
scope
.spawn(|| {
runtime.block_on(async {
let mut stdin = child.stdin.take().unwrap();
stdin.write_all(b"moved\n").await.unwrap();
drop(stdin);
read_all(child.stdout.take().unwrap()).await
})
})
.join()
.unwrap()
});
assert_eq!(text(&echoed), "moved");
}
#[test]
#[timeout(15000)]
fn the_types_print() {
let runtime = LocalRuntime::new().unwrap();
let mut command = filter();
command.stdin(Stdio::piped()).stdout(Stdio::piped());
let mut child: Child<Local> = command.spawn(&runtime).unwrap();
let id = child.id();
assert!(format!("{command:?}").contains(if cfg!(windows) { "sort" } else { "cat" }));
let printed = format!("{child:?}");
assert!(printed.contains("Child"), "{printed}");
assert!(printed.contains(&id.to_string()), "{printed}");
assert!(printed.contains("ChildStdin"), "{printed}");
assert!(printed.contains("ChildStdout"), "{printed}");
assert_eq!(
format!("{:?}", child.stderr),
"None",
"a pipe that was not asked for is not there"
);
drop(child.stdin.take());
assert!(runtime.block_on(child.status()).unwrap().success());
}
fn shell(script: &str) -> Command {
let (program, flag) = if cfg!(windows) {
("cmd", "/C")
} else {
("sh", "-c")
};
let mut command = Command::new(program);
command.arg(flag).arg(script);
command
}
fn script(unix: &str, windows: &str) -> Command {
shell(if cfg!(windows) { windows } else { unix })
}
fn filter() -> Command {
Command::new(if cfg!(windows) { "sort" } else { "cat" })
}
fn sleeper() -> Command {
if cfg!(windows) {
let mut command = Command::new("ping");
command
.args(["-n", "10", "127.0.0.1"])
.stdout(Stdio::null());
command
} else {
let mut command = Command::new("sleep");
command.arg("10");
command
}
}
async fn read_all<R>(mut reader: R) -> Vec<u8>
where
R: AsyncRead + Unpin,
{
let mut data = Vec::new();
reader.read_to_end(&mut data).await.unwrap();
data
}
fn text(bytes: &[u8]) -> String {
String::from_utf8_lossy(bytes)
.replace("\r\n", "\n")
.trim()
.to_owned()
}
#[cfg(unix)]
fn pattern(len: usize) -> Vec<u8> {
(0..len).map(|i| (i % 251) as u8).collect()
}
#[cfg(unix)]
fn exists(id: u32) -> bool {
let pid = Pid::from_raw(id as i32).expect("a child's process ID is not zero");
test_kill_process(pid) != Err(Errno::SRCH)
}
#[cfg(unix)]
fn disappears(id: u32) -> bool {
let deadline = Instant::now() + Duration::from_secs(5);
while exists(id) {
if Instant::now() > deadline {
return false;
}
thread::sleep(Duration::from_millis(10));
}
true
}