use std::io::{self, Read as _};
use std::ops::{Deref, DerefMut};
use std::process::{Child, Command, Output};
pub(crate) struct Reaped {
child: Child,
}
impl Reaped {
pub(crate) fn spawn(command: &mut Command) -> io::Result<Self> {
Ok(Self {
child: command.spawn()?,
})
}
pub(crate) fn kill_and_wait(&mut self) -> io::Result<()> {
if let Err(error) = self.child.kill()
&& error.kind() != io::ErrorKind::InvalidInput
{
return Err(error);
}
self.child.wait()?;
Ok(())
}
pub(crate) fn wait_with_output(&mut self) -> io::Result<Output> {
drop(self.child.stdin.take());
let stdout_pipe = self.child.stdout.take();
let stderr_pipe = self.child.stderr.take();
let drain = stdout_pipe.map(|mut pipe| {
std::thread::spawn(move || {
let mut buffer = Vec::new();
pipe.read_to_end(&mut buffer).map(|_| buffer)
})
});
let mut stderr = Vec::new();
if let Some(mut pipe) = stderr_pipe {
pipe.read_to_end(&mut stderr)?;
}
let stdout = match drain {
Some(handle) => handle
.join()
.map_err(|_| io::Error::other("the stdout drain thread panicked"))??,
None => Vec::new(),
};
let status = self.child.wait()?;
Ok(Output {
status,
stdout,
stderr,
})
}
}
impl Deref for Reaped {
type Target = Child;
fn deref(&self) -> &Self::Target {
&self.child
}
}
impl DerefMut for Reaped {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.child
}
}
impl Drop for Reaped {
fn drop(&mut self) {
let pid = self.child.id();
if let Err(error) = self.kill_and_wait() {
eprintln!(
"test teardown could not reap child process {pid}: {error} — it may still be \
running"
);
}
}
}
#[cfg(test)]
mod reaping {
use super::Reaped;
use std::io;
use std::process::{Command, Stdio};
#[test]
fn a_running_child_is_killed_and_reaped_and_the_teardown_is_idempotent() -> io::Result<()> {
let mut guard = Reaped::spawn(Command::new("/bin/sh").args(["-c", "sleep 300"]))?;
assert!(
guard.try_wait()?.is_none(),
"a freshly spawned `sleep 300` must still be running"
);
guard.kill_and_wait()?;
let Some(status) = guard.try_wait()? else {
return Err(io::Error::other(
"a reaped child must report an exit status",
));
};
assert!(
!status.success(),
"a killed child must not report a successful exit: {status}"
);
guard.kill_and_wait()
}
#[test]
fn a_child_that_already_exited_is_not_a_teardown_failure() -> io::Result<()> {
let mut guard = Reaped::spawn(Command::new("/bin/sh").args(["-c", "exit 0"]))?;
let status = guard.wait()?;
assert!(status.success(), "`exit 0` must exit successfully");
guard.kill_and_wait()
}
#[test]
fn the_drain_collects_both_streams_and_the_status() -> io::Result<()> {
let mut guard = Reaped::spawn(
Command::new("/bin/sh")
.args(["-c", "echo to-stdout; echo to-stderr >&2; exit 3"])
.stdout(Stdio::piped())
.stderr(Stdio::piped()),
)?;
let output = guard.wait_with_output()?;
assert_eq!(
output.status.code(),
Some(3),
"the drain must carry the child's own exit code"
);
assert_eq!(String::from_utf8_lossy(&output.stdout).trim(), "to-stdout");
assert_eq!(String::from_utf8_lossy(&output.stderr).trim(), "to-stderr");
Ok(())
}
}