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