Skip to main content

lean_ctx/core/patterns/
cargo.rs

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