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