lean_ctx/core/patterns/
test.rs1pub 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}