use std::fmt;
use crate::Signal;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
#[non_exhaustive]
pub struct ExitStatus {
exit_code: u32,
signal: Option<Signal>,
}
impl ExitStatus {
#[must_use]
pub const fn with_exit_code(exit_code: u32) -> Self {
Self {
exit_code,
signal: None,
}
}
#[must_use]
pub const fn with_signal(signal: Signal) -> Self {
Self {
exit_code: 1,
signal: Some(signal),
}
}
#[must_use]
pub const fn success(&self) -> bool {
self.signal.is_none() && self.exit_code == 0
}
#[must_use]
pub const fn exit_code(&self) -> u32 {
self.exit_code
}
#[must_use]
pub const fn signal(&self) -> Option<Signal> {
self.signal
}
#[must_use]
pub fn exit_code_posix(&self) -> i32 {
#[cfg(unix)]
if let Some(signal) = self.signal {
return 128 + signal.number();
}
self.exit_code as i32
}
pub(crate) fn from_tastty(status: tastty::ExitStatus) -> Self {
let signal = status.signal().map(|n| Signal::from_number(n as i32));
Self {
exit_code: status.exit_code(),
signal,
}
}
}
impl fmt::Display for ExitStatus {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.success() {
return f.write_str("Success");
}
if let Some(signal) = self.signal {
return match signal.name() {
Some(name) => write!(f, "Terminated by {name}"),
None => write!(f, "Terminated by signal {}", signal.number()),
};
}
write!(f, "Exited with code {}", self.exit_code)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_exit_code_passes_through() {
assert_eq!(ExitStatus::with_exit_code(0).exit_code_posix(), 0);
assert_eq!(ExitStatus::with_exit_code(42).exit_code_posix(), 42);
}
#[cfg(unix)]
#[test]
fn signal_exit_uses_unix_convention() {
assert_eq!(
ExitStatus::with_signal(Signal::INT).exit_code_posix(),
128 + 2
);
assert_eq!(
ExitStatus::with_signal(Signal::TERM).exit_code_posix(),
128 + 15
);
assert_eq!(
ExitStatus::with_signal(Signal::KILL).exit_code_posix(),
128 + 9
);
}
#[test]
fn success_distinguishes_zero_exit_from_signal_or_failure() {
assert!(ExitStatus::with_exit_code(0).success());
assert!(!ExitStatus::with_exit_code(1).success());
assert!(!ExitStatus::with_signal(Signal::INT).success());
}
#[test]
fn display_formats_each_outcome() {
assert_eq!(ExitStatus::with_exit_code(0).to_string(), "Success");
assert_eq!(
ExitStatus::with_exit_code(7).to_string(),
"Exited with code 7"
);
assert_eq!(
ExitStatus::with_signal(Signal::INT).to_string(),
"Terminated by SIGINT"
);
assert_eq!(
ExitStatus::with_signal(Signal::from_number(99)).to_string(),
"Terminated by signal 99"
);
}
#[test]
fn from_tastty_signal_number_lifts_to_canonical_signal() {
let upstream = tastty::ExitStatus::with_signal(2);
let wrapped = ExitStatus::from_tastty(upstream);
assert_eq!(wrapped.signal(), Some(Signal::INT));
assert_eq!(wrapped.exit_code(), 1);
}
#[test]
fn from_tastty_clean_exit_carries_no_signal() {
let upstream = tastty::ExitStatus::with_exit_code(0);
let wrapped = ExitStatus::from_tastty(upstream);
assert!(wrapped.success());
assert_eq!(wrapped.signal(), None);
assert_eq!(wrapped.exit_code(), 0);
}
#[test]
fn from_tastty_uncanonical_signal_number_passes_through() {
let upstream = tastty::ExitStatus::with_signal(99);
let wrapped = ExitStatus::from_tastty(upstream);
assert_eq!(wrapped.signal(), Some(Signal::from_number(99)));
assert_eq!(wrapped.exit_code(), 1);
}
}