use std::sync::mpsc::RecvTimeoutError;
use std::time::{Duration, Instant};
use super::{Ended, Launcher, OutputLine, Spawn, piece_end};
#[cfg(unix)]
#[test]
fn stdin_reaches_the_child_and_its_lines_arrive_tagged() {
let how = Spawn {
stdin: Some(b"one\ntwo".to_vec()),
merge_stderr: false,
};
let process = Launcher::default().spawn("sh", ["-c", "cat; echo; echo err >&2"], how);
let mut process = process.ok();
let status = process
.as_mut()
.and_then(|process| process.wait_for(Duration::from_secs(5)).ok().flatten());
assert!(status.is_some_and(|status| status.success()));
let lines: Vec<OutputLine> = process
.as_ref()
.map(|process| process.lines().iter().collect())
.unwrap_or_default();
let out: Vec<&OutputLine> = lines
.iter()
.filter(|line| matches!(line, OutputLine::Out(_)))
.collect();
assert_eq!(
out,
[
&OutputLine::Out("one\n".to_owned()),
&OutputLine::Out("two\n".to_owned())
]
);
assert!(lines.contains(&OutputLine::Err("err\n".to_owned())));
}
#[cfg(unix)]
#[test]
fn stop_ends_a_child_with_sigterm_before_the_grace() {
use std::os::unix::process::ExitStatusExt;
let process = Launcher::default().spawn("sleep", ["30"], Spawn::default());
let mut process = process.ok();
let started = Instant::now();
let status = process
.as_mut()
.and_then(|process| process.stop(Duration::from_secs(10)).ok());
assert_eq!(
status.and_then(|status| status.signal()),
Some(libc::SIGTERM)
);
assert!(started.elapsed() < Duration::from_secs(5));
}
#[cfg(unix)]
#[test]
fn waiting_for_ever_returns_when_the_process_ends() {
let mut process = Launcher::default()
.spawn("true", [""; 0], Spawn::default())
.ok();
let status = process
.as_mut()
.and_then(|process| process.wait_for(Duration::MAX).ok().flatten());
assert!(status.is_some_and(|status| status.success()));
}
#[cfg(unix)]
#[test]
fn output_runs_a_process_to_its_end() {
let process =
Launcher::default().spawn("sh", ["-c", "echo a; echo b >&2; exit 3"], Spawn::default());
let output = process
.ok()
.and_then(|process| process.output(Some(Duration::from_secs(10))).ok());
assert_eq!(
output.map(|output| (output.stdout, output.stderr, output.status.code())),
Some((b"a\n".to_vec(), b"b\n".to_vec(), Some(3)))
);
}
#[cfg(unix)]
#[test]
fn a_sink_that_stops_ends_the_wait_and_kills_the_process() {
let how = Spawn {
merge_stderr: true,
..Spawn::default()
};
let process = Launcher::default().spawn("sh", ["-c", "echo ready; exec sleep 30"], how);
let mut process = process.ok();
let started = Instant::now();
let mut seen = Vec::new();
let ended = process.as_mut().and_then(|process| {
process
.run(None, None, |line| {
seen.push(line);
false
})
.ok()
});
assert_eq!(ended, Some(Ended::Stopped));
assert_eq!(seen, [OutputLine::Out("ready\n".to_owned())]);
assert!(started.elapsed() < Duration::from_secs(10));
let status = process
.as_mut()
.and_then(|process| process.try_wait().ok().flatten());
assert!(status.is_some_and(|status| !status.success()));
}
#[cfg(unix)]
#[test]
fn a_process_that_closes_its_output_is_waited_for_until_the_deadline() {
let process = Launcher::default().spawn(
"sh",
["-c", "exec >&- 2>&-; exec sleep 30"],
Spawn::default(),
);
let mut process = process.ok();
let closed = process
.as_ref()
.map(|process| process.lines().recv_timeout(Duration::from_secs(10)));
assert_eq!(closed, Some(Err(RecvTimeoutError::Disconnected)));
let deadline = Instant::now() + Duration::from_millis(200);
let ended = process
.as_mut()
.and_then(|process| process.run(Some(deadline), None, |_| true).ok());
assert_eq!(ended, Some(Ended::TimedOut));
assert!(Instant::now() >= deadline);
let status = process
.as_mut()
.and_then(|process| process.try_wait().ok().flatten());
assert!(status.is_some_and(|status| !status.success()));
}
#[cfg(target_os = "linux")]
#[test]
fn dropping_a_process_kills_its_group() {
let how = Spawn {
merge_stderr: true,
..Spawn::default()
};
let process = Launcher::default().spawn("sh", ["-c", "sleep 30 & echo $!; wait"], how);
let grandchild = process.as_ref().ok().and_then(|process| {
let line = process.lines().recv_timeout(Duration::from_secs(5)).ok()?;
line.text().trim().parse::<u32>().ok()
});
assert!(grandchild.is_some());
drop(process);
let alive = |pid: u32| {
std::fs::read_to_string(format!("/proc/{pid}/stat")).is_ok_and(|stat| {
!stat
.rsplit(')')
.next()
.unwrap_or_default()
.trim_start()
.starts_with('Z')
})
};
let deadline = Instant::now() + Duration::from_secs(5);
while grandchild.is_some_and(alive) && Instant::now() < deadline {
std::thread::sleep(Duration::from_millis(20));
}
assert!(!grandchild.is_some_and(alive));
}
#[test]
fn a_piece_ends_after_a_newline_or_before_a_character() {
assert_eq!(piece_end(b"one\ntwo"), Some(4));
assert_eq!(piece_end(b"no newline yet"), None);
let mut long = vec![b'a'; super::MAX_PIECE - 1];
long.extend_from_slice("é".as_bytes());
assert_eq!(piece_end(&long), Some(super::MAX_PIECE - 1));
}