lean_ctx/core/patterns/
playwright.rs1macro_rules! static_regex {
2 ($pattern:expr) => {{
3 static RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
4 RE.get_or_init(|| {
5 regex::Regex::new($pattern).expect(concat!("BUG: invalid static regex: ", $pattern))
6 })
7 }};
8}
9
10fn pw_failed_re() -> &'static regex::Regex {
11 static_regex!(r"^\s+\d+\)\s+(.+)$")
12}
13
14pub fn compress(command: &str, output: &str) -> Option<String> {
15 if command.contains("cypress") {
16 return Some(compress_cypress(output));
17 }
18 Some(compress_playwright(output))
19}
20
21fn compress_playwright(output: &str) -> String {
22 let trimmed = output.trim();
23 if trimmed.is_empty() {
24 return "ok".to_string();
25 }
26
27 let mut passed = 0u32;
28 let mut failed = 0u32;
29 let mut skipped = 0u32;
30 let mut failed_names = Vec::new();
31 let mut duration = String::new();
32
33 for line in trimmed.lines() {
34 let l = line.trim().to_lowercase();
35 if l.contains("passed") {
36 if let Some(n) = extract_number(&l, "passed") {
37 passed = n;
38 }
39 }
40 if l.contains("failed") {
41 if let Some(n) = extract_number(&l, "failed") {
42 failed = n;
43 }
44 }
45 if l.contains("skipped") {
46 if let Some(n) = extract_number(&l, "skipped") {
47 skipped = n;
48 }
49 }
50 if let Some(caps) = pw_failed_re().captures(line) {
51 failed_names.push(caps[1].trim().to_string());
52 }
53 if l.contains("finished in") || l.contains("duration") {
54 duration = line.trim().to_string();
55 }
56 }
57
58 let total = passed + failed + skipped;
59 if total == 0 {
60 return compact_output(trimmed, 10);
61 }
62
63 let mut parts = Vec::new();
64 parts.push(format!(
65 "{total} tests: {passed} passed, {failed} failed, {skipped} skipped"
66 ));
67
68 if !failed_names.is_empty() {
69 parts.push("failed:".to_string());
70 for name in failed_names.iter().take(10) {
71 parts.push(format!(" {name}"));
72 }
73 if failed_names.len() > 10 {
74 parts.push(format!(" ... +{} more", failed_names.len() - 10));
75 }
76 }
77
78 if !duration.is_empty() {
79 parts.push(duration);
80 }
81
82 parts.join("\n")
83}
84
85fn compress_cypress(output: &str) -> String {
86 let trimmed = output.trim();
87 if trimmed.is_empty() {
88 return "ok".to_string();
89 }
90
91 let mut passed = 0u32;
92 let mut failed = 0u32;
93 let mut pending = 0u32;
94
95 for line in trimmed.lines() {
96 let l = line.trim().to_lowercase();
97 if l.contains("passing") {
98 passed += extract_first_number(&l);
99 }
100 if l.contains("failing") {
101 failed += extract_first_number(&l);
102 }
103 if l.contains("pending") {
104 pending += extract_first_number(&l);
105 }
106 }
107
108 let total = passed + failed + pending;
109 if total == 0 {
110 return compact_output(trimmed, 10);
111 }
112
113 format!("{total} tests: {passed} passed, {failed} failed, {pending} pending")
114}
115
116fn extract_number(line: &str, keyword: &str) -> Option<u32> {
117 let pos = line.find(keyword)?;
118 let before = &line[..pos];
119 before.split_whitespace().last()?.parse().ok()
120}
121
122fn extract_first_number(line: &str) -> u32 {
123 for word in line.split_whitespace() {
124 if let Ok(n) = word.parse::<u32>() {
125 return n;
126 }
127 }
128 0
129}
130
131fn compact_output(text: &str, max: usize) -> String {
132 let lines: Vec<&str> = text.lines().filter(|l| !l.trim().is_empty()).collect();
133 if lines.len() <= max {
134 return lines.join("\n");
135 }
136 format!(
137 "{}\n... ({} more lines)",
138 lines[..max].join("\n"),
139 lines.len() - max
140 )
141}