1use std::fmt;
2use std::path::PathBuf;
3
4#[derive(Debug)]
6pub enum TestxError {
7 NoFrameworkDetected { path: PathBuf },
9
10 RunnerNotFound { runner: String },
12
13 ExecutionFailed {
15 command: String,
16 source: std::io::Error,
17 },
18
19 Timeout { seconds: u64 },
21
22 ParseError { message: String },
24
25 ConfigError { message: String },
27
28 AdapterNotFound { name: String },
30
31 IoError {
33 context: String,
34 source: std::io::Error,
35 },
36
37 PathError { message: String },
39
40 WatchError { message: String },
42
43 PluginError { message: String },
45
46 FilterError { pattern: String, message: String },
48
49 HistoryError { message: String },
51
52 CoverageError { message: String },
54
55 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
147pub 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}