Skip to main content

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}