use shep_core::protocol::RpcErrorCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum ExitCode {
#[cfg_attr(windows, allow(dead_code))]
Success = 0,
Failure = 1,
Usage = 2,
NotFound = 3,
InvalidConfig = 4,
#[cfg_attr(windows, allow(dead_code))]
DaemonUnreachable = 5,
ProtocolMismatch = 6,
SpawnFailed = 7,
DeadlineExceeded = 8,
Internal = 9,
#[cfg_attr(windows, allow(dead_code))]
DaemonAlreadyRunning = 10,
FlockEmpty = 11,
}
impl ExitCode {
#[must_use]
pub const fn code_str(self) -> &'static str {
match self {
Self::Success => "success",
Self::Failure => "failure",
Self::Usage => "usage",
Self::NotFound => "not_found",
Self::InvalidConfig => "invalid_config",
Self::DaemonUnreachable => "daemon_unreachable",
Self::ProtocolMismatch => "protocol_mismatch",
Self::SpawnFailed => "spawn_failed",
Self::DeadlineExceeded => "deadline_exceeded",
Self::Internal => "internal",
Self::DaemonAlreadyRunning => "daemon_already_running",
Self::FlockEmpty => "flock_empty",
}
}
}
impl From<RpcErrorCode> for ExitCode {
fn from(code: RpcErrorCode) -> Self {
match code {
RpcErrorCode::NotFound => Self::NotFound,
RpcErrorCode::InvalidConfig => Self::InvalidConfig,
RpcErrorCode::SpawnFailed => Self::SpawnFailed,
RpcErrorCode::ProtocolMismatch => Self::ProtocolMismatch,
RpcErrorCode::Internal => Self::Internal,
RpcErrorCode::DeadlineExceeded => Self::DeadlineExceeded,
_ => Self::Internal,
}
}
}
impl From<&shep_client::ConnectError> for ExitCode {
fn from(err: &shep_client::ConnectError) -> Self {
use shep_client::ConnectError::{
Connect, HandshakeClosed, HandshakeTimeout, Io, ProtocolMismatch, Wire,
};
match err {
ProtocolMismatch { .. } => Self::ProtocolMismatch,
Connect { .. } | Io(_) | Wire(_) | HandshakeClosed | HandshakeTimeout { .. } => {
Self::DaemonUnreachable
}
_ => Self::Failure,
}
}
}
impl From<&shep_client::RequestError> for ExitCode {
fn from(err: &shep_client::RequestError) -> Self {
use shep_client::RequestError::{Closed, Rpc, Timeout, Wire};
match err {
Rpc(rpc) => Self::from(rpc.code),
Timeout { .. } => Self::DeadlineExceeded,
Closed => Self::DaemonUnreachable,
Wire(_) => Self::Internal,
_ => Self::Failure,
}
}
}
impl From<&shep_client::spawn::SpawnError> for ExitCode {
fn from(err: &shep_client::spawn::SpawnError) -> Self {
use shep_client::spawn::SpawnError::{Connect, DaemonExited, DeadlineExpired, Launch};
match err {
Connect(inner) => Self::from(inner),
Launch(_) | DaemonExited { .. } | DeadlineExpired { .. } => Self::DaemonUnreachable,
_ => Self::Failure,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_rpc_error_code_maps_to_a_distinct_nonzero_exit_code() {
let codes = shep_core::protocol::RpcErrorCode::ALL;
let mapped: Vec<u8> = codes.iter().map(|c| ExitCode::from(*c) as u8).collect();
assert!(
mapped.iter().all(|&c| c != 0),
"no error may map to Success"
);
let unique: std::collections::HashSet<_> = mapped.iter().collect();
assert_eq!(
unique.len(),
mapped.len(),
"distinct causes need distinct exit codes: {mapped:?}"
);
}
#[test]
fn every_exit_code_has_its_own_machine_readable_spelling() {
let all = [
ExitCode::Success,
ExitCode::Failure,
ExitCode::Usage,
ExitCode::NotFound,
ExitCode::InvalidConfig,
ExitCode::DaemonUnreachable,
ExitCode::ProtocolMismatch,
ExitCode::SpawnFailed,
ExitCode::DeadlineExceeded,
ExitCode::Internal,
ExitCode::DaemonAlreadyRunning,
ExitCode::FlockEmpty,
];
let strings: Vec<&str> = all.iter().map(|c| c.code_str()).collect();
assert!(strings.iter().all(|s| !s.is_empty()));
assert!(
strings
.iter()
.all(|s| s.chars().all(|c| c.is_ascii_lowercase() || c == '_')),
"these go on the JSON surface: {strings:?}"
);
let unique: std::collections::HashSet<_> = strings.iter().collect();
assert_eq!(
unique.len(),
strings.len(),
"duplicated spelling: {strings:?}"
);
}
#[cfg(unix)]
#[test]
fn the_already_running_exit_code_matches_the_clients_constant() {
assert_eq!(
ExitCode::DaemonAlreadyRunning as i32,
shep_client::spawn::DAEMON_ALREADY_RUNNING
);
}
}