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
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}