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