use std::process::ExitStatus;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum Ending {
Exited(i32),
Signalled(i32),
Unstated,
}
impl Ending {
#[must_use]
pub(super) fn of(status: ExitStatus) -> Self {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt as _;
match (status.code(), status.signal()) {
(Some(code), _) => Self::Exited(code),
(None, Some(signal)) => Self::Signalled(signal),
(None, None) => Self::Unstated,
}
}
#[cfg(not(unix))]
{
match status.code() {
Some(code) => Self::Exited(code),
None => Self::Unstated,
}
}
}
#[must_use]
pub(super) const fn succeeded(self) -> bool {
matches!(self, Self::Exited(0))
}
#[must_use]
pub(super) const fn reported_code(self) -> Option<i32> {
match self {
Self::Exited(code) => Some(code),
Self::Signalled(signal) => Some(128 + signal),
Self::Unstated => None,
}
}
#[must_use]
pub(super) fn described(self) -> String {
match self {
Self::Exited(code) => format!("exited {code}"),
Self::Signalled(signal) => match signal_name(signal) {
Some(name) => format!("was ended by signal {signal} ({name}) rather than exiting"),
None => format!("was ended by signal {signal} rather than exiting"),
},
Self::Unstated => "ended without saying whether it exited or was signalled".to_owned(),
}
}
}
#[cfg(unix)]
fn signal_name(signal: i32) -> Option<&'static str> {
nix::sys::signal::Signal::try_from(signal)
.ok()
.map(nix::sys::signal::Signal::as_str)
}
#[cfg(not(unix))]
const fn signal_name(_signal: i32) -> Option<&'static str> {
None
}
#[cfg(test)]
mod tests {
use super::Ending;
#[test]
fn an_exit_reports_its_own_code_and_says_it_exited() {
assert_eq!(Ending::Exited(0).reported_code(), Some(0));
assert!(Ending::Exited(0).succeeded());
assert_eq!(Ending::Exited(3).reported_code(), Some(3));
assert!(!Ending::Exited(3).succeeded());
assert_eq!(Ending::Exited(3).described(), "exited 3");
}
#[test]
fn two_different_signals_are_two_different_reports() {
let segfault = Ending::Signalled(11);
let killed = Ending::Signalled(9);
assert_ne!(segfault.reported_code(), killed.reported_code());
assert_ne!(segfault.described(), killed.described());
assert_eq!(killed.reported_code(), Some(137));
assert_eq!(segfault.reported_code(), Some(139));
assert!(!segfault.succeeded());
assert!(
segfault.described().contains("signal 11"),
"{}",
segfault.described()
);
}
#[cfg(unix)]
#[test]
fn a_signal_is_named_in_words_beside_its_number() {
assert!(
Ending::Signalled(11).described().contains("SIGSEGV"),
"{}",
Ending::Signalled(11).described()
);
assert!(
Ending::Signalled(9).described().contains("SIGKILL"),
"{}",
Ending::Signalled(9).described()
);
}
#[test]
fn an_unstated_ending_reports_no_code() {
assert_eq!(Ending::Unstated.reported_code(), None);
assert!(!Ending::Unstated.succeeded());
}
#[tokio::test]
async fn a_real_process_that_exits_is_read_as_an_exit() -> Result<(), Box<dyn std::error::Error>>
{
let status = tokio::process::Command::new("sh")
.arg("-c")
.arg("exit 7")
.status()
.await?;
assert_eq!(Ending::of(status), Ending::Exited(7));
Ok(())
}
}