Skip to main content

testx/
error.rs

1use std::fmt;
2use std::path::PathBuf;
3
4/// All error types that testx can produce.
5#[derive(Debug)]
6pub enum TestxError {
7    /// No test framework could be detected in the project directory.
8    NoFrameworkDetected { path: PathBuf },
9
10    /// A required test runner binary is not on PATH.
11    RunnerNotFound { runner: String },
12
13    /// Failed to spawn or execute the test command.
14    ExecutionFailed {
15        command: String,
16        source: std::io::Error,
17    },
18
19    /// Test process exceeded the configured timeout.
20    Timeout { seconds: u64 },
21
22    /// Could not parse test runner output into structured results.
23    ParseError { message: String },
24
25    /// Configuration file is invalid or contains bad values.
26    ConfigError { message: String },
27
28    /// The user specified an adapter that doesn't exist.
29    AdapterNotFound { name: String },
30
31    /// File system operation failed.
32    IoError {
33        context: String,
34        source: std::io::Error,
35    },
36
37    /// Path resolution failed.
38    PathError { message: String },
39
40    /// Watch mode error (file watcher failure).
41    WatchError { message: String },
42
43    /// Plugin loading or execution error.
44    PluginError { message: String },
45
46    /// Filter pattern is invalid.
47    FilterError { pattern: String, message: String },
48
49    /// History/database error.
50    HistoryError { message: String },
51
52    /// Coverage tool error.
53    CoverageError { message: String },
54
55    /// Multiple errors collected from parallel execution.
56    MultipleErrors { errors: Vec<TestxError> },
57}
58
59impl fmt::Display for TestxError {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match self {
62            TestxError::NoFrameworkDetected { path } => {
63                write!(
64                    f,
65                    "No test framework detected in '{}'. Try 'testx detect' to diagnose, \
66                     or 'testx list' for supported frameworks.",
67                    path.display()
68                )
69            }
70            TestxError::RunnerNotFound { runner } => {
71                write!(
72                    f,
73                    "Test runner '{}' not found. Install it and try again.",
74                    runner
75                )
76            }
77            TestxError::ExecutionFailed { command, source } => {
78                write!(f, "Failed to execute command '{}': {}", command, source)
79            }
80            TestxError::Timeout { seconds } => {
81                write!(f, "Test process timed out after {}s", seconds)
82            }
83            TestxError::ParseError { message } => {
84                write!(f, "Failed to parse test output: {}", message)
85            }
86            TestxError::ConfigError { message } => {
87                write!(f, "Configuration error: {}", message)
88            }
89            TestxError::AdapterNotFound { name } => {
90                write!(
91                    f,
92                    "Adapter '{}' not found. Run 'testx list' to see available adapters.",
93                    name
94                )
95            }
96            TestxError::IoError { context, source } => {
97                write!(f, "{}: {}", context, source)
98            }
99            TestxError::PathError { message } => {
100                write!(f, "Path error: {}", message)
101            }
102            TestxError::WatchError { message } => {
103                write!(f, "Watch error: {}", message)
104            }
105            TestxError::PluginError { message } => {
106                write!(f, "Plugin error: {}", message)
107            }
108            TestxError::FilterError { pattern, message } => {
109                write!(f, "Invalid filter pattern '{}': {}", pattern, message)
110            }
111            TestxError::HistoryError { message } => {
112                write!(f, "History error: {}", message)
113            }
114            TestxError::CoverageError { message } => {
115                write!(f, "Coverage error: {}", message)
116            }
117            TestxError::MultipleErrors { errors } => {
118                write!(f, "Multiple errors occurred:")?;
119                for (i, err) in errors.iter().enumerate() {
120                    write!(f, "\n  {}. {}", i + 1, err)?;
121                }
122                Ok(())
123            }
124        }
125    }
126}
127
128impl std::error::Error for TestxError {
129    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
130        match self {
131            TestxError::ExecutionFailed { source, .. } => Some(source),
132            TestxError::IoError { source, .. } => Some(source),
133            _ => None,
134        }
135    }
136}
137
138impl From<std::io::Error> for TestxError {
139    fn from(err: std::io::Error) -> Self {
140        TestxError::IoError {
141            context: "I/O operation failed".into(),
142            source: err,
143        }
144    }
145}
146
147/// Convenience type alias for testx results.
148pub type Result<T> = std::result::Result<T, TestxError>;
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn error_display_no_framework() {
156        let err = TestxError::NoFrameworkDetected {
157            path: PathBuf::from("/tmp/project"),
158        };
159        let msg = err.to_string();
160        assert!(msg.contains("No test framework detected"));
161        assert!(msg.contains("/tmp/project"));
162    }
163
164    #[test]
165    fn error_display_runner_not_found() {
166        let err = TestxError::RunnerNotFound {
167            runner: "cargo".into(),
168        };
169        assert!(err.to_string().contains("cargo"));
170        assert!(err.to_string().contains("not found"));
171    }
172
173    #[test]
174    fn error_display_timeout() {
175        let err = TestxError::Timeout { seconds: 30 };
176        assert!(err.to_string().contains("30s"));
177    }
178
179    #[test]
180    fn error_display_adapter_not_found() {
181        let err = TestxError::AdapterNotFound {
182            name: "haskell".into(),
183        };
184        assert!(err.to_string().contains("haskell"));
185    }
186
187    #[test]
188    fn error_display_filter_error() {
189        let err = TestxError::FilterError {
190            pattern: "[invalid".into(),
191            message: "unclosed bracket".into(),
192        };
193        let msg = err.to_string();
194        assert!(msg.contains("[invalid"));
195        assert!(msg.contains("unclosed bracket"));
196    }
197
198    #[test]
199    fn error_display_multiple_errors() {
200        let err = TestxError::MultipleErrors {
201            errors: vec![
202                TestxError::Timeout { seconds: 10 },
203                TestxError::RunnerNotFound {
204                    runner: "npm".into(),
205                },
206            ],
207        };
208        let msg = err.to_string();
209        assert!(msg.contains("Multiple errors"));
210        assert!(msg.contains("10s"));
211        assert!(msg.contains("npm"));
212    }
213
214    #[test]
215    fn error_from_io_error() {
216        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
217        let testx_err: TestxError = io_err.into();
218        assert!(testx_err.to_string().contains("file not found"));
219    }
220
221    #[test]
222    fn error_display_execution_failed() {
223        let err = TestxError::ExecutionFailed {
224            command: "cargo test".into(),
225            source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "access denied"),
226        };
227        assert!(err.to_string().contains("cargo test"));
228        assert!(err.to_string().contains("access denied"));
229    }
230
231    #[test]
232    fn error_display_config_error() {
233        let err = TestxError::ConfigError {
234            message: "invalid TOML".into(),
235        };
236        assert!(err.to_string().contains("invalid TOML"));
237    }
238
239    #[test]
240    fn error_display_parse_error() {
241        let err = TestxError::ParseError {
242            message: "unexpected token".into(),
243        };
244        assert!(err.to_string().contains("unexpected token"));
245    }
246
247    #[test]
248    fn error_display_watch_error() {
249        let err = TestxError::WatchError {
250            message: "inotify limit".into(),
251        };
252        assert!(err.to_string().contains("inotify limit"));
253    }
254
255    #[test]
256    fn error_display_plugin_error() {
257        let err = TestxError::PluginError {
258            message: "script failed".into(),
259        };
260        assert!(err.to_string().contains("script failed"));
261    }
262
263    #[test]
264    fn error_display_history_error() {
265        let err = TestxError::HistoryError {
266            message: "db locked".into(),
267        };
268        assert!(err.to_string().contains("db locked"));
269    }
270
271    #[test]
272    fn error_display_coverage_error() {
273        let err = TestxError::CoverageError {
274            message: "lcov not found".into(),
275        };
276        assert!(err.to_string().contains("lcov not found"));
277    }
278
279    #[test]
280    fn error_display_path_error() {
281        let err = TestxError::PathError {
282            message: "not absolute".into(),
283        };
284        assert!(err.to_string().contains("not absolute"));
285    }
286
287    #[test]
288    fn error_display_io_error() {
289        let err = TestxError::IoError {
290            context: "reading config".into(),
291            source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
292        };
293        let msg = err.to_string();
294        assert!(msg.contains("reading config"));
295        assert!(msg.contains("missing"));
296    }
297
298    #[test]
299    fn error_source_chain() {
300        let err = TestxError::ExecutionFailed {
301            command: "test".into(),
302            source: std::io::Error::other("boom"),
303        };
304        assert!(std::error::Error::source(&err).is_some());
305
306        let err2 = TestxError::Timeout { seconds: 5 };
307        assert!(std::error::Error::source(&err2).is_none());
308    }
309}