use atap::{Runtime, RuntimeError, TaskHandle, process::Process, signal::SignalKind};
use std::{
fs, thread,
time::{Duration, Instant},
};
const PATIENCE: Duration = Duration::from_secs(20);
fn settled<T>(handle: &TaskHandle<T>, patience: Duration) -> Option<T> {
let deadline = Instant::now() + patience;
while Instant::now() < deadline {
match handle.try_take() {
Ok(value) => return Some(value),
Err(RuntimeError::NotReady) => thread::sleep(Duration::from_millis(1)),
Err(_) => break,
}
}
None
}
#[test]
fn a_run_reports_its_exit_code() {
let _ = Runtime::init();
println!("running /usr/bin/true");
let ok =
Runtime::block(Process::run("/usr/bin/true", Process::NO_ARGS)).expect("true must run");
assert!(ok.success(), "true must succeed, got {:?}", ok);
assert_eq!(ok.code(), Some(0), "true must exit zero");
println!("running /usr/bin/false");
let failed =
Runtime::block(Process::run("/usr/bin/false", Process::NO_ARGS)).expect("false must run");
assert!(!failed.success(), "false must not succeed");
assert_eq!(
failed.code(),
Some(1),
"false must exit one, got {:?}",
failed.code()
);
assert_eq!(failed.signal(), None, "false was not killed");
}
#[test]
fn output_comes_back_on_the_right_stream() {
let _ = Runtime::init();
let handle = Runtime::task(Process::output(
"/bin/sh",
["-c", "echo out; echo err 1>&2"],
))
.spawn();
let found = settled(&handle, PATIENCE)
.expect("a two line child must settle")
.expect("sh must run");
assert_eq!(
found.stdout(),
b"out\n",
"stdout was {:?}",
String::from_utf8_lossy(found.stdout())
);
assert_eq!(
found.stderr(),
b"err\n",
"stderr was {:?}",
String::from_utf8_lossy(found.stderr())
);
assert!(found.status().success(), "the shell itself must succeed");
}
#[test]
fn a_child_that_floods_both_pipes_does_not_deadlock() {
let _ = Runtime::init();
const FLOOD: usize = 4 * 1024 * 1024;
let script = format!("head -c {FLOOD} /dev/zero & head -c {FLOOD} /dev/zero 1>&2; wait");
println!("flooding both streams with {FLOOD} bytes each");
let handle = Runtime::task(Process::output("/bin/sh", ["-c", &script])).spawn();
let found = settled(&handle, PATIENCE)
.expect("a flooding child must settle rather than deadlock")
.expect("sh must run");
assert_eq!(found.stdout().len(), FLOOD, "stdout was truncated");
assert_eq!(found.stderr().len(), FLOOD, "stderr was truncated");
}
#[test]
fn many_children_at_once() {
let _ = Runtime::init();
const CHILDREN: usize = 16;
let handles = (0..CHILDREN)
.map(|index| {
Runtime::task(Process::output("/bin/sh", ["-c", &format!("echo {index}")])).spawn()
})
.collect::<Vec<_>>();
println!("waiting on {CHILDREN} children at once");
for (index, handle) in handles.iter().enumerate() {
let found = settled(handle, PATIENCE)
.unwrap_or_else(|| panic!("child {index} never settled"))
.unwrap_or_else(|error| panic!("child {index} could not run: {error}"));
assert_eq!(
found.stdout(),
format!("{index}\n").as_bytes(),
"child {index} came back with somebody else's output"
);
}
}
#[test]
fn a_cancelled_child_stops_running() {
let _ = Runtime::init();
let scratch = std::env::temp_dir().join(format!("atap-cancel-{}.txt", std::process::id()));
let _ = fs::remove_file(&scratch);
let script = format!(
"while true; do echo x >> {}; sleep 0.05; done",
scratch.display()
);
let handle = Runtime::task(Process::run("/bin/sh", ["-c", &script])).spawn();
thread::sleep(Duration::from_millis(500));
let before_cancel = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
assert!(
before_cancel > 0,
"the child must be writing before the cancel, or this proves nothing"
);
println!("cancelling a child that writes while it lives");
handle.clone().cancel();
let _ = handle.wait();
thread::sleep(Duration::from_millis(750));
let settled_at = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
thread::sleep(Duration::from_millis(750));
let later = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
let _ = fs::remove_file(&scratch);
assert_eq!(
settled_at, later,
"the child went on writing after its task was cancelled, \
{settled_at} bytes then {later}"
);
assert!(
handle.is_cancelled(),
"the task must settle cancelled, was {:?}",
handle.state()
);
}
#[test]
fn a_timed_out_child_stops_running() {
let _ = Runtime::init();
let scratch = std::env::temp_dir().join(format!("atap-timeout-{}.txt", std::process::id()));
let _ = fs::remove_file(&scratch);
let script = format!(
"while true; do echo x >> {}; sleep 0.05; done",
scratch.display()
);
let handle = Runtime::task(Process::run("/bin/sh", ["-c", &script]))
.timeout(Duration::from_millis(400))
.spawn();
assert_eq!(
handle.take_with_timeout(PATIENCE).map(|_| ()),
Err(RuntimeError::TimedOut)
);
let written = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
assert!(written > 0, "the child never wrote, so this proves nothing");
thread::sleep(Duration::from_millis(750));
let settled_at = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
thread::sleep(Duration::from_millis(750));
let later = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
let _ = fs::remove_file(&scratch);
assert_eq!(
settled_at, later,
"the child went on writing after its task timed out, {settled_at} bytes then {later}"
);
}
#[test]
fn a_timed_out_output_stops() {
let _ = Runtime::init();
let started = Instant::now();
let handle = Runtime::task(Process::output(
"/bin/sh",
["-c", "while true; do echo line; done"],
))
.timeout(Duration::from_millis(300))
.spawn();
assert_eq!(
handle.take_with_timeout(PATIENCE).map(|_| ()),
Err(RuntimeError::TimedOut)
);
assert!(started.elapsed() >= Duration::from_millis(300));
}
#[test]
fn a_program_that_is_not_there_reports_it() {
let _ = Runtime::init();
let found = Runtime::block(Process::run("/no/such/program", Process::NO_ARGS));
assert_eq!(
found,
Err(RuntimeError::CheckError(Some(libc::ENOENT))),
"a missing program must come back as ENOENT, got {found:?}"
);
}
#[test]
fn stdin_is_not_the_terminal() {
let _ = Runtime::init();
let handle = Runtime::task(Process::output("/bin/cat", Process::NO_ARGS)).spawn();
let found = settled(&handle, PATIENCE)
.expect("cat must reach the end of its input")
.expect("cat must run");
assert!(found.stdout().is_empty(), "there was nothing to read");
assert!(found.status().success(), "cat must finish happily");
}
#[test]
fn sigpipe_is_reset() {
let _ = Runtime::init();
let handle = Runtime::task(Process::output("/bin/sh", ["-c", "yes | head -1"])).spawn();
let found = settled(&handle, PATIENCE)
.expect("a closed pipe must kill the writer rather than spin")
.expect("sh must run");
assert_eq!(found.stdout(), b"y\n", "head takes exactly one line");
}
#[test]
fn a_zero_byte_is_refused() {
let _ = Runtime::init();
let bad_argument = Runtime::block(Process::run("/bin/echo", ["a\0b"]));
assert_eq!(
bad_argument,
Err(RuntimeError::BadArgument),
"a zero byte in an argument must be refused, got {bad_argument:?}"
);
let bad_program = Runtime::block(Process::run("/bin/ec\0ho", ["fine"]));
assert_eq!(
bad_program,
Err(RuntimeError::BadPath),
"a zero byte in the program must be refused, got {bad_program:?}"
);
}
#[test]
fn a_run_child_still_has_somewhere_to_write() {
let _ = Runtime::init();
let wrote = Runtime::block(Process::run(
"/bin/sh",
["-c", "echo run-inherits-stderr 1>&2"],
))
.expect("sh must run");
assert!(
wrote.success(),
"a run child must inherit a usable stderr, exited {:?}",
wrote.code()
);
let to_stdout = Runtime::block(Process::run("/bin/sh", ["-c", "echo run-inherits-stdout"]))
.expect("sh must run");
assert!(
to_stdout.success(),
"a run child must inherit a usable stdout, exited {:?}",
to_stdout.code()
);
}
#[test]
fn input_reaches_the_child() {
let _ = Runtime::init();
let handle =
Runtime::task(Process::output("/bin/cat", Process::NO_ARGS).input(b"hello\n".as_slice()))
.spawn();
let found = settled(&handle, PATIENCE)
.expect("cat must settle")
.expect("cat must run");
assert_eq!(
found.stdout(),
b"hello\n",
"cat gave back {:?}",
String::from_utf8_lossy(found.stdout())
);
}
#[test]
fn a_child_fed_more_than_a_pipe_holds_does_not_deadlock() {
let _ = Runtime::init();
const FLOOD: usize = 4 * 1024 * 1024;
let fed = vec![b'z'; FLOOD];
println!("feeding cat {FLOOD} bytes while reading it back");
let handle =
Runtime::task(Process::output("/bin/cat", Process::NO_ARGS).input(fed.as_slice())).spawn();
let found = settled(&handle, PATIENCE)
.expect("a flooded child must settle rather than deadlock")
.expect("cat must run");
assert_eq!(
found.stdout().len(),
FLOOD,
"cat gave back the wrong amount"
);
assert!(found.status().success(), "cat must finish happily");
}
#[test]
fn input_ends_so_the_child_sees_its_end() {
let _ = Runtime::init();
let handle =
Runtime::task(Process::output("/usr/bin/wc", ["-c"]).input(b"12345".as_slice())).spawn();
let found = settled(&handle, PATIENCE)
.expect("wc must reach the end of its input")
.expect("wc must run");
let counted = String::from_utf8_lossy(found.stdout()).trim().to_string();
assert_eq!(counted, "5", "wc counted {counted:?}");
}
#[test]
fn a_child_that_ignores_its_input_finishes() {
let _ = Runtime::init();
let fed = vec![b'z'; 4 * 1024 * 1024];
let handle =
Runtime::task(Process::output("/bin/sh", ["-c", "echo done"]).input(fed.as_slice()))
.spawn();
let found = settled(&handle, PATIENCE)
.expect("a child that ignores its input must still finish")
.expect("sh must run");
assert_eq!(found.stdout(), b"done\n", "the child ran to its own end");
assert!(found.status().success(), "and was not treated as a failure");
}
#[test]
fn a_child_that_takes_part_of_its_input_finishes() {
let _ = Runtime::init();
let fed = vec![b'z'; 4 * 1024 * 1024];
let handle =
Runtime::task(Process::output("/usr/bin/head", ["-c", "10"]).input(fed.as_slice())).spawn();
let found = settled(&handle, PATIENCE)
.expect("a child that stops reading must not hang its parent")
.expect("head must run");
assert_eq!(
found.stdout().len(),
10,
"head takes exactly what it asked for"
);
assert!(
found.status().success(),
"a child stopping early is not a failure, exited {:?}",
found.status().code()
);
}
#[test]
fn input_reaches_a_run_child() {
let _ = Runtime::init();
let found =
Runtime::block(Process::run("/usr/bin/grep", ["-q", "ping"]).input(b"ping\n".as_slice()))
.expect("grep must run");
assert!(found.success(), "grep must find what it was fed");
let missing =
Runtime::block(Process::run("/usr/bin/grep", ["-q", "ping"]).input(b"pong\n".as_slice()))
.expect("grep must run");
assert_eq!(
missing.code(),
Some(1),
"grep must not find what it was not fed"
);
}
#[test]
fn an_empty_input_is_the_same_as_none() {
let _ = Runtime::init();
let handle =
Runtime::task(Process::output("/bin/cat", Process::NO_ARGS).input(b"".as_slice())).spawn();
let found = settled(&handle, PATIENCE)
.expect("an empty input must still end")
.expect("cat must run");
assert!(found.stdout().is_empty(), "there was nothing to give it");
assert!(found.status().success(), "cat must finish happily");
}
#[test]
fn in_dir_changes_where_the_child_starts() {
let _ = Runtime::init();
let handle =
Runtime::task(Process::output("/bin/pwd", Process::NO_ARGS).in_dir("/usr")).spawn();
let found = settled(&handle, PATIENCE)
.expect("pwd must settle")
.expect("pwd must run");
let where_it_ran = String::from_utf8_lossy(found.stdout()).trim().to_string();
assert_eq!(
where_it_ran, "/usr",
"the child started in {where_it_ran:?}"
);
}
#[test]
fn a_relative_program_runs_once_in_the_new_directory() {
let _ = Runtime::init();
let scratch = std::env::temp_dir().join(format!("atap-relative-{}.txt", std::process::id()));
let _ = fs::remove_file(&scratch);
let script = format!("echo x >> {}", scratch.display());
let found = Runtime::block(Process::run("./sh", ["-c", &script]).in_dir("/bin"))
.expect("a relative program must run rather than report a phantom ENOENT");
assert!(found.success(), "the shell itself must succeed");
let wrote = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
let _ = fs::remove_file(&scratch);
assert_eq!(
wrote, 2,
"the program must run exactly once, wrote {wrote} bytes"
);
}
#[test]
fn a_directory_that_is_not_there_is_reported() {
let _ = Runtime::init();
let found = Runtime::block(Process::run("/bin/pwd", Process::NO_ARGS).in_dir("/no/such/dir"));
assert!(
found.is_err(),
"a missing directory must not be silently ignored, got {found:?}"
);
}
#[test]
fn a_relative_directory_is_refused() {
let _ = Runtime::init();
let relative = Runtime::block(Process::run("/bin/pwd", Process::NO_ARGS).in_dir("build"));
assert_eq!(
relative,
Err(RuntimeError::BadDirectory),
"a relative directory must be refused, got {relative:?}"
);
let holed = Runtime::block(Process::run("/bin/pwd", Process::NO_ARGS).in_dir("/a\0b"));
assert_eq!(
holed,
Err(RuntimeError::BadDirectory),
"a zero byte must be refused, got {holed:?}"
);
}
#[test]
fn env_puts_a_variable_in_the_child() {
let _ = Runtime::init();
let handle = Runtime::task(
Process::output("/bin/sh", ["-c", "printf %s \"$ATAP_TEST\""]).env([("ATAP_TEST", "yes")]),
)
.spawn();
let found = settled(&handle, PATIENCE)
.expect("sh must settle")
.expect("sh must run");
assert_eq!(found.stdout(), b"yes", "the variable did not arrive");
}
#[test]
fn env_leaves_the_rest_of_the_environment_alone() {
let _ = Runtime::init();
let handle = Runtime::task(
Process::output("/bin/sh", ["-c", "printf %s \"$PATH\""]).env([("ATAP_TEST", "yes")]),
)
.spawn();
let found = settled(&handle, PATIENCE)
.expect("sh must settle")
.expect("sh must run");
assert!(
!found.stdout().is_empty(),
"an overlay must not replace the whole environment"
);
}
#[test]
fn env_replaces_a_variable_rather_than_adding_it_twice() {
let _ = Runtime::init();
let handle = Runtime::task(
Process::output("/bin/sh", ["-c", "env | grep -c '^HOME='"]).env([("HOME", "/atap")]),
)
.spawn();
let found = settled(&handle, PATIENCE)
.expect("sh must settle")
.expect("sh must run");
let seen = String::from_utf8_lossy(found.stdout()).trim().to_string();
assert_eq!(seen, "1", "HOME appeared {seen} times, not once");
let value = Runtime::task(
Process::output("/bin/sh", ["-c", "printf %s \"$HOME\""]).env([("HOME", "/atap")]),
)
.spawn();
let found = settled(&value, PATIENCE)
.expect("sh must settle")
.expect("sh must run");
assert_eq!(
found.stdout(),
b"/atap",
"and the overlay's value is the one kept"
);
}
#[test]
fn env_only_gives_the_child_nothing_else() {
let _ = Runtime::init();
let handle =
Runtime::task(Process::output("/usr/bin/env", Process::NO_ARGS).env_only([("ONLY", "1")]))
.spawn();
let found = settled(&handle, PATIENCE)
.expect("env must settle")
.expect("env must run");
assert_eq!(
found.stdout(),
b"ONLY=1\n",
"the child kept more than it was given: {:?}",
String::from_utf8_lossy(found.stdout())
);
}
#[test]
fn a_bad_variable_is_refused() {
let _ = Runtime::init();
for (name, value, why) in [
("A\0B", "x", "a zero byte in the name"),
("A", "x\0y", "a zero byte in the value"),
("A=B", "x", "an equals sign in the name"),
("", "x", "an empty name"),
] {
let found =
Runtime::block(Process::run("/usr/bin/true", Process::NO_ARGS).env([(name, value)]));
assert_eq!(
found,
Err(RuntimeError::BadVariable),
"{why} must be refused, got {found:?}"
);
}
}
#[test]
fn all_three_at_once() {
let _ = Runtime::init();
let handle = Runtime::task(
Process::output("/bin/sh", ["-c", "cat; pwd; printf %s \"$V\""])
.input(b"fed\n".as_slice())
.in_dir("/usr")
.env([("V", "set")]),
)
.spawn();
let found = settled(&handle, PATIENCE)
.expect("sh must settle")
.expect("sh must run");
let said = String::from_utf8_lossy(found.stdout()).to_string();
assert_eq!(
said, "fed\n/usr\nset",
"the three settings interfered: {said:?}"
);
}
#[test]
fn a_running_child_talks_back() {
let _ = Runtime::init();
let child =
Runtime::block(Process::spawn("/bin/cat", Process::NO_ARGS)).expect("cat must start");
let input = child.stdin().expect("a fresh child's input is open");
for line in [b"one\n".as_slice(), b"two\n".as_slice()] {
Runtime::block(input.send(line)).expect("the child must take its input");
let echoed = Runtime::task(child.stdout().recv_until(b"\n", 64))
.spawn()
.take_with_timeout(PATIENCE)
.expect("the echo must come back");
assert_eq!(echoed.as_deref(), Ok(line));
}
child.close_stdin();
drop(input);
assert!(child.stdin().is_none(), "a closed input is gone");
let status = Runtime::task(child.wait())
.spawn()
.take_with_timeout(PATIENCE)
.expect("cat must end once its input does")
.expect("cat must be reaped");
assert!(status.success());
assert_eq!(
Runtime::block(child.wait()),
Ok(status),
"a second wait gives the same answer"
);
}
#[test]
fn a_running_child_keeps_its_streams_apart() {
let _ = Runtime::init();
let child = Runtime::block(Process::spawn("/bin/sh", ["-c", "echo out; echo err >&2"]))
.expect("sh must start");
assert_eq!(
Runtime::block(child.stdout().recv_to_end()),
Ok(b"out\n".to_vec())
);
assert_eq!(
Runtime::block(child.stderr().recv_to_end()),
Ok(b"err\n".to_vec())
);
assert!(Runtime::block(child.wait()).unwrap().success());
}
#[test]
fn a_running_child_can_be_killed() {
let _ = Runtime::init();
let child = Runtime::block(Process::spawn("/bin/sleep", ["60"])).expect("sleep must start");
assert!(child.id() > 0);
let waiting = Runtime::task(child.wait()).spawn();
Runtime::block(child.kill()).expect("the kill must go");
let status = waiting
.take_with_timeout(PATIENCE)
.expect("the wait must see the kill")
.expect("the child must be reaped");
assert_eq!(status.signal(), Some(libc::SIGKILL));
assert_eq!(Runtime::block(child.kill()), Err(RuntimeError::Finished));
}
#[test]
fn a_running_child_can_be_signalled() {
let _ = Runtime::init();
let child = Runtime::block(Process::spawn("/bin/sleep", ["60"])).expect("sleep must start");
Runtime::block(child.signal(SignalKind::Terminate)).expect("the signal must go");
let status = Runtime::task(child.wait())
.spawn()
.take_with_timeout(PATIENCE)
.expect("the wait must see the signal")
.expect("the child must be reaped");
assert_eq!(status.signal(), Some(libc::SIGTERM));
}
#[test]
fn a_cancelled_wait_leaves_the_child() {
let _ = Runtime::init();
let child =
Runtime::block(Process::spawn("/bin/cat", Process::NO_ARGS)).expect("cat must start");
let waiting = Runtime::task(child.wait()).spawn();
thread::sleep(Duration::from_millis(100));
waiting.clone().cancel();
assert_eq!(
waiting.join_with_timeout(PATIENCE),
Err(RuntimeError::Cancelled)
);
let input = child.stdin().unwrap();
Runtime::block(input.send(b"still here\n".as_slice())).expect("the child must still read");
assert_eq!(
Runtime::block(child.stdout().recv_until(b"\n", 64)),
Ok(b"still here\n".to_vec())
);
Runtime::block(child.kill()).unwrap();
assert!(Runtime::block(child.wait()).unwrap().signal().is_some());
}
#[test]
fn dropping_a_running_child_kills_it() {
let _ = Runtime::init();
let scratch = std::env::temp_dir().join(format!("atap-spawned-{}.txt", std::process::id()));
let _ = fs::remove_file(&scratch);
let script = format!(
"while true; do echo x >> {}; sleep 0.05; done",
scratch.display()
);
let child = Runtime::block(Process::spawn("/bin/sh", ["-c", &script])).expect("sh must start");
let copy = child.clone();
thread::sleep(Duration::from_millis(300));
drop(child);
thread::sleep(Duration::from_millis(200));
assert!(
fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0) > 0,
"the child never wrote, so this proves nothing"
);
drop(copy);
thread::sleep(Duration::from_millis(300));
let settled_at = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
thread::sleep(Duration::from_millis(750));
let later = fs::metadata(&scratch).map(|found| found.len()).unwrap_or(0);
let _ = fs::remove_file(&scratch);
assert_eq!(settled_at, later, "the child outlived its last handle");
}
#[test]
fn spawning_a_missing_program_fails() {
let _ = Runtime::init();
assert_eq!(
Runtime::block(Process::spawn("/no/such/program", Process::NO_ARGS)).map(|_| ()),
Err(RuntimeError::CheckError(Some(libc::ENOENT)))
);
}