1use std::sync::LazyLock;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum FailureCategory {
11 TypeError,
13 SyntaxError,
15 TestFail,
17 LintFail,
19 BuildFail,
21 PermissionDenied,
23 Timeout,
25 NotFound,
27 RuntimeError,
29 Unknown,
31}
32
33impl FailureCategory {
34 pub fn all() -> &'static [FailureCategory] {
36 &[
37 Self::TypeError,
38 Self::SyntaxError,
39 Self::TestFail,
40 Self::LintFail,
41 Self::BuildFail,
42 Self::PermissionDenied,
43 Self::Timeout,
44 Self::NotFound,
45 Self::RuntimeError,
46 Self::Unknown,
47 ]
48 }
49
50 pub fn label(&self) -> &'static str {
52 match self {
53 Self::TypeError => "type_error",
54 Self::SyntaxError => "syntax_error",
55 Self::TestFail => "test_fail",
56 Self::LintFail => "lint_fail",
57 Self::BuildFail => "build_fail",
58 Self::PermissionDenied => "permission_denied",
59 Self::Timeout => "timeout",
60 Self::NotFound => "not_found",
61 Self::RuntimeError => "runtime_error",
62 Self::Unknown => "unknown",
63 }
64 }
65}
66
67impl std::str::FromStr for FailureCategory {
68 type Err = ();
69 fn from_str(s: &str) -> Result<Self, Self::Err> {
70 match s {
71 "type_error" => Ok(Self::TypeError),
72 "syntax_error" => Ok(Self::SyntaxError),
73 "test_fail" => Ok(Self::TestFail),
74 "lint_fail" => Ok(Self::LintFail),
75 "build_fail" => Ok(Self::BuildFail),
76 "permission_denied" => Ok(Self::PermissionDenied),
77 "timeout" => Ok(Self::Timeout),
78 "not_found" => Ok(Self::NotFound),
79 "runtime_error" => Ok(Self::RuntimeError),
80 "unknown" => Ok(Self::Unknown),
81 _ => Err(()),
82 }
83 }
84}
85
86struct ClassificationRule {
87 category: FailureCategory,
88 pattern: regex::Regex,
89}
90
91static RULES: LazyLock<Vec<ClassificationRule>> = LazyLock::new(|| {
92 let raw: &[(&str, FailureCategory)] = &[
93 (r"(?i)error\[E\d{4}\]", FailureCategory::LintFail),
95 (
97 r"(?i)cannot find (value|function|type|module|struct|field|method)",
98 FailureCategory::TypeError,
99 ),
100 (r"(?i)mismatched types?", FailureCategory::TypeError),
101 (r"(?i)type mismatch", FailureCategory::TypeError),
102 (r"(?i)\bundefined\b", FailureCategory::TypeError),
103 (
104 r"(?i)is not (a |an )?(function|defined|iterable)",
105 FailureCategory::TypeError,
106 ),
107 (r"(?i)expected .+, found", FailureCategory::TypeError),
108 (r"(?i)no method named", FailureCategory::TypeError),
109 (
110 r"(?i)no (associated |)item named",
111 FailureCategory::TypeError,
112 ),
113 (r"(?i)borrow of moved value", FailureCategory::TypeError),
114 (r"(?i)use of moved value", FailureCategory::TypeError),
115 (r"(?i)cannot borrow", FailureCategory::TypeError),
116 (r"(?i)syntax error", FailureCategory::SyntaxError),
118 (r"(?i)unexpected token", FailureCategory::SyntaxError),
119 (
120 r"(?i)unexpected end of (file|input)",
121 FailureCategory::SyntaxError,
122 ),
123 (r"(?i)parse error", FailureCategory::SyntaxError),
124 (r"(?i)missing ';' or '}'", FailureCategory::SyntaxError),
125 (r"(?i)expected .+ but found", FailureCategory::SyntaxError),
126 (r"(?i)unclosed delimiter", FailureCategory::SyntaxError),
127 (r"(?i)test .* failed", FailureCategory::TestFail),
129 (r"(?i)assertion.*failed", FailureCategory::TestFail),
130 (r"(?i)panic!.*at", FailureCategory::TestFail),
131 (r"(?i)test result: FAILED", FailureCategory::TestFail),
132 (r"(?i)1 failed", FailureCategory::TestFail),
133 (r"(?i)\bFAIL\b", FailureCategory::TestFail),
134 (r"(?i)clippy::", FailureCategory::LintFail),
136 (r"(?i)warning: .*denied", FailureCategory::LintFail),
137 (
138 r"(?i)unused (import|variable|function)",
139 FailureCategory::LintFail,
140 ),
141 (r"(?i)compilation failed", FailureCategory::BuildFail),
143 (r"(?i)build failed", FailureCategory::BuildFail),
144 (r"(?i)cargo build.*failed", FailureCategory::BuildFail),
145 (r"(?i)could not compile", FailureCategory::BuildFail),
146 (r"(?i)linking.*failed", FailureCategory::BuildFail),
147 (r"(?i)fatal error:", FailureCategory::BuildFail),
148 (r"(?i)permission denied", FailureCategory::PermissionDenied),
150 (r"(?i)access denied", FailureCategory::PermissionDenied),
151 (r"(?i)EACCES", FailureCategory::PermissionDenied),
152 (r"(?i)EPERM", FailureCategory::PermissionDenied),
153 (
154 r"(?i)operation not permitted",
155 FailureCategory::PermissionDenied,
156 ),
157 (r"(?i)timed? ?out", FailureCategory::Timeout),
159 (r"(?i)deadline exceeded", FailureCategory::Timeout),
160 (r"(?i)ETIMEDOUT", FailureCategory::Timeout),
161 (r"(?i)not found", FailureCategory::NotFound),
163 (r"(?i)ENOENT", FailureCategory::NotFound),
164 (r"(?i)no such file", FailureCategory::NotFound),
165 (r"(?i)404", FailureCategory::NotFound),
166 (r"(?i)runtime error", FailureCategory::RuntimeError),
168 (r"(?i)segmentation fault", FailureCategory::RuntimeError),
169 (r"(?i)stack overflow", FailureCategory::RuntimeError),
170 (r"(?i)out of memory", FailureCategory::RuntimeError),
171 (r"(?i)OOM", FailureCategory::RuntimeError),
172 (r"(?i)panic!?", FailureCategory::RuntimeError),
173 ];
174
175 raw.iter()
176 .map(|(pattern, category)| ClassificationRule {
177 category: *category,
178 pattern: regex::Regex::new(pattern).expect("invalid classification regex"),
179 })
180 .collect()
181});
182
183pub fn classify_failure(error_message: &str) -> FailureCategory {
191 if let Some(rest) = error_message.strip_prefix("error[")
193 && let Some(bracket_end) = rest.find(']')
194 {
195 let code = &rest[..bracket_end];
196 if code.len() == 5
197 && code.as_bytes()[0] == b'E'
198 && code[1..].bytes().all(|b| b.is_ascii_digit())
199 {
200 return FailureCategory::LintFail;
201 }
202 }
203 for rule in RULES.iter() {
204 if rule.pattern.is_match(error_message) {
205 return rule.category;
206 }
207 }
208 FailureCategory::Unknown
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn type_error_undefined() {
217 assert_eq!(
218 classify_failure("Cannot find value `foo`"),
219 FailureCategory::TypeError
220 );
221 }
222
223 #[test]
224 fn type_error_mismatched() {
225 assert_eq!(
226 classify_failure("mismatched types: expected `i32`, found `String`"),
227 FailureCategory::TypeError
228 );
229 }
230
231 #[test]
232 fn syntax_error() {
233 assert_eq!(
234 classify_failure("syntax error: unexpected token `}`"),
235 FailureCategory::SyntaxError
236 );
237 }
238
239 #[test]
240 fn test_fail() {
241 assert_eq!(
242 classify_failure("test test_insert failed"),
243 FailureCategory::TestFail
244 );
245 }
246
247 #[test]
248 fn test_assertion() {
249 assert_eq!(
250 classify_failure("assertion `left == right` failed"),
251 FailureCategory::TestFail
252 );
253 }
254
255 #[test]
256 fn lint_fail() {
257 assert_eq!(
258 classify_failure("error[E0412]: cannot find type"),
259 FailureCategory::LintFail
260 );
261 }
262
263 #[test]
264 fn build_fail() {
265 assert_eq!(
266 classify_failure("could not compile `my-crate`"),
267 FailureCategory::BuildFail
268 );
269 }
270
271 #[test]
272 fn permission_denied() {
273 assert_eq!(
274 classify_failure("Permission denied (os error 13)"),
275 FailureCategory::PermissionDenied
276 );
277 }
278
279 #[test]
280 fn timeout() {
281 assert_eq!(
282 classify_failure("request timed out after 30s"),
283 FailureCategory::Timeout
284 );
285 }
286
287 #[test]
288 fn not_found() {
289 assert_eq!(
290 classify_failure("No such file or directory"),
291 FailureCategory::NotFound
292 );
293 }
294
295 #[test]
296 fn runtime_panic() {
297 assert_eq!(
298 classify_failure("thread 'main' panicked at 'overflow'"),
299 FailureCategory::RuntimeError
300 );
301 }
302
303 #[test]
304 fn unknown_for_gibberish() {
305 assert_eq!(
306 classify_failure("everything is fine"),
307 FailureCategory::Unknown
308 );
309 }
310
311 #[test]
312 fn category_labels() {
313 assert_eq!(FailureCategory::TypeError.label(), "type_error");
314 assert_eq!(FailureCategory::Unknown.label(), "unknown");
315 }
316
317 #[test]
318 fn all_categories_count() {
319 assert_eq!(FailureCategory::all().len(), 10);
320 }
321
322 #[test]
323 fn from_str_roundtrip() {
324 use std::str::FromStr;
325 for cat in FailureCategory::all() {
326 let label = cat.label();
327 let back = FailureCategory::from_str(label).unwrap();
328 assert_eq!(back, *cat, "roundtrip failed for {label}");
329 }
330 assert!(FailureCategory::from_str("not_a_real_category").is_err());
331 }
332}