zeph_tools/filter/
cargo_build.rs1use std::fmt::Write;
2use std::sync::LazyLock;
3
4use super::{
5 CargoBuildFilterConfig, CommandMatcher, FilterConfidence, FilterResult, OutputFilter,
6 make_result,
7};
8
9static CARGO_BUILD_MATCHER: LazyLock<CommandMatcher> = LazyLock::new(|| {
10 CommandMatcher::Custom(Box::new(|cmd| {
11 let c = cmd.to_lowercase();
12 let tokens: Vec<&str> = c.split_whitespace().collect();
13 if tokens.first() != Some(&"cargo") {
14 return false;
15 }
16 let dominated = ["test", "nextest", "clippy"];
17 !tokens.iter().skip(1).any(|t| dominated.contains(t))
18 }))
19});
20
21const NOISE_PREFIXES: &[&str] = &[
22 "Compiling ",
23 "Downloading ",
24 "Downloaded ",
25 "Updating ",
26 "Fetching ",
27 "Fresh ",
28 "Packaging ",
29 "Verifying ",
30 "Archiving ",
31 "Locking ",
32 "Adding ",
33 "Removing ",
34 "Checking ",
35 "Documenting ",
36 "Running ",
37 "Loaded ",
38 "Blocking ",
39 "Unpacking ",
40];
41
42const LONG_OUTPUT_THRESHOLD: usize = 30;
44const KEEP_HEAD: usize = 10;
45const KEEP_TAIL: usize = 5;
46
47fn is_noise(line: &str) -> bool {
48 let trimmed = line.trim_start();
49 NOISE_PREFIXES.iter().any(|p| trimmed.starts_with(p))
50}
51
52pub fn is_cargo_noise(line: &str) -> bool {
54 let trimmed = line.trim_start();
55 trimmed.starts_with("Finished ") || is_noise(line)
56}
57
58pub struct CargoBuildFilter;
59
60impl CargoBuildFilter {
61 #[must_use]
62 pub fn new(_config: CargoBuildFilterConfig) -> Self {
63 Self
64 }
65}
66
67impl OutputFilter for CargoBuildFilter {
68 fn name(&self) -> &'static str {
69 "cargo_build"
70 }
71
72 fn matcher(&self) -> &CommandMatcher {
73 &CARGO_BUILD_MATCHER
74 }
75
76 fn filter(&self, _command: &str, raw_output: &str, exit_code: i32) -> FilterResult {
77 let mut noise_count = 0usize;
78 let mut kept = Vec::new();
79 let mut finished_line: Option<&str> = None;
80
81 for line in raw_output.lines() {
82 let trimmed = line.trim_start();
83 if trimmed.starts_with("Finished ") {
84 finished_line = Some(trimmed);
85 noise_count += 1;
86 } else if is_noise(line) {
87 noise_count += 1;
88 } else {
89 kept.push(line);
90 }
91 }
92
93 if noise_count > 0 {
94 return build_noise_result(raw_output, &kept, finished_line, noise_count);
95 }
96
97 if exit_code != 0 {
98 return make_result(
99 raw_output,
100 raw_output.to_owned(),
101 FilterConfidence::Fallback,
102 );
103 }
104
105 let lines: Vec<&str> = raw_output.lines().collect();
107 if lines.len() > LONG_OUTPUT_THRESHOLD {
108 return truncate_long(raw_output, &lines);
109 }
110
111 make_result(
112 raw_output,
113 raw_output.to_owned(),
114 FilterConfidence::Fallback,
115 )
116 }
117}
118
119fn build_noise_result(
120 raw: &str,
121 kept: &[&str],
122 finished_line: Option<&str>,
123 noise_count: usize,
124) -> FilterResult {
125 let mut output = String::new();
126 if let Some(fin) = finished_line {
127 let _ = writeln!(output, "{fin}");
128 }
129 let _ = writeln!(output, "({noise_count} compile/fetch lines removed)");
130 if !kept.is_empty() {
131 output.push('\n');
132 if kept.len() > LONG_OUTPUT_THRESHOLD {
133 let omitted = kept.len() - KEEP_HEAD - KEEP_TAIL;
134 for line in &kept[..KEEP_HEAD] {
135 let _ = writeln!(output, "{line}");
136 }
137 let _ = writeln!(output, "\n... ({omitted} lines omitted) ...\n");
138 for line in &kept[kept.len() - KEEP_TAIL..] {
139 let _ = writeln!(output, "{line}");
140 }
141 } else {
142 for line in kept {
143 let _ = writeln!(output, "{line}");
144 }
145 }
146 }
147 make_result(raw, output.trim_end().to_owned(), FilterConfidence::Full)
148}
149
150fn truncate_long(raw: &str, lines: &[&str]) -> FilterResult {
151 let total = lines.len();
152 let omitted = total - KEEP_HEAD - KEEP_TAIL;
153 let mut output = String::new();
154 for line in &lines[..KEEP_HEAD] {
155 let _ = writeln!(output, "{line}");
156 }
157 let _ = writeln!(output, "\n... ({omitted} lines omitted) ...\n");
158 for line in &lines[total - KEEP_TAIL..] {
159 let _ = writeln!(output, "{line}");
160 }
161 make_result(raw, output.trim_end().to_owned(), FilterConfidence::Partial)
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 fn make_filter() -> CargoBuildFilter {
169 CargoBuildFilter::new(CargoBuildFilterConfig::default())
170 }
171
172 #[test]
173 fn matches_cargo_build_commands() {
174 let f = make_filter();
175 assert!(f.matcher().matches("cargo build"));
176 assert!(f.matcher().matches("cargo build --release"));
177 assert!(f.matcher().matches("cargo doc --no-deps"));
178 assert!(f.matcher().matches("cargo +nightly fmt --check"));
179 assert!(f.matcher().matches("cargo audit"));
180 assert!(f.matcher().matches("cargo tree --duplicates"));
181 assert!(f.matcher().matches("cargo bench"));
182 }
183
184 #[test]
185 fn skips_test_and_clippy() {
186 let f = make_filter();
187 assert!(!f.matcher().matches("cargo test"));
188 assert!(!f.matcher().matches("cargo nextest run"));
189 assert!(!f.matcher().matches("cargo clippy --workspace"));
190 }
191
192 #[test]
193 fn filters_compile_noise() {
194 let f = make_filter();
195 let raw = " Compiling serde v1.0.200\n Compiling zeph-core v0.9.9\n Compiling zeph-tools v0.9.9\n Finished `dev` profile [unoptimized + debuginfo] target(s) in 5.32s";
196 let result = f.filter("cargo build", raw, 0);
197 assert_eq!(result.confidence, FilterConfidence::Full);
198 assert!(result.output.contains("Finished"));
199 assert!(result.output.contains("4 compile/fetch lines removed"));
200 assert!(!result.output.contains("Compiling"));
201 }
202
203 #[test]
204 fn filters_audit_noise() {
205 let f = make_filter();
206 let raw = " Fetching advisory database from `https://github.com/RustSec/advisory-db.git`\n Loaded 920 security advisories (from /Users/rabax/.cargo/advisory-db)\n Updating crates.io index\n0 vulnerabilities found";
207 let result = f.filter("cargo audit", raw, 1);
208 assert_eq!(result.confidence, FilterConfidence::Full);
209 assert!(result.output.contains("3 compile/fetch lines removed"));
210 assert!(result.output.contains("0 vulnerabilities found"));
211 assert!(!result.output.contains("Fetching"));
212 }
213
214 #[test]
215 fn truncates_long_tree_output() {
216 let f = make_filter();
217 let mut lines = Vec::new();
218 for i in 0..80 {
219 lines.push(format!("├── dep-{i} v0.1.{i}"));
220 }
221 let raw = lines.join("\n");
222 let result = f.filter("cargo tree", &raw, 0);
223 assert_eq!(result.confidence, FilterConfidence::Partial);
224 assert!(result.output.contains("lines omitted"));
225 assert!(result.output.contains("dep-0"));
226 assert!(result.output.contains("dep-79"));
227 }
228
229 #[test]
230 fn preserves_full_on_error() {
231 let f = make_filter();
232 let raw = "error[E0308]: mismatched types\n --> src/main.rs:10:5";
233 let result = f.filter("cargo build", raw, 1);
234 assert_eq!(result.output, raw);
235 assert_eq!(result.confidence, FilterConfidence::Fallback);
236 }
237
238 #[test]
239 fn passthrough_short_output() {
240 let f = make_filter();
241 let raw = "some short output\nonly two lines";
242 let result = f.filter("cargo build", raw, 0);
243 assert_eq!(result.output, raw);
244 assert_eq!(result.confidence, FilterConfidence::Fallback);
245 }
246
247 #[test]
248 fn keeps_non_noise_lines() {
249 let f = make_filter();
250 let raw = " Compiling zeph-core v0.9.9\nwarning: unused import\n --> src/lib.rs:5:1\n Finished `dev` profile target(s) in 2.00s";
251 let result = f.filter("cargo build", raw, 0);
252 assert!(result.output.contains("warning: unused import"));
253 assert!(result.output.contains("src/lib.rs:5:1"));
254 assert!(!result.output.contains("Compiling"));
255 }
256
257 #[test]
258 fn cargo_build_filter_snapshot() {
259 let f = make_filter();
260 let raw = "\
261 Compiling zeph-core v0.11.0
262 Compiling zeph-tools v0.11.0
263 Compiling zeph-llm v0.11.0
264warning: unused import: `std::fmt`
265 --> crates/zeph-core/src/lib.rs:3:5
266 |
2673 | use std::fmt;
268 | ^^^^^^^^
269 = note: `#[warn(unused_imports)]` on by default
270 Finished `dev` profile [unoptimized + debuginfo] target(s) in 4.23s";
271 let result = f.filter("cargo build", raw, 0);
272 insta::assert_snapshot!(result.output);
273 }
274
275 #[test]
276 fn cargo_build_error_snapshot() {
277 let f = make_filter();
278 let raw = "\
279 Compiling zeph-core v0.11.0
280error[E0308]: mismatched types
281 --> crates/zeph-core/src/lib.rs:10:5
282 |
28310 | return 42;
284 | ^^ expected `()`, found integer
285error: could not compile `zeph-core` due to 1 previous error";
286 let result = f.filter("cargo build", raw, 1);
287 insta::assert_snapshot!(result.output);
288 }
289}