1use std::path::PathBuf;
6
7#[derive(Debug)]
9pub enum TspError {
10 InvalidFormat { message: String, hint: String },
12 ChecksumMismatch { expected: u32, computed: u32 },
14 ParseError {
16 file: PathBuf,
17 line: Option<usize>,
18 cause: String,
19 },
20 InvalidInstance { message: String },
22 SolverFailed { algorithm: String, reason: String },
24 BudgetExhausted { evaluations: usize, best_found: f64 },
26 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
94pub 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}