aube_codes/exit.rs
1//! Bespoke Unix exit codes per error code.
2//!
3//! Most aube errors exit with the generic [`EXIT_GENERIC`] (`1`). A
4//! curated subset — the ones a CI script or shell pipeline most often
5//! wants to branch on — gets its own exit code so callers can react
6//! without parsing stderr.
7//!
8//! Exit codes are declared inline on each [`crate::CodeMeta`] entry
9//! in [`crate::errors::ALL`]; this module just provides the lookup.
10//!
11//! The 8-bit exit-code space is lean (POSIX reserves several values
12//! 126–165 for shell signals), so codes are allocated in 10-wide
13//! ranges by category, with room to grow:
14//!
15//! | range | category |
16//! | ------ | ---------------------------------------------- |
17//! | 1 | generic / unknown error |
18//! | 2 | CLI usage error |
19//! | 10–19 | lockfile |
20//! | 20–29 | resolver |
21//! | 30–39 | tarball / store |
22//! | 40–49 | registry / network |
23//! | 50–59 | scripts / build |
24//! | 60–69 | linker |
25//! | 70–79 | manifest / workspace |
26//! | 80–89 | engine / cli surface |
27//! | 90–99 | misc / safety |
28//!
29//! Tooling consumers should branch on the *exit code* rather than the
30//! exit category, since the categories are documentation, not API.
31
32use crate::errors;
33
34/// Generic catch-all. Anything not explicitly assigned an exit code
35/// in [`crate::errors::ALL`] resolves to this exit code.
36pub const EXIT_GENERIC: i32 = 1;
37
38/// CLI usage error — bad flags, conflicting options, missing required
39/// arguments. Reserved as a convention, not currently emitted by aube
40/// itself (clap exits with this code on its own).
41pub const EXIT_CLI_USAGE: i32 = 2;
42
43/// Returns the bespoke exit code for `code`, or `None` if the code
44/// has no bespoke entry (the caller should use [`EXIT_GENERIC`]).
45///
46/// Linear-scan lookup over [`crate::errors::ALL`]. Fine for ~50
47/// entries and avoids dragging in a HashMap. The failure path is not
48/// hot.
49pub fn exit_code_for(code: &str) -> Option<i32> {
50 errors::ALL
51 .iter()
52 .find(|m| m.name == code)
53 .and_then(|m| m.exit_code)
54}
55
56#[cfg(test)]
57mod tests {
58 use super::*;
59 use std::collections::HashSet;
60
61 #[test]
62 fn exit_codes_are_unique() {
63 let mut seen = HashSet::new();
64 for meta in errors::ALL {
65 if let Some(exit) = meta.exit_code {
66 assert!(
67 seen.insert(exit),
68 "duplicate exit code {exit} (on {})",
69 meta.name
70 );
71 }
72 }
73 }
74
75 #[test]
76 fn exit_codes_are_in_valid_unix_range() {
77 // POSIX exit codes are 0–255. Reserve <10 for the special
78 // generic/usage entries; everything in `errors::ALL` should
79 // fall in [10, 125] to avoid colliding with shell signal
80 // codes (126–165 are reserved by POSIX).
81 for meta in errors::ALL {
82 if let Some(exit) = meta.exit_code {
83 assert!(
84 (10..=125).contains(&exit),
85 "exit code {exit} for {} is out of the [10, 125] range",
86 meta.name
87 );
88 }
89 }
90 }
91
92 #[test]
93 fn exit_lookup_round_trips() {
94 for meta in errors::ALL {
95 if let Some(expected) = meta.exit_code {
96 assert_eq!(
97 exit_code_for(meta.name),
98 Some(expected),
99 "round-trip failed for {}",
100 meta.name
101 );
102 }
103 }
104 }
105
106 #[test]
107 fn unknown_code_returns_none() {
108 assert_eq!(exit_code_for("ERR_AUBE_TOTALLY_MADE_UP"), None);
109 }
110}