lean_ctx/core/patterns/
cargo.rs1use regex::Regex;
2use std::sync::OnceLock;
3
4static COMPILING_RE: OnceLock<Regex> = OnceLock::new();
5static ERROR_RE: OnceLock<Regex> = OnceLock::new();
6static WARNING_RE: OnceLock<Regex> = OnceLock::new();
7static TEST_RESULT_RE: OnceLock<Regex> = OnceLock::new();
8static FINISHED_RE: OnceLock<Regex> = OnceLock::new();
9
10fn compiling_re() -> &'static Regex {
11 COMPILING_RE.get_or_init(|| Regex::new(r"Compiling (\S+) v(\S+)").unwrap())
12}
13fn error_re() -> &'static Regex {
14 ERROR_RE.get_or_init(|| Regex::new(r"error\[E(\d+)\]: (.+)").unwrap())
15}
16fn warning_re() -> &'static Regex {
17 WARNING_RE.get_or_init(|| Regex::new(r"warning: (.+)").unwrap())
18}
19fn test_result_re() -> &'static Regex {
20 TEST_RESULT_RE.get_or_init(|| {
21 Regex::new(r"test result: (\w+)\. (\d+) passed; (\d+) failed; (\d+) ignored").unwrap()
22 })
23}
24fn finished_re() -> &'static Regex {
25 FINISHED_RE.get_or_init(|| Regex::new(r"Finished .+ in (\d+\.?\d*s)").unwrap())
26}
27
28pub fn compress(command: &str, output: &str) -> Option<String> {
29 if command.contains("build") || command.contains("check") {
30 return Some(compress_build(output));
31 }
32 if command.contains("test") {
33 return Some(compress_test(output));
34 }
35 if command.contains("clippy") {
36 return Some(compress_clippy(output));
37 }
38 None
39}
40
41fn compress_build(output: &str) -> String {
42 let mut crate_count = 0u32;
43 let mut errors = Vec::new();
44 let mut warnings = 0u32;
45 let mut time = String::new();
46
47 for line in output.lines() {
48 if compiling_re().is_match(line) {
49 crate_count += 1;
50 }
51 if let Some(caps) = error_re().captures(line) {
52 errors.push(format!("E{}: {}", &caps[1], &caps[2]));
53 }
54 if warning_re().is_match(line) && !line.contains("generated") {
55 warnings += 1;
56 }
57 if let Some(caps) = finished_re().captures(line) {
58 time = caps[1].to_string();
59 }
60 }
61
62 let mut parts = Vec::new();
63 if crate_count > 0 {
64 parts.push(format!("compiled {crate_count} crates"));
65 }
66 if !errors.is_empty() {
67 parts.push(format!("{} errors:", errors.len()));
68 for e in &errors {
69 parts.push(format!(" {e}"));
70 }
71 }
72 if warnings > 0 {
73 parts.push(format!("{warnings} warnings"));
74 }
75 if !time.is_empty() {
76 parts.push(format!("({time})"));
77 }
78
79 if parts.is_empty() {
80 return "ok".to_string();
81 }
82 parts.join("\n")
83}
84
85fn compress_test(output: &str) -> String {
86 let mut results = Vec::new();
87 let mut failed_tests = Vec::new();
88 let mut time = String::new();
89
90 for line in output.lines() {
91 if let Some(caps) = test_result_re().captures(line) {
92 results.push(format!(
93 "{}: {} pass, {} fail, {} skip",
94 &caps[1], &caps[2], &caps[3], &caps[4]
95 ));
96 }
97 if line.contains("FAILED") && line.contains("---") {
98 let name = line.split_whitespace().nth(1).unwrap_or("?");
99 failed_tests.push(name.to_string());
100 }
101 if let Some(caps) = finished_re().captures(line) {
102 time = caps[1].to_string();
103 }
104 }
105
106 let mut parts = Vec::new();
107 if !results.is_empty() {
108 parts.extend(results);
109 }
110 if !failed_tests.is_empty() {
111 parts.push(format!("failed: {}", failed_tests.join(", ")));
112 }
113 if !time.is_empty() {
114 parts.push(format!("({time})"));
115 }
116
117 if parts.is_empty() {
118 return "ok".to_string();
119 }
120 parts.join("\n")
121}
122
123fn compress_clippy(output: &str) -> String {
124 let mut warnings = Vec::new();
125 let mut errors = Vec::new();
126
127 for line in output.lines() {
128 if let Some(caps) = error_re().captures(line) {
129 errors.push(caps[2].to_string());
130 } else if let Some(caps) = warning_re().captures(line) {
131 let msg = &caps[1];
132 if !msg.contains("generated") && !msg.starts_with('`') {
133 warnings.push(msg.to_string());
134 }
135 }
136 }
137
138 let mut parts = Vec::new();
139 if !errors.is_empty() {
140 parts.push(format!("{} errors: {}", errors.len(), errors.join("; ")));
141 }
142 if !warnings.is_empty() {
143 parts.push(format!("{} warnings", warnings.len()));
144 }
145
146 if parts.is_empty() {
147 return "clean".to_string();
148 }
149 parts.join("\n")
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155
156 #[test]
157 fn cargo_build_success() {
158 let output = " Compiling lean-ctx v2.1.1\n Finished release profile [optimized] target(s) in 30.5s";
159 let result = compress("cargo build", output).unwrap();
160 assert!(result.contains("compiled"), "should mention compilation");
161 assert!(result.contains("30.5s"), "should include build time");
162 }
163
164 #[test]
165 fn cargo_build_with_errors() {
166 let output = " Compiling lean-ctx v2.1.1\nerror[E0308]: mismatched types\n --> src/main.rs:10:5\n |\n10| 1 + \"hello\"\n | ^^^^^^^ expected integer, found &str";
167 let result = compress("cargo build", output).unwrap();
168 assert!(result.contains("E0308"), "should contain error code");
169 }
170
171 #[test]
172 fn cargo_test_success() {
173 let output = "running 5 tests\ntest test_one ... ok\ntest test_two ... ok\ntest test_three ... ok\ntest test_four ... ok\ntest test_five ... ok\n\ntest result: ok. 5 passed; 0 failed; 0 ignored";
174 let result = compress("cargo test", output).unwrap();
175 assert!(result.contains("5 pass"), "should show passed count");
176 }
177
178 #[test]
179 fn cargo_test_failure() {
180 let output = "running 3 tests\ntest test_ok ... ok\ntest test_fail ... FAILED\ntest test_ok2 ... ok\n\ntest result: FAILED. 2 passed; 1 failed; 0 ignored";
181 let result = compress("cargo test", output).unwrap();
182 assert!(result.contains("FAIL"), "should indicate failure");
183 }
184
185 #[test]
186 fn cargo_clippy_clean() {
187 let output = " Checking lean-ctx v2.1.1\n Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.2s";
188 let result = compress("cargo clippy", output).unwrap();
189 assert!(result.contains("clean"), "clean clippy should say clean");
190 }
191
192 #[test]
193 fn cargo_check_routes_to_build() {
194 let output = " Checking lean-ctx v2.1.1\n Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.1s";
195 let result = compress("cargo check", output);
196 assert!(
197 result.is_some(),
198 "cargo check should route to build compressor"
199 );
200 }
201}