1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//! Documented process exit codes.
//!
//! Mirrors
//! [`docs/contracts/exit-codes.md`](https://github.com/pulsearc-ai/ready-set/blob/main/docs/contracts/exit-codes.md)
//! exactly. Adding a variant requires a corresponding entry in that contract
//! document.
use crate::error::Error;
/// Documented process exit codes returned by `ready-set` commands.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ExitCode {
/// Success.
Ok,
/// The user's input was invalid.
UserError,
/// An I/O, permission, or environmental error.
SystemError,
/// A required external tool was not found on PATH.
DependencyMissing,
/// A command requiring a cargo workspace was invoked outside one.
NotCargoWorkspace,
/// A plugin violated the dispatcher↔plugin contract.
ContractViolation,
/// The dispatcher could not resolve the requested subcommand.
UnknownSubcommand,
/// A child process was terminated by signal `N`. The numeric exit
/// code emitted to the OS is `128 + N`, following the POSIX shell
/// convention. Only meaningful on Unix; Windows children always
/// have `ExitStatus::code() == Some(_)`.
Signaled(u8),
}
impl ExitCode {
/// Return the numeric exit code as a `u8`.
///
/// `Signaled(n)` returns `128 + n`, saturating at `255` for
/// hypothetical signal numbers that would overflow.
#[must_use]
pub const fn as_u8(self) -> u8 {
match self {
Self::Ok => 0,
Self::UserError => 1,
Self::SystemError => 2,
Self::DependencyMissing => 3,
Self::NotCargoWorkspace => 4,
Self::ContractViolation => 5,
Self::UnknownSubcommand => 127,
Self::Signaled(n) => 128_u8.saturating_add(n),
}
}
}
impl From<ExitCode> for std::process::ExitCode {
fn from(value: ExitCode) -> Self {
Self::from(value.as_u8())
}
}
impl From<&Error> for ExitCode {
fn from(value: &Error) -> Self {
match value {
Error::TomlParse(_) | Error::JsonParse(_) => Self::UserError,
Error::MissingDependency { .. } => Self::DependencyMissing,
Error::ContractViolation(_) => Self::ContractViolation,
// `Error` is `#[non_exhaustive]`; the wildcard catches both the
// current `Io`/`Other` variants and any added in future minor
// releases.
_ => Self::SystemError,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numeric_values_match_contract() {
assert_eq!(ExitCode::Ok.as_u8(), 0);
assert_eq!(ExitCode::UserError.as_u8(), 1);
assert_eq!(ExitCode::SystemError.as_u8(), 2);
assert_eq!(ExitCode::DependencyMissing.as_u8(), 3);
assert_eq!(ExitCode::NotCargoWorkspace.as_u8(), 4);
assert_eq!(ExitCode::ContractViolation.as_u8(), 5);
assert_eq!(ExitCode::UnknownSubcommand.as_u8(), 127);
assert_eq!(ExitCode::Signaled(0).as_u8(), 128);
assert_eq!(ExitCode::Signaled(2).as_u8(), 130); // SIGINT
assert_eq!(ExitCode::Signaled(15).as_u8(), 143); // SIGTERM
assert_eq!(ExitCode::Signaled(255).as_u8(), 255); // saturates
}
#[test]
fn maps_errors_to_codes() {
let io = Error::Io(std::io::Error::other("nope"));
assert_eq!(ExitCode::from(&io), ExitCode::SystemError);
let dep = Error::MissingDependency {
name: "git".into(),
hint: None,
};
assert_eq!(ExitCode::from(&dep), ExitCode::DependencyMissing);
let contract = Error::contract("bad");
assert_eq!(ExitCode::from(&contract), ExitCode::ContractViolation);
let toml = Error::TomlParse("oops".into());
assert_eq!(ExitCode::from(&toml), ExitCode::UserError);
}
}