aprender_tsp/
error.rs

1//! TSP-specific error types with actionable hints.
2//!
3//! Toyota Way Principle: *Jidoka* - Stop immediately on errors, provide clear diagnostics.
4
5use std::path::PathBuf;
6
7/// TSP-specific errors with actionable hints
8#[derive(Debug)]
9pub enum TspError {
10    /// Invalid .apr file format
11    InvalidFormat { message: String, hint: String },
12    /// Checksum verification failed
13    ChecksumMismatch { expected: u32, computed: u32 },
14    /// Instance parsing failed
15    ParseError {
16        file: PathBuf,
17        line: Option<usize>,
18        cause: String,
19    },
20    /// Invalid instance data
21    InvalidInstance { message: String },
22    /// Solver failed to find solution
23    SolverFailed { algorithm: String, reason: String },
24    /// Budget exhausted without convergence
25    BudgetExhausted { evaluations: usize, best_found: f64 },
26    /// I/O error
27    Io(std::io::Error),
28}
29
30impl std::fmt::Display for TspError {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        match self {
33            Self::InvalidFormat { message, hint } => {
34                write!(f, "Invalid .apr format: {message}\nHint: {hint}")
35            }
36            Self::ChecksumMismatch { expected, computed } => {
37                write!(
38                    f,
39                    "Model file corrupted: checksum mismatch\n\
40                     Expected: 0x{expected:08X}, Computed: 0x{computed:08X}\n\
41                     Hint: Re-train the model or restore from backup"
42                )
43            }
44            Self::ParseError { file, line, cause } => {
45                if let Some(line_num) = line {
46                    write!(
47                        f,
48                        "Parse error in {} at line {}: {}",
49                        file.display(),
50                        line_num,
51                        cause
52                    )
53                } else {
54                    write!(f, "Parse error in {}: {}", file.display(), cause)
55                }
56            }
57            Self::InvalidInstance { message } => {
58                write!(f, "Invalid TSP instance: {message}")
59            }
60            Self::SolverFailed { algorithm, reason } => {
61                write!(f, "{algorithm} solver failed: {reason}")
62            }
63            Self::BudgetExhausted {
64                evaluations,
65                best_found,
66            } => {
67                write!(
68                    f,
69                    "Budget exhausted after {evaluations} evaluations\n\
70                     Best solution found: {best_found:.2}\n\
71                     Hint: Increase --iterations or --timeout"
72                )
73            }
74            Self::Io(e) => write!(f, "I/O error: {e}"),
75        }
76    }
77}
78
79impl std::error::Error for TspError {
80    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
81        match self {
82            Self::Io(e) => Some(e),
83            _ => None,
84        }
85    }
86}
87
88impl From<std::io::Error> for TspError {
89    fn from(e: std::io::Error) -> Self {
90        Self::Io(e)
91    }
92}
93
94/// Result type alias for TSP operations
95pub type TspResult<T> = Result<T, TspError>;
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use std::error::Error;
101
102    #[test]
103    fn test_invalid_format_display() {
104        let err = TspError::InvalidFormat {
105            message: "Missing magic number".into(),
106            hint: "Ensure file starts with APR\\x00".into(),
107        };
108        let msg = err.to_string();
109        assert!(msg.contains("Invalid .apr format"));
110        assert!(msg.contains("Missing magic number"));
111        assert!(msg.contains("Hint:"));
112    }
113
114    #[test]
115    fn test_checksum_mismatch_display() {
116        let err = TspError::ChecksumMismatch {
117            expected: 0xDEAD_BEEF,
118            computed: 0xCAFE_BABE,
119        };
120        let msg = err.to_string();
121        assert!(msg.contains("checksum mismatch"));
122        assert!(msg.contains("DEADBEEF"));
123        assert!(msg.contains("CAFEBABE"));
124        assert!(msg.contains("Re-train"));
125    }
126
127    #[test]
128    fn test_parse_error_with_line() {
129        let err = TspError::ParseError {
130            file: PathBuf::from("test.tsp"),
131            line: Some(42),
132            cause: "Invalid coordinate".into(),
133        };
134        let msg = err.to_string();
135        assert!(msg.contains("test.tsp"));
136        assert!(msg.contains("line 42"));
137        assert!(msg.contains("Invalid coordinate"));
138    }
139
140    #[test]
141    fn test_parse_error_without_line() {
142        let err = TspError::ParseError {
143            file: PathBuf::from("test.tsp"),
144            line: None,
145            cause: "File not found".into(),
146        };
147        let msg = err.to_string();
148        assert!(msg.contains("test.tsp"));
149        assert!(!msg.contains("line"));
150    }
151
152    #[test]
153    fn test_invalid_instance_display() {
154        let err = TspError::InvalidInstance {
155            message: "Dimension must be positive".into(),
156        };
157        let msg = err.to_string();
158        assert!(msg.contains("Invalid TSP instance"));
159        assert!(msg.contains("Dimension must be positive"));
160    }
161
162    #[test]
163    fn test_solver_failed_display() {
164        let err = TspError::SolverFailed {
165            algorithm: "ACO".into(),
166            reason: "No feasible tour found".into(),
167        };
168        let msg = err.to_string();
169        assert!(msg.contains("ACO solver failed"));
170        assert!(msg.contains("No feasible tour"));
171    }
172
173    #[test]
174    fn test_budget_exhausted_display() {
175        let err = TspError::BudgetExhausted {
176            evaluations: 10000,
177            best_found: 12345.67,
178        };
179        let msg = err.to_string();
180        assert!(msg.contains("10000 evaluations"));
181        assert!(msg.contains("12345.67"));
182        assert!(msg.contains("--iterations"));
183    }
184
185    #[test]
186    fn test_io_error_conversion() {
187        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
188        let tsp_err: TspError = io_err.into();
189        assert!(matches!(tsp_err, TspError::Io(_)));
190        let msg = tsp_err.to_string();
191        assert!(msg.contains("I/O error"));
192    }
193
194    #[test]
195    fn test_error_source() {
196        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test");
197        let tsp_err = TspError::Io(io_err);
198        assert!(tsp_err.source().is_some());
199
200        let other_err = TspError::InvalidInstance {
201            message: "test".into(),
202        };
203        assert!(other_err.source().is_none());
204    }
205}