Skip to main content

apr_cli/
error.rs

1//! Error types for apr-cli
2//!
3//! Toyota Way: Jidoka - Stop and highlight problems immediately.
4
5use std::path::PathBuf;
6use std::process::ExitCode;
7use thiserror::Error;
8
9/// Result type alias for CLI operations
10pub type Result<T> = std::result::Result<T, CliError>;
11
12/// CLI error types
13#[derive(Error, Debug)]
14pub enum CliError {
15    /// File not found
16    #[error("File not found: {0}")]
17    FileNotFound(PathBuf),
18
19    /// Not a file (e.g., directory)
20    #[error("Not a file: {0}")]
21    NotAFile(PathBuf),
22
23    /// Invalid APR format
24    #[error("Invalid APR format: {0}")]
25    InvalidFormat(String),
26
27    /// IO error
28    #[error("IO error: {0}")]
29    Io(#[from] std::io::Error),
30
31    /// Validation failed
32    #[error("Validation failed: {0}")]
33    ValidationFailed(String),
34
35    /// Aprender error
36    #[error("Aprender error: {0}")]
37    Aprender(String),
38
39    /// Model loading failed (used with inference feature)
40    #[error("Model load failed: {0}")]
41    #[allow(dead_code)]
42    ModelLoadFailed(String),
43
44    /// Inference failed (used with inference feature)
45    #[error("Inference failed: {0}")]
46    #[allow(dead_code)]
47    InferenceFailed(String),
48
49    /// Feature disabled (used when optional features are not compiled)
50    #[error("Feature not enabled: {0}")]
51    #[allow(dead_code)]
52    FeatureDisabled(String),
53
54    /// Network error
55    #[error("Network error: {0}")]
56    NetworkError(String),
57}
58
59impl CliError {
60    /// Get exit code for this error
61    pub fn exit_code(&self) -> ExitCode {
62        match self {
63            Self::FileNotFound(_) | Self::NotAFile(_) => ExitCode::from(3),
64            Self::InvalidFormat(_) => ExitCode::from(4),
65            Self::Io(_) => ExitCode::from(7),
66            Self::ValidationFailed(_) => ExitCode::from(5),
67            Self::Aprender(_) => ExitCode::from(1),
68            Self::ModelLoadFailed(_) => ExitCode::from(6),
69            Self::InferenceFailed(_) => ExitCode::from(8),
70            Self::FeatureDisabled(_) => ExitCode::from(9),
71            Self::NetworkError(_) => ExitCode::from(10),
72        }
73    }
74}
75
76impl From<aprender::error::AprenderError> for CliError {
77    fn from(e: aprender::error::AprenderError) -> Self {
78        Self::Aprender(e.to_string())
79    }
80}
81
82#[cfg(test)]
83mod tests {
84    use super::*;
85    use std::path::PathBuf;
86
87    // ==================== Exit Code Tests ====================
88
89    #[test]
90    fn test_file_not_found_exit_code() {
91        let err = CliError::FileNotFound(PathBuf::from("/test"));
92        assert_eq!(err.exit_code(), ExitCode::from(3));
93    }
94
95    #[test]
96    fn test_not_a_file_exit_code() {
97        let err = CliError::NotAFile(PathBuf::from("/test"));
98        assert_eq!(err.exit_code(), ExitCode::from(3));
99    }
100
101    #[test]
102    fn test_invalid_format_exit_code() {
103        let err = CliError::InvalidFormat("bad".to_string());
104        assert_eq!(err.exit_code(), ExitCode::from(4));
105    }
106
107    #[test]
108    fn test_io_error_exit_code() {
109        let err = CliError::Io(std::io::Error::new(std::io::ErrorKind::Other, "test"));
110        assert_eq!(err.exit_code(), ExitCode::from(7));
111    }
112
113    #[test]
114    fn test_validation_failed_exit_code() {
115        let err = CliError::ValidationFailed("test".to_string());
116        assert_eq!(err.exit_code(), ExitCode::from(5));
117    }
118
119    #[test]
120    fn test_aprender_error_exit_code() {
121        let err = CliError::Aprender("test".to_string());
122        assert_eq!(err.exit_code(), ExitCode::from(1));
123    }
124
125    #[test]
126    fn test_model_load_failed_exit_code() {
127        let err = CliError::ModelLoadFailed("test".to_string());
128        assert_eq!(err.exit_code(), ExitCode::from(6));
129    }
130
131    #[test]
132    fn test_inference_failed_exit_code() {
133        let err = CliError::InferenceFailed("test".to_string());
134        assert_eq!(err.exit_code(), ExitCode::from(8));
135    }
136
137    #[test]
138    fn test_feature_disabled_exit_code() {
139        let err = CliError::FeatureDisabled("test".to_string());
140        assert_eq!(err.exit_code(), ExitCode::from(9));
141    }
142
143    #[test]
144    fn test_network_error_exit_code() {
145        let err = CliError::NetworkError("test".to_string());
146        assert_eq!(err.exit_code(), ExitCode::from(10));
147    }
148
149    // ==================== Display Tests ====================
150
151    #[test]
152    fn test_file_not_found_display() {
153        let err = CliError::FileNotFound(PathBuf::from("/model.apr"));
154        assert_eq!(err.to_string(), "File not found: /model.apr");
155    }
156
157    #[test]
158    fn test_not_a_file_display() {
159        let err = CliError::NotAFile(PathBuf::from("/dir"));
160        assert_eq!(err.to_string(), "Not a file: /dir");
161    }
162
163    #[test]
164    fn test_invalid_format_display() {
165        let err = CliError::InvalidFormat("bad magic".to_string());
166        assert_eq!(err.to_string(), "Invalid APR format: bad magic");
167    }
168
169    #[test]
170    fn test_validation_failed_display() {
171        let err = CliError::ValidationFailed("missing field".to_string());
172        assert_eq!(err.to_string(), "Validation failed: missing field");
173    }
174
175    #[test]
176    fn test_aprender_error_display() {
177        let err = CliError::Aprender("internal".to_string());
178        assert_eq!(err.to_string(), "Aprender error: internal");
179    }
180
181    #[test]
182    fn test_model_load_failed_display() {
183        let err = CliError::ModelLoadFailed("corrupt".to_string());
184        assert_eq!(err.to_string(), "Model load failed: corrupt");
185    }
186
187    #[test]
188    fn test_inference_failed_display() {
189        let err = CliError::InferenceFailed("OOM".to_string());
190        assert_eq!(err.to_string(), "Inference failed: OOM");
191    }
192
193    #[test]
194    fn test_feature_disabled_display() {
195        let err = CliError::FeatureDisabled("cuda".to_string());
196        assert_eq!(err.to_string(), "Feature not enabled: cuda");
197    }
198
199    #[test]
200    fn test_network_error_display() {
201        let err = CliError::NetworkError("timeout".to_string());
202        assert_eq!(err.to_string(), "Network error: timeout");
203    }
204
205    // ==================== Conversion Tests ====================
206
207    #[test]
208    fn test_io_error_conversion() {
209        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
210        let cli_err: CliError = io_err.into();
211        assert!(cli_err.to_string().contains("file missing"));
212        assert_eq!(cli_err.exit_code(), ExitCode::from(7));
213    }
214
215    #[test]
216    fn test_debug_impl() {
217        let err = CliError::FileNotFound(PathBuf::from("/test"));
218        let debug = format!("{:?}", err);
219        assert!(debug.contains("FileNotFound"));
220    }
221
222    // ==================== Result Type Alias ====================
223
224    #[test]
225    fn test_result_type_ok() {
226        let result: Result<i32> = Ok(42);
227        assert_eq!(result.unwrap(), 42);
228    }
229
230    #[test]
231    fn test_result_type_err() {
232        let result: Result<i32> = Err(CliError::InvalidFormat("test".to_string()));
233        assert!(result.is_err());
234    }
235
236    // ==================== Exit Code Uniqueness ====================
237
238    #[test]
239    fn test_all_exit_codes_are_distinct_per_category() {
240        // Verify exit codes map to distinct categories
241        let codes = vec![
242            (
243                CliError::FileNotFound(PathBuf::from("a")).exit_code(),
244                "file",
245            ),
246            (
247                CliError::InvalidFormat("a".to_string()).exit_code(),
248                "format",
249            ),
250            (
251                CliError::Io(std::io::Error::new(std::io::ErrorKind::Other, "")).exit_code(),
252                "io",
253            ),
254            (
255                CliError::ValidationFailed("a".to_string()).exit_code(),
256                "validation",
257            ),
258            (CliError::Aprender("a".to_string()).exit_code(), "aprender"),
259            (
260                CliError::ModelLoadFailed("a".to_string()).exit_code(),
261                "model_load",
262            ),
263            (
264                CliError::InferenceFailed("a".to_string()).exit_code(),
265                "inference",
266            ),
267            (
268                CliError::FeatureDisabled("a".to_string()).exit_code(),
269                "feature",
270            ),
271            (
272                CliError::NetworkError("a".to_string()).exit_code(),
273                "network",
274            ),
275        ];
276        // FileNotFound and NotAFile intentionally share exit code 3
277        assert_eq!(codes[0].0, ExitCode::from(3));
278    }
279}