Skip to main content

lean_ctx/core/patterns/
test.rs

1pub fn compress(output: &str) -> Option<String> {
2    if let Some(r) = try_pytest(output) {
3        return Some(r);
4    }
5    if let Some(r) = try_vitest(output) {
6        return Some(r);
7    }
8    if let Some(r) = try_jest(output) {
9        return Some(r);
10    }
11    if let Some(r) = try_go_test(output) {
12        return Some(r);
13    }
14    if let Some(r) = try_rspec(output) {
15        return Some(r);
16    }
17    if let Some(r) = try_mocha(output) {
18        return Some(r);
19    }
20    None
21}
22
23fn try_pytest(output: &str) -> Option<String> {
24    if !output.contains("test session starts") && !output.contains("pytest") {
25        return None;
26    }
27
28    let mut passed = 0u32;
29    let mut failed = 0u32;
30    let mut skipped = 0u32;
31    let mut time = String::new();
32    let mut failures = Vec::new();
33
34    for line in output.lines() {
35        let trimmed = line.trim();
36        if (trimmed.contains("passed") || trimmed.contains("failed") || trimmed.contains("error"))
37            && (trimmed.starts_with('=') || trimmed.starts_with('-'))
38        {
39            for word in trimmed.split_whitespace() {
40                if let Some(n) = word.strip_suffix("passed").or_else(|| {
41                    if trimmed.contains(" passed") {
42                        word.parse::<u32>().ok().map(|_| word)
43                    } else {
44                        None
45                    }
46                }) {
47                    if let Ok(v) = n.trim().parse::<u32>() {
48                        passed = v;
49                    }
50                }
51            }
52            if let Some(pos) = trimmed.find(" passed") {
53                let before = &trimmed[..pos];
54                if let Some(num_str) = before.split_whitespace().last() {
55                    if let Ok(v) = num_str.parse::<u32>() {
56                        passed = v;
57                    }
58                }
59            }
60            if let Some(pos) = trimmed.find(" failed") {
61                let before = &trimmed[..pos];
62                if let Some(num_str) = before.split_whitespace().last() {
63                    if let Ok(v) = num_str.parse::<u32>() {
64                        failed = v;
65                    }
66                }
67            }
68            if let Some(pos) = trimmed.find(" skipped") {
69                let before = &trimmed[..pos];
70                if let Some(num_str) = before.split_whitespace().last() {
71                    if let Ok(v) = num_str.parse::<u32>() {
72                        skipped = v;
73                    }
74                }
75            }
76            if let Some(pos) = trimmed.find(" in ") {
77                time = trimmed[pos + 4..].trim_end_matches('=').trim().to_string();
78            }
79        }
80        if trimmed.starts_with("FAILED ") {
81            failures.push(
82                trimmed
83                    .strip_prefix("FAILED ")
84                    .unwrap_or(trimmed)
85                    .to_string(),
86            );
87        }
88    }
89
90    if passed == 0 && failed == 0 {
91        return None;
92    }
93
94    let mut result = format!("pytest: {passed} passed");
95    if failed > 0 {
96        result.push_str(&format!(", {failed} failed"));
97    }
98    if skipped > 0 {
99        result.push_str(&format!(", {skipped} skipped"));
100    }
101    if !time.is_empty() {
102        result.push_str(&format!(" ({time})"));
103    }
104
105    for f in failures.iter().take(5) {
106        result.push_str(&format!("\n  FAIL: {f}"));
107    }
108
109    Some(result)
110}
111
112fn try_jest(output: &str) -> Option<String> {
113    if !output.contains("Tests:") && !output.contains("Test Suites:") {
114        return None;
115    }
116
117    let mut suites_line = String::new();
118    let mut tests_line = String::new();
119    let mut time_line = String::new();
120
121    for line in output.lines() {
122        let trimmed = line.trim();
123        if trimmed.starts_with("Test Suites:") {
124            suites_line = trimmed.to_string();
125        } else if trimmed.starts_with("Tests:") {
126            tests_line = trimmed.to_string();
127        } else if trimmed.starts_with("Time:") {
128            time_line = trimmed.to_string();
129        }
130    }
131
132    if tests_line.is_empty() {
133        return None;
134    }
135
136    let mut result = String::new();
137    if !suites_line.is_empty() {
138        result.push_str(&suites_line);
139        result.push('\n');
140    }
141    result.push_str(&tests_line);
142    if !time_line.is_empty() {
143        result.push('\n');
144        result.push_str(&time_line);
145    }
146
147    Some(result)
148}
149
150fn try_go_test(output: &str) -> Option<String> {
151    if !output.contains("--- PASS") && !output.contains("--- FAIL") && !output.contains("PASS\n") {
152        return None;
153    }
154
155    let mut passed = 0u32;
156    let mut failed = 0u32;
157    let mut failures = Vec::new();
158    let mut packages = Vec::new();
159
160    for line in output.lines() {
161        let trimmed = line.trim();
162        if trimmed.starts_with("--- PASS:") {
163            passed += 1;
164        } else if trimmed.starts_with("--- FAIL:") {
165            failed += 1;
166            failures.push(
167                trimmed
168                    .strip_prefix("--- FAIL: ")
169                    .unwrap_or(trimmed)
170                    .to_string(),
171            );
172        } else if trimmed.starts_with("ok ") || trimmed.starts_with("FAIL\t") {
173            packages.push(trimmed.to_string());
174        }
175    }
176
177    if passed == 0 && failed == 0 {
178        return None;
179    }
180
181    let mut result = format!("go test: {passed} passed");
182    if failed > 0 {
183        result.push_str(&format!(", {failed} failed"));
184    }
185
186    for pkg in &packages {
187        result.push_str(&format!("\n  {pkg}"));
188    }
189
190    for f in failures.iter().take(5) {
191        result.push_str(&format!("\n  FAIL: {f}"));
192    }
193
194    Some(result)
195}
196
197fn try_vitest(output: &str) -> Option<String> {
198    if !output.contains("PASS") && !output.contains("FAIL") {
199        return None;
200    }
201    if !output.contains(" Tests ") && !output.contains("Test Files") {
202        return None;
203    }
204
205    let mut test_files_line = String::new();
206    let mut tests_line = String::new();
207    let mut duration_line = String::new();
208    let mut failures = Vec::new();
209
210    for line in output.lines() {
211        let trimmed = line.trim();
212        let plain = strip_ansi(trimmed);
213        if plain.contains("Test Files") {
214            test_files_line = plain.clone();
215        } else if plain.starts_with("Tests") && plain.contains("passed") {
216            tests_line = plain.clone();
217        } else if plain.contains("Duration") || plain.contains("Time") {
218            if plain.contains("ms") || plain.contains("s") {
219                duration_line = plain.clone();
220            }
221        } else if plain.contains("FAIL")
222            && (plain.contains(".test.") || plain.contains(".spec.") || plain.contains("_test."))
223        {
224            failures.push(plain.clone());
225        }
226    }
227
228    if tests_line.is_empty() && test_files_line.is_empty() {
229        return None;
230    }
231
232    let mut result = String::new();
233    if !test_files_line.is_empty() {
234        result.push_str(&test_files_line);
235    }
236    if !tests_line.is_empty() {
237        if !result.is_empty() {
238            result.push('\n');
239        }
240        result.push_str(&tests_line);
241    }
242    if !duration_line.is_empty() {
243        result.push('\n');
244        result.push_str(&duration_line);
245    }
246
247    for f in failures.iter().take(10) {
248        result.push_str(&format!("\n  FAIL: {f}"));
249    }
250
251    Some(result)
252}
253
254fn strip_ansi(s: &str) -> String {
255    crate::core::compressor::strip_ansi(s)
256}
257
258fn try_rspec(output: &str) -> Option<String> {
259    if !output.contains("examples") || !output.contains("failures") {
260        return None;
261    }
262
263    for line in output.lines().rev() {
264        let trimmed = line.trim();
265        if trimmed.contains("example") && trimmed.contains("failure") {
266            return Some(format!("rspec: {trimmed}"));
267        }
268    }
269
270    None
271}
272
273fn try_mocha(output: &str) -> Option<String> {
274    let has_passing = output.contains(" passing");
275    let has_failing = output.contains(" failing");
276    if !has_passing && !has_failing {
277        return None;
278    }
279
280    let mut passing = 0u32;
281    let mut failing = 0u32;
282    let mut duration = String::new();
283    let mut failures = Vec::new();
284    let mut in_failure = false;
285
286    for line in output.lines() {
287        let trimmed = line.trim();
288        if trimmed.contains(" passing") {
289            let before_passing = trimmed.split(" passing").next().unwrap_or("");
290            if let Ok(n) = before_passing.trim().parse::<u32>() {
291                passing = n;
292            }
293            if let Some(start) = trimmed.rfind('(') {
294                if let Some(end) = trimmed.rfind(')') {
295                    if start < end {
296                        duration = trimmed[start + 1..end].to_string();
297                    }
298                }
299            }
300        }
301        if trimmed.contains(" failing") {
302            let before_failing = trimmed.split(" failing").next().unwrap_or("");
303            if let Ok(n) = before_failing.trim().parse::<u32>() {
304                failing = n;
305                in_failure = true;
306            }
307        }
308        if in_failure && trimmed.starts_with(|c: char| c.is_ascii_digit()) && trimmed.contains(')')
309        {
310            if let Some((_, desc)) = trimmed.split_once(')') {
311                failures.push(desc.trim().to_string());
312            }
313        }
314    }
315
316    let mut result = format!("mocha: {passing} passed");
317    if failing > 0 {
318        result.push_str(&format!(", {failing} failed"));
319    }
320    if !duration.is_empty() {
321        result.push_str(&format!(" ({duration})"));
322    }
323
324    for f in failures.iter().take(10) {
325        result.push_str(&format!("\n  FAIL: {f}"));
326    }
327
328    Some(result)
329}
330
331#[cfg(test)]
332mod mocha_tests {
333    use super::*;
334
335    #[test]
336    fn mocha_passing_only() {
337        let output = "  3 passing (50ms)";
338        let result = try_mocha(output).expect("should match");
339        assert!(result.contains("3 passed"));
340        assert!(result.contains("50ms"));
341    }
342
343    #[test]
344    fn mocha_with_failures() {
345        let output =
346            "  2 passing (100ms)\n  1 failing\n\n  1) Array #indexOf():\n     Error: expected -1";
347        let result = try_mocha(output).expect("should match");
348        assert!(result.contains("2 passed"));
349        assert!(result.contains("1 failed"));
350        assert!(result.contains("FAIL:"));
351    }
352}