From 99307b2f51d205c381e64f38331c46403e858930 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 12 Jun 2026 20:30:39 +0200 Subject: [PATCH 01/29] fix(cargo): surface --message-format=json build errors instead of "compiled" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With --message-format=json, cargo emits diagnostics as NDJSON, so the block handler that keys on human `error[..]` lines counted zero errors and summarized a failing build as "cargo build (1 crates compiled)" — the exact same wording as a successful build. The error was dropped from the inline output and only the exit code (easily swallowed by an && / || chain) hinted at failure. Recover the NDJSON `compiler-message` diagnostics from the raw stream when building the summary: count them and render them. A failed json build now shows the real error[E..] plus "N errors", and the exit code is used as a backstop so a non-zero build is never reported as compiled. Non-json output is untouched (the helper returns nothing for it). Closes #2419 --- src/cmds/rust/cargo_cmd.rs | 106 +++++++++++++++++++++++++++++++++---- 1 file changed, 96 insertions(+), 10 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 061b1bfbde..ccaae5458e 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -93,19 +93,31 @@ impl BlockHandler for CargoBuildHandler { !(line.trim().is_empty() && block.len() > 3) } - fn format_summary(&self, _exit_code: i32, _raw: &str) -> Option { - if self.error_count == 0 && self.warnings == 0 { + fn format_summary(&self, exit_code: i32, raw: &str) -> Option { + let json_errors = extract_json_diagnostics(raw, "error"); + let errors = self.error_count.max(json_errors.len()); + let warnings = self + .warnings + .max(extract_json_diagnostics(raw, "warning").len()); + + if errors == 0 && warnings == 0 && exit_code == 0 { let mut s = format!("cargo build ({} crates compiled)", self.compiled); if let Some(ref finished) = self.finished_line { s = format!("{}\n{}", s, finished); } - Some(format!("{}\n", s)) - } else { - Some(format!( - "cargo build: {} errors, {} warnings ({} crates)\n", - self.error_count, self.warnings, self.compiled - )) + return Some(format!("{}\n", s)); + } + + let mut out = String::new(); + for d in &json_errors { + out.push_str(d); + out.push('\n'); } + out.push_str(&format!( + "cargo build: {} errors, {} warnings ({} crates)\n", + errors, warnings, self.compiled + )); + Some(out) } } @@ -757,6 +769,36 @@ fn filter_cargo_nextest(output: &str) -> String { String::new() } +fn extract_json_diagnostics(raw: &str, level: &str) -> Vec { + let mut diags = Vec::new(); + for line in raw.lines() { + let line = line.trim_start(); + if !line.starts_with('{') { + continue; + } + let Ok(v) = serde_json::from_str::(line) else { + continue; + }; + if v["reason"].as_str() != Some("compiler-message") { + continue; + } + let msg = &v["message"]; + if msg["level"].as_str() != Some(level) { + continue; + } + let text = msg["message"].as_str().unwrap_or(""); + if text.starts_with("aborting due to") || text.starts_with("could not compile") { + continue; + } + if let Some(rendered) = msg["rendered"].as_str() { + diags.push(rendered.trim_end().to_string()); + } else if !text.is_empty() { + diags.push(text.to_string()); + } + } + diags +} + fn filter_cargo_build(output: &str) -> String { let mut handler = CargoBuildHandler::new(); let mut blocks: Vec> = Vec::new(); @@ -786,7 +828,13 @@ fn filter_cargo_build(output: &str) -> String { blocks.push(current_block); } - if handler.error_count == 0 && handler.warnings == 0 { + let json_errors = extract_json_diagnostics(output, "error"); + let errors = handler.error_count.max(json_errors.len()); + let warnings = handler + .warnings + .max(extract_json_diagnostics(output, "warning").len()); + + if errors == 0 && warnings == 0 { let mut s = format!("cargo build ({} crates compiled)", handler.compiled); if let Some(ref finished) = handler.finished_line { s = format!("{}\n{}", s, finished); @@ -796,8 +844,12 @@ fn filter_cargo_build(output: &str) -> String { let mut result = format!( "cargo build: {} errors, {} warnings ({} crates)\n", - handler.error_count, handler.warnings, handler.compiled + errors, warnings, handler.compiled ); + for d in &json_errors { + result.push_str(d); + result.push('\n'); + } const MAX_CHECK_BLOCKS: usize = CAP_ERRORS; for (i, blk) in blocks.iter().enumerate().take(MAX_CHECK_BLOCKS) { result.push_str(&blk.join("\n")); @@ -2129,6 +2181,40 @@ error: aborting due to 1 previous error assert!(!result.contains("aborting"), "got: {}", result); } + #[test] + fn test_cargo_build_stream_json_failure_not_compiled() { + let input = concat!( + " Compiling v_cargo v0.1.0\n", + r#"{"reason":"compiler-message","message":{"rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18","level":"error","message":"mismatched types"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let mut f = BlockStreamFilter::new(CargoBuildHandler::new()); + let result = run_block_filter(&mut f, input, 101); + assert!(result.contains("E0308"), "got: {}", result); + assert!(result.contains("1 errors"), "got: {}", result); + assert!( + !result.contains("crates compiled"), + "failed json build must not be reported as compiled: {}", + result + ); + } + + #[test] + fn test_filter_cargo_build_json_failure_not_compiled() { + let output = concat!( + r#"{"reason":"compiler-message","message":{"rendered":"error[E0277]: the trait bound is not satisfied","level":"error","message":"trait bound"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let result = filter_cargo_build(output); + assert!(result.contains("E0277"), "got: {}", result); + assert!(result.contains("1 errors"), "got: {}", result); + assert!(!result.contains("crates compiled"), "got: {}", result); + } + #[test] fn test_cargo_test_stream_all_pass() { let input = r#" Compiling rtk v0.5.0 From 877c7efcbc2bf3990246182707eb8fd2e82d8f42 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Sat, 13 Jun 2026 12:40:23 +0200 Subject: [PATCH 02/29] refactor(cargo): single-pass json diagnostics and a shared build summary Parse the --message-format=json stream once into errors and warnings instead of scanning it twice per call site, and move the success and failure summaries into two small helpers shared by the streaming and batch paths. Also render json warnings, not just errors, so json mode matches how the human path already surfaces warning blocks. --- src/cmds/rust/cargo_cmd.rs | 131 +++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 49 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index ccaae5458e..2ce1c283ed 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -94,30 +94,22 @@ impl BlockHandler for CargoBuildHandler { } fn format_summary(&self, exit_code: i32, raw: &str) -> Option { - let json_errors = extract_json_diagnostics(raw, "error"); - let errors = self.error_count.max(json_errors.len()); - let warnings = self - .warnings - .max(extract_json_diagnostics(raw, "warning").len()); + let json = extract_json_diagnostics(raw); + let errors = self.error_count.max(json.errors.len()); + let warnings = self.warnings.max(json.warnings.len()); if errors == 0 && warnings == 0 && exit_code == 0 { - let mut s = format!("cargo build ({} crates compiled)", self.compiled); - if let Some(ref finished) = self.finished_line { - s = format!("{}\n{}", s, finished); - } - return Some(format!("{}\n", s)); - } - - let mut out = String::new(); - for d in &json_errors { - out.push_str(d); - out.push('\n'); + return Some(cargo_build_success_line( + self.compiled, + self.finished_line.as_deref(), + )); } - out.push_str(&format!( - "cargo build: {} errors, {} warnings ({} crates)\n", - errors, warnings, self.compiled - )); - Some(out) + Some(cargo_build_failure_summary( + self.compiled, + errors, + warnings, + &json, + )) } } @@ -769,8 +761,14 @@ fn filter_cargo_nextest(output: &str) -> String { String::new() } -fn extract_json_diagnostics(raw: &str, level: &str) -> Vec { - let mut diags = Vec::new(); +struct JsonDiagnostics { + errors: Vec, + warnings: Vec, +} + +fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics { + let mut errors = Vec::new(); + let mut warnings = Vec::new(); for line in raw.lines() { let line = line.trim_start(); if !line.starts_with('{') { @@ -783,20 +781,52 @@ fn extract_json_diagnostics(raw: &str, level: &str) -> Vec { continue; } let msg = &v["message"]; - if msg["level"].as_str() != Some(level) { - continue; - } + let bucket = match msg["level"].as_str() { + Some("error") => &mut errors, + Some("warning") => &mut warnings, + _ => continue, + }; let text = msg["message"].as_str().unwrap_or(""); if text.starts_with("aborting due to") || text.starts_with("could not compile") { continue; } if let Some(rendered) = msg["rendered"].as_str() { - diags.push(rendered.trim_end().to_string()); + bucket.push(rendered.trim_end().to_string()); } else if !text.is_empty() { - diags.push(text.to_string()); + bucket.push(text.to_string()); } } - diags + JsonDiagnostics { errors, warnings } +} + +fn cargo_build_success_line(compiled: usize, finished: Option<&str>) -> String { + let mut s = format!("cargo build ({} crates compiled)", compiled); + if let Some(f) = finished { + s = format!("{}\n{}", s, f); + } + format!("{}\n", s) +} + +fn cargo_build_failure_summary( + compiled: usize, + errors: usize, + warnings: usize, + json: &JsonDiagnostics, +) -> String { + let mut out = String::new(); + for d in &json.errors { + out.push_str(d); + out.push('\n'); + } + for d in &json.warnings { + out.push_str(d); + out.push('\n'); + } + out.push_str(&format!( + "cargo build: {} errors, {} warnings ({} crates)\n", + errors, warnings, compiled + )); + out } fn filter_cargo_build(output: &str) -> String { @@ -828,28 +858,15 @@ fn filter_cargo_build(output: &str) -> String { blocks.push(current_block); } - let json_errors = extract_json_diagnostics(output, "error"); - let errors = handler.error_count.max(json_errors.len()); - let warnings = handler - .warnings - .max(extract_json_diagnostics(output, "warning").len()); + let json = extract_json_diagnostics(output); + let errors = handler.error_count.max(json.errors.len()); + let warnings = handler.warnings.max(json.warnings.len()); if errors == 0 && warnings == 0 { - let mut s = format!("cargo build ({} crates compiled)", handler.compiled); - if let Some(ref finished) = handler.finished_line { - s = format!("{}\n{}", s, finished); - } - return s; + return cargo_build_success_line(handler.compiled, handler.finished_line.as_deref()); } - let mut result = format!( - "cargo build: {} errors, {} warnings ({} crates)\n", - errors, warnings, handler.compiled - ); - for d in &json_errors { - result.push_str(d); - result.push('\n'); - } + let mut result = cargo_build_failure_summary(handler.compiled, errors, warnings, &json); const MAX_CHECK_BLOCKS: usize = CAP_ERRORS; for (i, blk) in blocks.iter().enumerate().take(MAX_CHECK_BLOCKS) { result.push_str(&blk.join("\n")); @@ -2184,8 +2201,8 @@ error: aborting due to 1 previous error #[test] fn test_cargo_build_stream_json_failure_not_compiled() { let input = concat!( - " Compiling v_cargo v0.1.0\n", - r#"{"reason":"compiler-message","message":{"rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18","level":"error","message":"mismatched types"}}"#, + " Compiling v_cargo v0.1.0 (/tmp/v_cargo)\n", + r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"code":{"code":"E0308"},"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18"}}"#, "\n", r#"{"reason":"build-finished","success":false}"#, "\n", @@ -2201,6 +2218,22 @@ error: aborting due to 1 previous error ); } + #[test] + fn test_cargo_build_stream_json_warning_rendered() { + let input = concat!( + " Compiling v_cargo v0.1.0 (/tmp/v_cargo)\n", + r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"level":"warning","message":"unused variable: `x`","rendered":"warning: unused variable: `x`\n --> src/main.rs:2:9"}}"#, + "\n", + r#"{"reason":"build-finished","success":true}"#, + "\n", + ); + let mut f = BlockStreamFilter::new(CargoBuildHandler::new()); + let result = run_block_filter(&mut f, input, 0); + assert!(result.contains("unused variable"), "got: {}", result); + assert!(result.contains("1 warnings"), "got: {}", result); + assert!(!result.contains("crates compiled"), "got: {}", result); + } + #[test] fn test_filter_cargo_build_json_failure_not_compiled() { let output = concat!( From 5ee9c825cf18e4d65f1c1cb774bdd83ac089d9ac Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Thu, 18 Jun 2026 17:09:53 +0200 Subject: [PATCH 03/29] fix(cargo): cap json build diagnostics like the human-text path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The --message-format=json failure summary rendered every error and warning in full, while the human-text path bounds blocks at CAP_ERRORS. A json build with many errors could therefore print an unbounded wall of diagnostics — exactly what RTK normally caps. Cap the json error/warning lists at CAP_ERRORS/CAP_WARNINGS and append a '… +N more' hint on overflow. The summary line still reports the real total counts. --- src/cmds/rust/cargo_cmd.rs | 42 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 2ce1c283ed..2fc5324194 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -814,14 +814,23 @@ fn cargo_build_failure_summary( json: &JsonDiagnostics, ) -> String { let mut out = String::new(); - for d in &json.errors { + for d in json.errors.iter().take(CAP_ERRORS) { out.push_str(d); out.push('\n'); } - for d in &json.warnings { + if json.errors.len() > CAP_ERRORS { + out.push_str(&format!("… +{} more errors\n", json.errors.len() - CAP_ERRORS)); + } + for d in json.warnings.iter().take(CAP_WARNINGS) { out.push_str(d); out.push('\n'); } + if json.warnings.len() > CAP_WARNINGS { + out.push_str(&format!( + "… +{} more warnings\n", + json.warnings.len() - CAP_WARNINGS + )); + } out.push_str(&format!( "cargo build: {} errors, {} warnings ({} crates)\n", errors, warnings, compiled @@ -2248,6 +2257,35 @@ error: aborting due to 1 previous error assert!(!result.contains("crates compiled"), "got: {}", result); } + #[test] + fn test_filter_cargo_build_json_errors_capped() { + let total = CAP_ERRORS + 5; + let mut output = String::new(); + for i in 0..total { + output.push_str(&format!( + r#"{{"reason":"compiler-message","message":{{"code":{{"code":"E0308"}},"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types ({})"}}}}"#, + i + )); + output.push('\n'); + } + output.push_str(r#"{"reason":"build-finished","success":false}"#); + output.push('\n'); + + let result = filter_cargo_build(&output); + let rendered = result.matches("error[E0308]").count(); + assert_eq!(rendered, CAP_ERRORS, "json errors must be capped: {}", result); + assert!( + result.contains(&format!("… +{} more errors", total - CAP_ERRORS)), + "expected overflow hint: {}", + result + ); + assert!( + result.contains(&format!("{} errors", total)), + "summary must report the real total: {}", + result + ); + } + #[test] fn test_cargo_test_stream_all_pass() { let input = r#" Compiling rtk v0.5.0 From 23191946dfba3003ca1efba530e5241e37b0574f Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Thu, 18 Jun 2026 18:28:40 +0200 Subject: [PATCH 04/29] test(cargo): assert token savings on the json build-failure path cargo_cmd had no token-savings test despite the repo's >=60% rule, so nothing locked in the gain from extracting rendered diagnostics out of the verbose json envelope and capping them. Add a savings assertion on a large failing --message-format=json build. --- src/cmds/rust/cargo_cmd.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 2fc5324194..e19923d142 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -2286,6 +2286,43 @@ error: aborting due to 1 previous error ); } + #[test] + fn test_filter_cargo_build_json_savings() { + // Real --message-format=json lines carry a verbose envelope (spans, + // children, code, message) around the human `rendered` text. The filter + // keeps only `rendered` and caps the list, so savings come from both + // dropping the envelope and capping a large failing build. + let template = r#"{"reason":"compiler-message","package_id":"demo 0.1.0 (path+file:///tmp/demo)","manifest_path":"/tmp/demo/Cargo.toml","target":{"kind":["bin"],"crate_types":["bin"],"name":"demo","src_path":"/tmp/demo/src/main.rs","edition":"2021","doc":true,"doctest":false,"test":true},"message":{"rendered":"error[E0308]: mismatched types\n --> src/main.rs:IDX:18\n |\nIDX | let _xIDX: i32 = \"errIDX\";\n | --- ^^^^^^^ expected `i32`, found `&str`\n | |\n | expected due to this\n\n","$message_type":"diagnostic","children":[{"children":[],"code":null,"level":"note","message":"expected due to the type annotation here","rendered":null,"spans":[]}],"code":{"code":"E0308","explanation":null},"level":"error","message":"mismatched types","spans":[{"byte_end":40,"byte_start":33,"column_end":25,"column_start":18,"expansion":null,"file_name":"src/main.rs","is_primary":true,"label":"expected `i32`, found `&str`","line_end":IDX,"line_start":IDX,"suggested_replacement":null,"suggestion_applicability":null,"text":[{"highlight_end":25,"highlight_start":18,"text":" let _xIDX: i32 = \"errIDX\";"}]}]}}"#; + + let total = CAP_ERRORS + 30; + let mut input = String::new(); + for i in 0..total { + input.push_str(&template.replace("IDX", &i.to_string())); + input.push('\n'); + } + input.push_str(r#"{"reason":"build-finished","success":false}"#); + input.push('\n'); + + let result = filter_cargo_build(&input); + + // The cap is what bounds the output, so assert it holds here too. + let rendered = result.matches("error[E0308]").count(); + assert_eq!(rendered, CAP_ERRORS, "json errors must be capped: {}", result); + assert!( + result.contains(&format!("… +{} more errors", total - CAP_ERRORS)), + "expected overflow hint: {}", + result + ); + + let raw = input.split_whitespace().count(); + let out = result.split_whitespace().count(); + let savings = 100.0 - (out as f64 / raw as f64) * 100.0; + assert!( + savings >= 60.0, + "token savings dropped below 60%: {savings:.1}%" + ); + } + #[test] fn test_cargo_test_stream_all_pass() { let input = r#" Compiling rtk v0.5.0 From d061c23eff9ff73c8309d4873f03f5fc653760b9 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Thu, 18 Jun 2026 19:05:41 +0200 Subject: [PATCH 05/29] test(cargo): cover the json build success path The json-aware summary had tests for the failure and capped paths but none for a clean --message-format=json build, where artifacts and build-finished:true must still report crates compiled. --- src/cmds/rust/cargo_cmd.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index e19923d142..c664e5c426 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -2188,6 +2188,24 @@ error: test run failed assert!(!result.contains("Compiling"), "got: {}", result); } + #[test] + fn test_cargo_build_stream_json_success() { + let input = concat!( + " Compiling demo v0.1.0 (/tmp/demo)\n", + r#"{"reason":"compiler-artifact","package_id":"demo 0.1.0","target":{"name":"demo"},"executable":"/tmp/demo/target/debug/demo"}"#, + "\n", + " Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.39s\n", + r#"{"reason":"build-finished","success":true}"#, + "\n", + ); + let mut f = BlockStreamFilter::new(CargoBuildHandler::new()); + let result = run_block_filter(&mut f, input, 0); + assert!(result.contains("1 crates compiled"), "got: {}", result); + assert!(result.contains("Finished"), "got: {}", result); + assert!(!result.contains("error"), "got: {}", result); + assert!(!result.contains("Compiling"), "got: {}", result); + } + #[test] fn test_cargo_build_stream_errors() { let input = r#" Compiling rtk v0.5.0 From 9ae0872aeebc777f7850d5b4f16b0943fe7bc765 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Wed, 1 Jul 2026 21:30:36 +0200 Subject: [PATCH 06/29] fix(cargo): skip "N warnings generated" record in json diagnostics --- src/cmds/rust/cargo_cmd.rs | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index c664e5c426..ed64b38e48 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -787,7 +787,10 @@ fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics { _ => continue, }; let text = msg["message"].as_str().unwrap_or(""); - if text.starts_with("aborting due to") || text.starts_with("could not compile") { + if text.starts_with("aborting due to") + || text.starts_with("could not compile") + || (text.contains("warning") && text.contains("generated")) + { continue; } if let Some(rendered) = msg["rendered"].as_str() { @@ -2261,6 +2264,24 @@ error: aborting due to 1 previous error assert!(!result.contains("crates compiled"), "got: {}", result); } + #[test] + fn test_extract_json_diagnostics_skips_generated_summary() { + let output = concat!( + r#"{"reason":"compiler-message","message":{"level":"warning","message":"unused variable: `x`","rendered":"warning: unused variable: `x`"}}"#, + "\n", + r#"{"reason":"compiler-message","message":{"level":"warning","message":"1 warning generated","rendered":"warning: 1 warning generated"}}"#, + "\n", + r#"{"reason":"build-finished","success":true}"#, + "\n", + ); + let json = extract_json_diagnostics(output); + assert_eq!(json.warnings.len(), 1, "generated summary must not inflate the count"); + assert!( + !json.warnings.iter().any(|w| w.contains("generated")), + "the generated summary line must not appear in the output" + ); + } + #[test] fn test_filter_cargo_build_json_failure_not_compiled() { let output = concat!( From b47653a9ec2d5a3fcc1df28b36d23156bb92112a Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Wed, 1 Jul 2026 21:33:56 +0200 Subject: [PATCH 07/29] fix(cargo): strip ansi from json rendered diagnostics --- src/cmds/rust/cargo_cmd.rs | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index ed64b38e48..c63c470356 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -794,7 +794,7 @@ fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics { continue; } if let Some(rendered) = msg["rendered"].as_str() { - bucket.push(rendered.trim_end().to_string()); + bucket.push(crate::core::utils::strip_ansi(rendered).trim_end().to_string()); } else if !text.is_empty() { bucket.push(text.to_string()); } @@ -2264,6 +2264,29 @@ error: aborting due to 1 previous error assert!(!result.contains("crates compiled"), "got: {}", result); } + #[test] + fn test_extract_json_diagnostics_strips_ansi() { + let output = concat!( + r#"{"reason":"compiler-message","message":{"level":"error","message":"mismatched types","rendered":"\u001b[1m\u001b[31merror[E0308]\u001b[0m: mismatched types"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let json = extract_json_diagnostics(output); + assert_eq!(json.errors.len(), 1); + assert!( + !json.errors[0].contains('\u{1b}'), + "ansi escapes must be stripped: {:?}", + json.errors[0] + ); + assert!( + json.errors[0].contains("error[E0308]"), + "got: {:?}", + json.errors[0] + ); + } + + #[test] fn test_extract_json_diagnostics_skips_generated_summary() { let output = concat!( From cf1caf93b8cdfd2db974ff3cb1b3859cbb955257 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Wed, 1 Jul 2026 21:37:36 +0200 Subject: [PATCH 08/29] fix(cargo): tee-hint dropped json diagnostics beyond the cap --- src/cmds/rust/cargo_cmd.rs | 51 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index c63c470356..332fd7abb8 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -834,6 +834,18 @@ fn cargo_build_failure_summary( json.warnings.len() - CAP_WARNINGS )); } + if json.errors.len() > CAP_ERRORS || json.warnings.len() > CAP_WARNINGS { + let full = json + .errors + .iter() + .chain(json.warnings.iter()) + .map(String::as_str) + .collect::>() + .join("\n\n"); + if let Some(hint) = crate::core::tee::force_tee_hint(&full, "cargo-json-issues") { + out.push_str(&format!(" {}\n", hint)); + } + } out.push_str(&format!( "cargo build: {} errors, {} warnings ({} crates)\n", errors, warnings, compiled @@ -2348,6 +2360,45 @@ error: aborting due to 1 previous error ); } + #[test] + fn test_filter_cargo_build_json_errors_tee_captured() { + let tee_dir = std::env::temp_dir().join(format!("rtk-tee-json-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&tee_dir); + std::env::set_var("RTK_TEE_DIR", &tee_dir); + + let total = CAP_ERRORS + 3; + let mut output = String::new(); + for i in 0..total { + output.push_str(&format!( + r#"{{"reason":"compiler-message","message":{{"level":"error","message":"mismatched types","rendered":"error[E0308]: dropped marker {}"}}}}"#, + i + )); + output.push('\n'); + } + output.push_str(r#"{"reason":"build-finished","success":false}"#); + output.push('\n'); + + let result = filter_cargo_build(&output); + std::env::remove_var("RTK_TEE_DIR"); + + // A dropped error (past the cap) must be recoverable from the tee file, + // not silently discarded. Only assert when tee actually wrote a file. + if result.contains("[full output:") { + let captured: String = std::fs::read_dir(&tee_dir) + .into_iter() + .flatten() + .flatten() + .filter_map(|e| std::fs::read_to_string(e.path()).ok()) + .collect(); + assert!( + captured.contains(&format!("dropped marker {}", total - 1)), + "tee file must hold the dropped diagnostics: {}", + captured + ); + } + let _ = std::fs::remove_dir_all(&tee_dir); + } + #[test] fn test_filter_cargo_build_json_savings() { // Real --message-format=json lines carry a verbose envelope (spans, From ff6cebda4578a6f60e5a75304450e8a15e529366 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Wed, 1 Jul 2026 21:38:11 +0200 Subject: [PATCH 09/29] refactor(cargo): extract shared json diag count merge --- src/cmds/rust/cargo_cmd.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 332fd7abb8..316043b3d9 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -95,8 +95,7 @@ impl BlockHandler for CargoBuildHandler { fn format_summary(&self, exit_code: i32, raw: &str) -> Option { let json = extract_json_diagnostics(raw); - let errors = self.error_count.max(json.errors.len()); - let warnings = self.warnings.max(json.warnings.len()); + let (errors, warnings) = merge_diag_counts(self.error_count, self.warnings, &json); if errors == 0 && warnings == 0 && exit_code == 0 { return Some(cargo_build_success_line( @@ -802,6 +801,13 @@ fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics { JsonDiagnostics { errors, warnings } } +fn merge_diag_counts(error_count: usize, warnings: usize, json: &JsonDiagnostics) -> (usize, usize) { + ( + error_count.max(json.errors.len()), + warnings.max(json.warnings.len()), + ) +} + fn cargo_build_success_line(compiled: usize, finished: Option<&str>) -> String { let mut s = format!("cargo build ({} crates compiled)", compiled); if let Some(f) = finished { @@ -883,8 +889,7 @@ fn filter_cargo_build(output: &str) -> String { } let json = extract_json_diagnostics(output); - let errors = handler.error_count.max(json.errors.len()); - let warnings = handler.warnings.max(json.warnings.len()); + let (errors, warnings) = merge_diag_counts(handler.error_count, handler.warnings, &json); if errors == 0 && warnings == 0 { return cargo_build_success_line(handler.compiled, handler.finished_line.as_deref()); From 9f20821e7c71b9ffbaa78fa446d443cc3b1f0741 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Wed, 1 Jul 2026 21:38:54 +0200 Subject: [PATCH 10/29] refactor(cargo): single-alloc cargo_build_success_line --- src/cmds/rust/cargo_cmd.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 316043b3d9..0cf5539bd6 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -809,11 +809,10 @@ fn merge_diag_counts(error_count: usize, warnings: usize, json: &JsonDiagnostics } fn cargo_build_success_line(compiled: usize, finished: Option<&str>) -> String { - let mut s = format!("cargo build ({} crates compiled)", compiled); - if let Some(f) = finished { - s = format!("{}\n{}", s, f); + match finished { + Some(f) => format!("cargo build ({} crates compiled)\n{}\n", compiled, f), + None => format!("cargo build ({} crates compiled)\n", compiled), } - format!("{}\n", s) } fn cargo_build_failure_summary( From 5ea03f3d1bd30803a333ead8082ee716e25256fc Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Thu, 2 Jul 2026 00:34:06 +0200 Subject: [PATCH 11/29] feat(cargo): route --message-format=json builds through explicit arg detection Detect --message-format=json (and the json-* variants, inline and space-separated) in the args and route build/check to the batch filter_cargo_build path instead of inferring json mode from output content. Makes json routing an explicit decision and removes reliance on the has_compile_errors invariant. --- src/cmds/rust/cargo_cmd.rs | 87 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 0cf5539bd6..77419b076f 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -301,7 +301,27 @@ fn run_cargo_streamed( ) } +fn has_json_message_format(args: &[String]) -> bool { + let mut iter = args.iter(); + while let Some(arg) = iter.next() { + if arg.starts_with("--message-format=") && arg.contains("json") { + return true; + } + if arg == "--message-format" { + if let Some(val) = iter.next() { + if val.contains("json") { + return true; + } + } + } + } + false +} + fn run_build(args: &[String], verbose: u8) -> Result { + if has_json_message_format(args) { + return run_cargo_filtered("build", args, verbose, filter_cargo_build); + } run_cargo_streamed( "build", args, @@ -324,6 +344,9 @@ fn run_clippy(args: &[String], verbose: u8) -> Result { } fn run_check(args: &[String], verbose: u8) -> Result { + if has_json_message_format(args) { + return run_cargo_filtered("check", args, verbose, filter_cargo_build); + } run_cargo_streamed( "check", args, @@ -2321,6 +2344,70 @@ error: aborting due to 1 previous error ); } + #[test] + fn test_json_format_inline_eq() { + assert!(has_json_message_format(&["--message-format=json".into()])); + } + + #[test] + fn test_json_format_space_separated() { + assert!(has_json_message_format(&[ + "--message-format".into(), + "json".into(), + ])); + } + + #[test] + fn test_json_format_rendered_ansi() { + assert!(has_json_message_format(&[ + "--message-format=json-diagnostic-rendered-ansi".into() + ])); + } + + #[test] + fn test_json_format_render_diagnostics() { + assert!(has_json_message_format(&[ + "--message-format=json-render-diagnostics".into() + ])); + } + + #[test] + fn test_json_format_space_rendered() { + assert!(has_json_message_format(&[ + "--message-format".into(), + "json-diagnostic-rendered-ansi".into(), + ])); + } + + #[test] + fn test_non_json_format_not_detected() { + assert!(!has_json_message_format(&["--message-format=human".into()])); + } + + #[test] + fn test_no_format_flag() { + assert!(!has_json_message_format(&[ + "--release".into(), + "-p".into(), + "my-crate".into(), + ])); + } + + #[test] + fn test_json_format_among_other_args() { + assert!(has_json_message_format(&[ + "--release".into(), + "-p".into(), + "my-crate".into(), + "--message-format=json".into(), + ])); + } + + #[test] + fn test_json_format_trailing_message_format_no_value() { + assert!(!has_json_message_format(&["--message-format".into()])); + } + #[test] fn test_filter_cargo_build_json_failure_not_compiled() { let output = concat!( From d12528055ce690bfb27aa73a259b68983b31d941 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Thu, 2 Jul 2026 01:11:07 +0200 Subject: [PATCH 12/29] test(cargo): drop flaky tee-capture test The test set a shared RTK_TEE_DIR and read back the tee file, but with the finding-1 tee call other parallel tests now write to the same dir and the max_files cleanup could evict the file before the read. The overflow line and true totals are already covered by test_filter_cargo_build_json_errors_capped, and force_tee_hint has its own tests in core/tee.rs. --- src/cmds/rust/cargo_cmd.rs | 39 -------------------------------------- 1 file changed, 39 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 77419b076f..195ebe3bb0 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -2451,45 +2451,6 @@ error: aborting due to 1 previous error ); } - #[test] - fn test_filter_cargo_build_json_errors_tee_captured() { - let tee_dir = std::env::temp_dir().join(format!("rtk-tee-json-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&tee_dir); - std::env::set_var("RTK_TEE_DIR", &tee_dir); - - let total = CAP_ERRORS + 3; - let mut output = String::new(); - for i in 0..total { - output.push_str(&format!( - r#"{{"reason":"compiler-message","message":{{"level":"error","message":"mismatched types","rendered":"error[E0308]: dropped marker {}"}}}}"#, - i - )); - output.push('\n'); - } - output.push_str(r#"{"reason":"build-finished","success":false}"#); - output.push('\n'); - - let result = filter_cargo_build(&output); - std::env::remove_var("RTK_TEE_DIR"); - - // A dropped error (past the cap) must be recoverable from the tee file, - // not silently discarded. Only assert when tee actually wrote a file. - if result.contains("[full output:") { - let captured: String = std::fs::read_dir(&tee_dir) - .into_iter() - .flatten() - .flatten() - .filter_map(|e| std::fs::read_to_string(e.path()).ok()) - .collect(); - assert!( - captured.contains(&format!("dropped marker {}", total - 1)), - "tee file must hold the dropped diagnostics: {}", - captured - ); - } - let _ = std::fs::remove_dir_all(&tee_dir); - } - #[test] fn test_filter_cargo_build_json_savings() { // Real --message-format=json lines carry a verbose envelope (spans, From 6c9a60d83e2911f7367642b2e8242d11035118e7 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Thu, 2 Jul 2026 01:18:11 +0200 Subject: [PATCH 13/29] fix(cargo): tighten json warning-summary skip to ends_with contains("generated") could swallow a real diagnostic mentioning both "warning" and "generated"; the rustc summary record ends with "generated", so match that precisely. --- src/cmds/rust/cargo_cmd.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 195ebe3bb0..a0f0af4df7 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -811,7 +811,7 @@ fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics { let text = msg["message"].as_str().unwrap_or(""); if text.starts_with("aborting due to") || text.starts_with("could not compile") - || (text.contains("warning") && text.contains("generated")) + || (text.contains("warning") && text.ends_with("generated")) { continue; } From 90b4f5f40fc561bad3616c3aefc3cb21c22c0088 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 10:56:26 +0200 Subject: [PATCH 14/29] fix(cargo): label check output as "check" instead of "build" --- src/cmds/rust/cargo_cmd.rs | 75 +++++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 9 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index a0f0af4df7..078907162e 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -39,15 +39,25 @@ struct CargoBuildHandler { warnings: usize, error_count: usize, finished_line: Option, + label: &'static str, } impl CargoBuildHandler { fn new() -> Self { + Self::with_label("build") + } + + fn for_check() -> Self { + Self::with_label("check") + } + + fn with_label(label: &'static str) -> Self { Self { compiled: 0, warnings: 0, error_count: 0, finished_line: None, + label, } } } @@ -101,6 +111,7 @@ impl BlockHandler for CargoBuildHandler { return Some(cargo_build_success_line( self.compiled, self.finished_line.as_deref(), + self.label, )); } Some(cargo_build_failure_summary( @@ -108,6 +119,7 @@ impl BlockHandler for CargoBuildHandler { errors, warnings, &json, + self.label, )) } } @@ -345,13 +357,15 @@ fn run_clippy(args: &[String], verbose: u8) -> Result { fn run_check(args: &[String], verbose: u8) -> Result { if has_json_message_format(args) { - return run_cargo_filtered("check", args, verbose, filter_cargo_build); + return run_cargo_filtered("check", args, verbose, |o| { + filter_cargo_build_labeled(o, "check") + }); } run_cargo_streamed( "check", args, verbose, - Box::new(BlockStreamFilter::new(CargoBuildHandler::new())), + Box::new(BlockStreamFilter::new(CargoBuildHandler::for_check())), ) } @@ -831,10 +845,10 @@ fn merge_diag_counts(error_count: usize, warnings: usize, json: &JsonDiagnostics ) } -fn cargo_build_success_line(compiled: usize, finished: Option<&str>) -> String { +fn cargo_build_success_line(compiled: usize, finished: Option<&str>, label: &str) -> String { match finished { - Some(f) => format!("cargo build ({} crates compiled)\n{}\n", compiled, f), - None => format!("cargo build ({} crates compiled)\n", compiled), + Some(f) => format!("cargo {} ({} crates compiled)\n{}\n", label, compiled, f), + None => format!("cargo {} ({} crates compiled)\n", label, compiled), } } @@ -843,6 +857,7 @@ fn cargo_build_failure_summary( errors: usize, warnings: usize, json: &JsonDiagnostics, + label: &str, ) -> String { let mut out = String::new(); for d in json.errors.iter().take(CAP_ERRORS) { @@ -875,13 +890,17 @@ fn cargo_build_failure_summary( } } out.push_str(&format!( - "cargo build: {} errors, {} warnings ({} crates)\n", - errors, warnings, compiled + "cargo {}: {} errors, {} warnings ({} crates)\n", + label, errors, warnings, compiled )); out } fn filter_cargo_build(output: &str) -> String { + filter_cargo_build_labeled(output, "build") +} + +fn filter_cargo_build_labeled(output: &str, label: &str) -> String { let mut handler = CargoBuildHandler::new(); let mut blocks: Vec> = Vec::new(); let mut current_block: Vec = Vec::new(); @@ -914,10 +933,10 @@ fn filter_cargo_build(output: &str) -> String { let (errors, warnings) = merge_diag_counts(handler.error_count, handler.warnings, &json); if errors == 0 && warnings == 0 { - return cargo_build_success_line(handler.compiled, handler.finished_line.as_deref()); + return cargo_build_success_line(handler.compiled, handler.finished_line.as_deref(), label); } - let mut result = cargo_build_failure_summary(handler.compiled, errors, warnings, &json); + let mut result = cargo_build_failure_summary(handler.compiled, errors, warnings, &json, label); const MAX_CHECK_BLOCKS: usize = CAP_ERRORS; for (i, blk) in blocks.iter().enumerate().take(MAX_CHECK_BLOCKS) { result.push_str(&blk.join("\n")); @@ -2303,6 +2322,44 @@ error: aborting due to 1 previous error assert!(!result.contains("crates compiled"), "got: {}", result); } + #[test] + fn test_cargo_check_stream_success_label() { + let input = " Checking demo v0.1.0 (/tmp/demo)\n Finished dev [unoptimized + debuginfo] target(s) in 0.42s\n"; + let mut f = BlockStreamFilter::new(CargoBuildHandler::for_check()); + let result = run_block_filter(&mut f, input, 0); + assert!(result.contains("cargo check"), "got: {}", result); + assert!(!result.contains("cargo build"), "got: {}", result); + } + + #[test] + fn test_cargo_check_stream_json_failure_label() { + let input = concat!( + " Checking v_cargo v0.1.0 (/tmp/v_cargo)\n", + r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"code":{"code":"E0308"},"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let mut f = BlockStreamFilter::new(CargoBuildHandler::for_check()); + let result = run_block_filter(&mut f, input, 101); + assert!(result.contains("cargo check:"), "got: {}", result); + assert!(!result.contains("cargo build:"), "got: {}", result); + assert!(result.contains("1 errors"), "got: {}", result); + } + + #[test] + fn test_filter_cargo_build_labeled_check() { + let input = concat!( + r#"{"reason":"compiler-message","message":{"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let result = filter_cargo_build_labeled(input, "check"); + assert!(result.contains("cargo check:"), "got: {}", result); + assert!(!result.contains("cargo build:"), "got: {}", result); + } + #[test] fn test_extract_json_diagnostics_strips_ansi() { let output = concat!( From f9b720c8f7e7c972739b84679d451e50ba0b6124 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 10:58:13 +0200 Subject: [PATCH 15/29] fix(cargo): surface --message-format=json test compile errors --- src/cmds/rust/cargo_cmd.rs | 61 +++++++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 078907162e..752ee0e02e 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -198,21 +198,21 @@ impl BlockHandler for CargoTestHandler { } fn format_summary(&self, _exit_code: i32, raw: &str) -> Option { - if self.summary_lines.is_empty() && self.has_compile_errors { - let build_filtered = filter_cargo_build(raw); - if build_filtered.starts_with("cargo build:") { - return Some(format!( - "{}\n", - build_filtered.replacen("cargo build:", "cargo test:", 1) - )); + if self.summary_lines.is_empty() { + let json = extract_json_diagnostics(raw); + if self.has_compile_errors || !json.errors.is_empty() { + let build_filtered = filter_cargo_build_labeled(raw, "test"); + if !build_filtered.is_empty() { + return Some(format!("{}\n", build_filtered)); + } + // Fallback: last 5 meaningful lines + let meaningful: Vec<&str> = raw + .lines() + .filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with("Compiling")) + .collect(); + let last5: Vec<&str> = meaningful.iter().rev().take(5).rev().copied().collect(); + return Some(format!("{}\n", last5.join("\n"))); } - // Fallback: last 5 meaningful lines - let meaningful: Vec<&str> = raw - .lines() - .filter(|l| !l.trim().is_empty() && !l.trim_start().starts_with("Compiling")) - .collect(); - let last5: Vec<&str> = meaningful.iter().rev().take(5).rev().copied().collect(); - return Some(format!("{}\n", last5.join("\n"))); } // No failures emitted — aggregate pass results @@ -1170,15 +1170,17 @@ pub(crate) fn filter_cargo_test(output: &str) -> String { } if result.trim().is_empty() { - let has_compile_errors = output.lines().any(|line| { - let trimmed = line.trim_start(); - trimmed.starts_with("error[") || trimmed.starts_with("error:") - }); + let json = extract_json_diagnostics(output); + let has_compile_errors = !json.errors.is_empty() + || output.lines().any(|line| { + let trimmed = line.trim_start(); + trimmed.starts_with("error[") || trimmed.starts_with("error:") + }); if has_compile_errors { - let build_filtered = filter_cargo_build(output); - if build_filtered.starts_with("cargo build:") { - return build_filtered.replacen("cargo build:", "cargo test:", 1); + let build_filtered = filter_cargo_build_labeled(output, "test"); + if !build_filtered.is_empty() { + return build_filtered; } } @@ -2629,4 +2631,21 @@ error: could not compile `rtk` (test "repro_compile_fail") due to 1 previous err assert!(result.contains("cargo test:"), "got: {}", result); assert!(result.contains("1 errors"), "got: {}", result); } + + #[test] + fn test_cargo_test_stream_json_compile_error() { + let input = concat!( + " Compiling v_cargo v0.1.0 (/tmp/v_cargo)\n", + r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"code":{"code":"E0425"},"level":"error","message":"cannot find value `missing_symbol` in this scope","rendered":"error[E0425]: cannot find value `missing_symbol` in this scope\n --> tests/repro.rs:3:13"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let mut f = BlockStreamFilter::new(CargoTestHandler::new()); + let result = run_block_filter(&mut f, input, 101); + assert!(!result.trim().is_empty(), "json compile error must not be silent"); + assert!(result.contains("cargo test:"), "got: {}", result); + assert!(result.contains("1 errors"), "got: {}", result); + assert!(result.contains("E0425"), "diagnostic must be surfaced: {}", result); + } } From 58abbf253c0fcca15ccd3d8f7441898fe72b3399 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 11:16:35 +0200 Subject: [PATCH 16/29] fix(cargo): surface --message-format=json clippy errors --- src/cmds/rust/cargo_cmd.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 752ee0e02e..e663bb8e0e 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -352,6 +352,11 @@ fn run_test(args: &[String], verbose: u8) -> Result { } fn run_clippy(args: &[String], verbose: u8) -> Result { + if has_json_message_format(args) { + return run_cargo_filtered("clippy", args, verbose, |o| { + filter_cargo_build_labeled(o, "clippy") + }); + } run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy) } @@ -2349,6 +2354,25 @@ error: aborting due to 1 previous error assert!(result.contains("1 errors"), "got: {}", result); } + #[test] + fn test_filter_cargo_build_labeled_clippy_json_not_clean() { + let input = concat!( + " Checking demo v0.1.0 (/tmp/demo)\n", + r#"{"reason":"compiler-message","message":{"code":{"code":"E0308"},"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let result = filter_cargo_build_labeled(input, "clippy"); + assert!(result.contains("cargo clippy:"), "got: {}", result); + assert!(result.contains("1 errors"), "got: {}", result); + assert!( + !result.contains("No issues found"), + "json clippy errors must not be swallowed: {}", + result + ); + } + #[test] fn test_filter_cargo_build_labeled_check() { let input = concat!( From 7fb9459fc6fe7b4d69ad8abfeb815427b615c368 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 11:19:34 +0200 Subject: [PATCH 17/29] fix(cargo): surface --message-format=json install errors --- src/cmds/rust/cargo_cmd.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index e663bb8e0e..1f30959de6 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -375,6 +375,11 @@ fn run_check(args: &[String], verbose: u8) -> Result { } fn run_install(args: &[String], verbose: u8) -> Result { + if has_json_message_format(args) { + return run_cargo_filtered("install", args, verbose, |o| { + filter_cargo_build_labeled(o, "install") + }); + } run_cargo_filtered("install", args, verbose, filter_cargo_install) } @@ -2354,6 +2359,25 @@ error: aborting due to 1 previous error assert!(result.contains("1 errors"), "got: {}", result); } + #[test] + fn test_filter_cargo_build_labeled_install_json_failure() { + let input = concat!( + " Compiling sometool v0.1.0\n", + r#"{"reason":"compiler-message","message":{"code":{"code":"E0432"},"level":"error","message":"unresolved import","rendered":"error[E0432]: unresolved import `foo`\n --> src/main.rs:1:5"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let result = filter_cargo_build_labeled(input, "install"); + assert!(result.contains("cargo install:"), "got: {}", result); + assert!(result.contains("1 errors"), "got: {}", result); + assert!( + !result.contains("crates compiled"), + "failed json install must not report success: {}", + result + ); + } + #[test] fn test_filter_cargo_build_labeled_clippy_json_not_clean() { let input = concat!( From e4bfe3593c04a497770f6f6492599ac77b3c948d Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 11:58:30 +0200 Subject: [PATCH 18/29] fix(cargo): restore failure check before trusting reused build filter The !is_empty() guard was always true (filter_cargo_build_labeled never returns empty), making the last-5-lines fallback dead code and letting a success-shaped line through on a false-positive compile-error detection. Check for the "cargo test:" failure summary instead. --- src/cmds/rust/cargo_cmd.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 1f30959de6..033686874b 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -202,7 +202,7 @@ impl BlockHandler for CargoTestHandler { let json = extract_json_diagnostics(raw); if self.has_compile_errors || !json.errors.is_empty() { let build_filtered = filter_cargo_build_labeled(raw, "test"); - if !build_filtered.is_empty() { + if build_filtered.contains("cargo test:") { return Some(format!("{}\n", build_filtered)); } // Fallback: last 5 meaningful lines @@ -1189,7 +1189,7 @@ pub(crate) fn filter_cargo_test(output: &str) -> String { if has_compile_errors { let build_filtered = filter_cargo_build_labeled(output, "test"); - if !build_filtered.is_empty() { + if build_filtered.contains("cargo test:") { return build_filtered; } } @@ -2696,4 +2696,23 @@ error: could not compile `rtk` (test "repro_compile_fail") due to 1 previous err assert!(result.contains("1 errors"), "got: {}", result); assert!(result.contains("E0425"), "diagnostic must be surfaced: {}", result); } + + #[test] + fn test_cargo_test_stream_no_diagnostic_falls_back_to_raw() { + // "could not compile" sets has_compile_errors but is skipped by the build + // re-scan, so the reused filter yields a success-shaped line. We must not + // trust it — fall back to the raw tail instead of a bogus "compiled". + let input = concat!( + " Compiling foo v0.1.0 (/tmp/foo)\n", + "error: could not compile `foo` (test \"bar\") due to previous error\n", + ); + let mut f = BlockStreamFilter::new(CargoTestHandler::new()); + let result = run_block_filter(&mut f, input, 101); + assert!( + !result.contains("crates compiled"), + "must not report a failed test run as compiled: {}", + result + ); + assert!(result.contains("could not compile"), "got: {}", result); + } } From 751bf3e76077db278a54a0bfa5177162478fe8b8 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 11:59:41 +0200 Subject: [PATCH 19/29] revert(cargo): drop --message-format=json install routing cargo install --message-format=json is effectively never used, and routing its success through the build filter dropped the "Installed " line (cargo prints it as plain text, not a json record). Keep install on its dedicated filter which preserves that information. --- src/cmds/rust/cargo_cmd.rs | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 033686874b..b613ed249a 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -375,11 +375,6 @@ fn run_check(args: &[String], verbose: u8) -> Result { } fn run_install(args: &[String], verbose: u8) -> Result { - if has_json_message_format(args) { - return run_cargo_filtered("install", args, verbose, |o| { - filter_cargo_build_labeled(o, "install") - }); - } run_cargo_filtered("install", args, verbose, filter_cargo_install) } @@ -2359,25 +2354,6 @@ error: aborting due to 1 previous error assert!(result.contains("1 errors"), "got: {}", result); } - #[test] - fn test_filter_cargo_build_labeled_install_json_failure() { - let input = concat!( - " Compiling sometool v0.1.0\n", - r#"{"reason":"compiler-message","message":{"code":{"code":"E0432"},"level":"error","message":"unresolved import","rendered":"error[E0432]: unresolved import `foo`\n --> src/main.rs:1:5"}}"#, - "\n", - r#"{"reason":"build-finished","success":false}"#, - "\n", - ); - let result = filter_cargo_build_labeled(input, "install"); - assert!(result.contains("cargo install:"), "got: {}", result); - assert!(result.contains("1 errors"), "got: {}", result); - assert!( - !result.contains("crates compiled"), - "failed json install must not report success: {}", - result - ); - } - #[test] fn test_filter_cargo_build_labeled_clippy_json_not_clean() { let input = concat!( From 25d6a09c01a6d906da49ba8c51f61ce4ee2d7785 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 12:04:22 +0200 Subject: [PATCH 20/29] fix(cargo): honor last --message-format when the flag is repeated cargo uses last-flag-wins, so --message-format=json --message-format=human builds in human format. Track the last occurrence instead of returning on the first json match. --- src/cmds/rust/cargo_cmd.rs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index b613ed249a..97bc027f5c 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -314,20 +314,18 @@ fn run_cargo_streamed( } fn has_json_message_format(args: &[String]) -> bool { + let mut json = false; let mut iter = args.iter(); while let Some(arg) = iter.next() { - if arg.starts_with("--message-format=") && arg.contains("json") { - return true; - } - if arg == "--message-format" { + if let Some(val) = arg.strip_prefix("--message-format=") { + json = val.contains("json"); + } else if arg == "--message-format" { if let Some(val) = iter.next() { - if val.contains("json") { - return true; - } + json = val.contains("json"); } } } - false + json } fn run_build(args: &[String], verbose: u8) -> Result { @@ -2491,6 +2489,18 @@ error: aborting due to 1 previous error assert!(!has_json_message_format(&["--message-format".into()])); } + #[test] + fn test_json_format_last_occurrence_wins() { + assert!(!has_json_message_format(&[ + "--message-format=json".into(), + "--message-format=human".into(), + ])); + assert!(has_json_message_format(&[ + "--message-format=human".into(), + "--message-format=json".into(), + ])); + } + #[test] fn test_filter_cargo_build_json_failure_not_compiled() { let output = concat!( From c02becb79b624fbcfde2edf8ec89e42d1db8919d Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 12:05:37 +0200 Subject: [PATCH 21/29] refactor(cargo): thread label to handler in filter_cargo_build_labeled The batch filter built the handler with a hardcoded "build" label. It is unused today (only the free summary functions receive the label) but would silently mislabel if the function ever called handler.format_summary. Pass the real label through. --- src/cmds/rust/cargo_cmd.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 97bc027f5c..9ee34d7ac0 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -903,8 +903,8 @@ fn filter_cargo_build(output: &str) -> String { filter_cargo_build_labeled(output, "build") } -fn filter_cargo_build_labeled(output: &str, label: &str) -> String { - let mut handler = CargoBuildHandler::new(); +fn filter_cargo_build_labeled(output: &str, label: &'static str) -> String { + let mut handler = CargoBuildHandler::with_label(label); let mut blocks: Vec> = Vec::new(); let mut current_block: Vec = Vec::new(); let mut in_block = false; From 59ed885752a9ba3e77ee2dd02a75000cc6c1aba9 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 12:06:59 +0200 Subject: [PATCH 22/29] fix(cargo): match the human path when skipping the json warning summary cargo emits "\`crate\` generated N warnings" which ends in "warning", so the ends_with("generated") check never matched it and the summary could inflate the count. Use contains, matching the human-text skip elsewhere in the file. --- src/cmds/rust/cargo_cmd.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 9ee34d7ac0..823bda3bb5 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -828,7 +828,7 @@ fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics { let text = msg["message"].as_str().unwrap_or(""); if text.starts_with("aborting due to") || text.starts_with("could not compile") - || (text.contains("warning") && text.ends_with("generated")) + || (text.contains("warning") && text.contains("generated")) { continue; } @@ -2412,7 +2412,7 @@ error: aborting due to 1 previous error let output = concat!( r#"{"reason":"compiler-message","message":{"level":"warning","message":"unused variable: `x`","rendered":"warning: unused variable: `x`"}}"#, "\n", - r#"{"reason":"compiler-message","message":{"level":"warning","message":"1 warning generated","rendered":"warning: 1 warning generated"}}"#, + r#"{"reason":"compiler-message","message":{"level":"warning","message":"`demo` (bin \"demo\") generated 1 warning","rendered":"warning: `demo` (bin \"demo\") generated 1 warning"}}"#, "\n", r#"{"reason":"build-finished","success":true}"#, "\n", From 1f74614ea8c148fbfe6886b445519e58d32af3ec Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 12:22:44 +0200 Subject: [PATCH 23/29] fix(cargo): keep "No issues found" wording for clean clippy json runs Routing clippy json through the build filter reported a clean run as "cargo clippy (N crates compiled)". Detect the clean case up front and emit the same "cargo clippy: No issues found" the human path uses; errors and warnings still route through the diagnostics summary. --- src/cmds/rust/cargo_cmd.rs | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 823bda3bb5..03a50b1ef0 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -351,9 +351,7 @@ fn run_test(args: &[String], verbose: u8) -> Result { fn run_clippy(args: &[String], verbose: u8) -> Result { if has_json_message_format(args) { - return run_cargo_filtered("clippy", args, verbose, |o| { - filter_cargo_build_labeled(o, "clippy") - }); + return run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy_json); } run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy) } @@ -1371,6 +1369,14 @@ fn filter_cargo_clippy(output: &str) -> String { result.trim().to_string() } +fn filter_cargo_clippy_json(output: &str) -> String { + let json = extract_json_diagnostics(output); + if json.errors.is_empty() && json.warnings.is_empty() { + return "cargo clippy: No issues found".to_string(); + } + filter_cargo_build_labeled(output, "clippy") +} + pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result { crate::core::runner::run_passthrough("cargo", args, verbose) } @@ -2371,6 +2377,32 @@ error: aborting due to 1 previous error ); } + #[test] + fn test_filter_cargo_clippy_json_clean() { + let input = concat!( + " Checking demo v0.1.0 (/tmp/demo)\n", + r#"{"reason":"compiler-artifact","package_id":"demo 0.1.0","target":{"name":"demo"}}"#, + "\n", + r#"{"reason":"build-finished","success":true}"#, + "\n", + ); + assert_eq!(filter_cargo_clippy_json(input), "cargo clippy: No issues found"); + } + + #[test] + fn test_filter_cargo_clippy_json_warnings() { + let input = concat!( + " Checking demo v0.1.0 (/tmp/demo)\n", + r#"{"reason":"compiler-message","message":{"level":"warning","message":"unused variable: `x`","rendered":"warning: unused variable: `x`\n --> src/main.rs:2:9"}}"#, + "\n", + r#"{"reason":"build-finished","success":true}"#, + "\n", + ); + let result = filter_cargo_clippy_json(input); + assert!(result.contains("unused variable"), "got: {}", result); + assert!(result.contains("cargo clippy: 0 errors, 1 warnings"), "got: {}", result); + } + #[test] fn test_filter_cargo_build_labeled_check() { let input = concat!( From 8654edb4cb7b0e4153ed0b8e27f19a5e78476ed6 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 19:28:52 +0200 Subject: [PATCH 24/29] refactor(cargo): parse json diagnostics into typed structs, count ICE Deserialize each compiler-message line into typed structs instead of indexing serde_json::Value, mirroring filter_go_test_json. Route the internal-compiler-error level into the errors bucket so an ICE is no longer dropped. --- src/cmds/rust/cargo_cmd.rs | 57 ++++++++++++++++++++++++++++---------- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 03a50b1ef0..9b9c892694 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -6,6 +6,7 @@ use crate::core::stream::{BlockHandler, BlockStreamFilter, StreamFilter}; use crate::core::truncate::{CAP_ERRORS, CAP_LIST, CAP_WARNINGS}; use crate::core::utils::{resolved_command, truncate}; use anyhow::Result; +use serde::Deserialize; use std::cmp::Ordering; use std::collections::HashMap; use std::ffi::OsString; @@ -803,6 +804,20 @@ struct JsonDiagnostics { warnings: Vec, } +#[derive(Deserialize)] +struct CargoJsonLine { + reason: String, + message: Option, +} + +#[derive(Deserialize)] +struct CargoDiagnostic { + level: String, + #[serde(default)] + message: String, + rendered: Option, +} + fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics { let mut errors = Vec::new(); let mut warnings = Vec::new(); @@ -811,29 +826,30 @@ fn extract_json_diagnostics(raw: &str) -> JsonDiagnostics { if !line.starts_with('{') { continue; } - let Ok(v) = serde_json::from_str::(line) else { + let Ok(entry) = serde_json::from_str::(line) else { continue; }; - if v["reason"].as_str() != Some("compiler-message") { + if entry.reason != "compiler-message" { continue; } - let msg = &v["message"]; - let bucket = match msg["level"].as_str() { - Some("error") => &mut errors, - Some("warning") => &mut warnings, + let Some(msg) = entry.message else { + continue; + }; + let bucket = match msg.level.as_str() { + "error" | "error: internal compiler error" => &mut errors, + "warning" => &mut warnings, _ => continue, }; - let text = msg["message"].as_str().unwrap_or(""); - if text.starts_with("aborting due to") - || text.starts_with("could not compile") - || (text.contains("warning") && text.contains("generated")) + if msg.message.starts_with("aborting due to") + || msg.message.starts_with("could not compile") + || (msg.message.contains("warning") && msg.message.contains("generated")) { continue; } - if let Some(rendered) = msg["rendered"].as_str() { - bucket.push(crate::core::utils::strip_ansi(rendered).trim_end().to_string()); - } else if !text.is_empty() { - bucket.push(text.to_string()); + if let Some(rendered) = msg.rendered { + bucket.push(crate::core::utils::strip_ansi(&rendered).trim_end().to_string()); + } else if !msg.message.is_empty() { + bucket.push(msg.message); } } JsonDiagnostics { errors, warnings } @@ -2457,6 +2473,19 @@ error: aborting due to 1 previous error ); } + #[test] + fn test_extract_json_diagnostics_counts_ice_as_error() { + let output = concat!( + r#"{"reason":"compiler-message","message":{"level":"error: internal compiler error","message":"unexpected panic","rendered":"error: internal compiler error: unexpected panic"}}"#, + "\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let json = extract_json_diagnostics(output); + assert_eq!(json.errors.len(), 1, "an ICE must count as an error"); + assert!(json.errors[0].contains("internal compiler error"), "got: {:?}", json.errors[0]); + } + #[test] fn test_json_format_inline_eq() { assert!(has_json_message_format(&["--message-format=json".into()])); From e0c5306a652d20d24840d0616a6be123078fb11a Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 19:34:04 +0200 Subject: [PATCH 25/29] refactor(cargo): put failure summary line first, reuse join_with_overflow --- src/cmds/rust/cargo_cmd.rs | 54 ++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 9b9c892694..bf601b8256 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -4,7 +4,7 @@ use crate::core::args_utils; use crate::core::runner; use crate::core::stream::{BlockHandler, BlockStreamFilter, StreamFilter}; use crate::core::truncate::{CAP_ERRORS, CAP_LIST, CAP_WARNINGS}; -use crate::core::utils::{resolved_command, truncate}; +use crate::core::utils::{join_with_overflow, resolved_command, truncate}; use anyhow::Result; use serde::Deserialize; use std::cmp::Ordering; @@ -876,23 +876,29 @@ fn cargo_build_failure_summary( json: &JsonDiagnostics, label: &str, ) -> String { - let mut out = String::new(); - for d in json.errors.iter().take(CAP_ERRORS) { - out.push_str(d); - out.push('\n'); - } - if json.errors.len() > CAP_ERRORS { - out.push_str(&format!("… +{} more errors\n", json.errors.len() - CAP_ERRORS)); - } - for d in json.warnings.iter().take(CAP_WARNINGS) { - out.push_str(d); + let mut out = format!( + "cargo {}: {} errors, {} warnings ({} crates)\n", + label, errors, warnings, compiled + ); + if !json.errors.is_empty() { + let shown: Vec = json.errors.iter().take(CAP_ERRORS).cloned().collect(); + out.push_str(&join_with_overflow( + &shown, + json.errors.len(), + CAP_ERRORS, + "errors", + )); out.push('\n'); } - if json.warnings.len() > CAP_WARNINGS { - out.push_str(&format!( - "… +{} more warnings\n", - json.warnings.len() - CAP_WARNINGS + if !json.warnings.is_empty() { + let shown: Vec = json.warnings.iter().take(CAP_WARNINGS).cloned().collect(); + out.push_str(&join_with_overflow( + &shown, + json.warnings.len(), + CAP_WARNINGS, + "warnings", )); + out.push('\n'); } if json.errors.len() > CAP_ERRORS || json.warnings.len() > CAP_WARNINGS { let full = json @@ -906,10 +912,6 @@ fn cargo_build_failure_summary( out.push_str(&format!(" {}\n", hint)); } } - out.push_str(&format!( - "cargo {}: {} errors, {} warnings ({} crates)\n", - label, errors, warnings, compiled - )); out } @@ -2419,6 +2421,20 @@ error: aborting due to 1 previous error assert!(result.contains("cargo clippy: 0 errors, 1 warnings"), "got: {}", result); } + #[test] + fn test_failure_summary_line_is_first() { + let json = JsonDiagnostics { + errors: vec!["error[E0308]: mismatched types".to_string()], + warnings: Vec::new(), + }; + let out = cargo_build_failure_summary(1, 1, 0, &json, "build"); + assert!( + out.starts_with("cargo build: 1 errors"), + "summary line must come first: {}", + out + ); + } + #[test] fn test_filter_cargo_build_labeled_check() { let input = concat!( From a3e65e99e9eb7e1151b726b836b372ab5d857893 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 19:42:26 +0200 Subject: [PATCH 26/29] fix(cargo): give the json batch filter the exit code --- src/cmds/rust/cargo_cmd.rs | 108 +++++++++++++++++++++++++++++-------- 1 file changed, 86 insertions(+), 22 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index bf601b8256..2e7049e2ae 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -121,6 +121,7 @@ impl BlockHandler for CargoBuildHandler { warnings, &json, self.label, + exit_code, )) } } @@ -202,7 +203,9 @@ impl BlockHandler for CargoTestHandler { if self.summary_lines.is_empty() { let json = extract_json_diagnostics(raw); if self.has_compile_errors || !json.errors.is_empty() { - let build_filtered = filter_cargo_build_labeled(raw, "test"); + // Content-based (exit 0): a real compile error yields "cargo test: N + // errors"; a bare "could not compile" leaves the raw tail fallback. + let build_filtered = filter_cargo_build_labeled(raw, "test", 0); if build_filtered.contains("cargo test:") { return Some(format!("{}\n", build_filtered)); } @@ -287,6 +290,38 @@ where ) } +/// Same as `run_cargo_filtered` but the filter also receives the child exit code, +/// so it can tell a genuine failure from a clean run when no diagnostics parse. +fn run_cargo_filtered_with_exit( + subcommand: &str, + args: &[String], + verbose: u8, + filter_fn: F, +) -> Result +where + F: Fn(&str, i32) -> String, +{ + let mut cmd = resolved_command("cargo"); + cmd.arg(subcommand); + + let restored_args = args_utils::restore_double_dash(args); + for arg in &restored_args { + cmd.arg(arg); + } + + if verbose > 0 { + eprintln!("Running: cargo {} {}", subcommand, restored_args.join(" ")); + } + + runner::run_filtered_with_exit( + cmd, + &format!("cargo {}", subcommand), + &restored_args.join(" "), + filter_fn, + runner::RunOptions::with_tee(&format!("cargo_{}", subcommand)), + ) +} + fn run_cargo_streamed( subcommand: &str, args: &[String], @@ -331,7 +366,9 @@ fn has_json_message_format(args: &[String]) -> bool { fn run_build(args: &[String], verbose: u8) -> Result { if has_json_message_format(args) { - return run_cargo_filtered("build", args, verbose, filter_cargo_build); + return run_cargo_filtered_with_exit("build", args, verbose, |o, exit| { + filter_cargo_build_labeled(o, "build", exit) + }); } run_cargo_streamed( "build", @@ -352,15 +389,15 @@ fn run_test(args: &[String], verbose: u8) -> Result { fn run_clippy(args: &[String], verbose: u8) -> Result { if has_json_message_format(args) { - return run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy_json); + return run_cargo_filtered_with_exit("clippy", args, verbose, filter_cargo_clippy_json); } run_cargo_filtered("clippy", args, verbose, filter_cargo_clippy) } fn run_check(args: &[String], verbose: u8) -> Result { if has_json_message_format(args) { - return run_cargo_filtered("check", args, verbose, |o| { - filter_cargo_build_labeled(o, "check") + return run_cargo_filtered_with_exit("check", args, verbose, |o, exit| { + filter_cargo_build_labeled(o, "check", exit) }); } run_cargo_streamed( @@ -875,11 +912,16 @@ fn cargo_build_failure_summary( warnings: usize, json: &JsonDiagnostics, label: &str, + exit_code: i32, ) -> String { - let mut out = format!( - "cargo {}: {} errors, {} warnings ({} crates)\n", - label, errors, warnings, compiled - ); + let mut out = if errors == 0 && warnings == 0 { + format!("cargo {}: failed (exit {})\n", label, exit_code) + } else { + format!( + "cargo {}: {} errors, {} warnings ({} crates)\n", + label, errors, warnings, compiled + ) + }; if !json.errors.is_empty() { let shown: Vec = json.errors.iter().take(CAP_ERRORS).cloned().collect(); out.push_str(&join_with_overflow( @@ -915,11 +957,12 @@ fn cargo_build_failure_summary( out } +#[cfg(test)] fn filter_cargo_build(output: &str) -> String { - filter_cargo_build_labeled(output, "build") + filter_cargo_build_labeled(output, "build", 0) } -fn filter_cargo_build_labeled(output: &str, label: &'static str) -> String { +fn filter_cargo_build_labeled(output: &str, label: &'static str, exit_code: i32) -> String { let mut handler = CargoBuildHandler::with_label(label); let mut blocks: Vec> = Vec::new(); let mut current_block: Vec = Vec::new(); @@ -951,11 +994,12 @@ fn filter_cargo_build_labeled(output: &str, label: &'static str) -> String { let json = extract_json_diagnostics(output); let (errors, warnings) = merge_diag_counts(handler.error_count, handler.warnings, &json); - if errors == 0 && warnings == 0 { + if errors == 0 && warnings == 0 && exit_code == 0 { return cargo_build_success_line(handler.compiled, handler.finished_line.as_deref(), label); } - let mut result = cargo_build_failure_summary(handler.compiled, errors, warnings, &json, label); + let mut result = + cargo_build_failure_summary(handler.compiled, errors, warnings, &json, label, exit_code); const MAX_CHECK_BLOCKS: usize = CAP_ERRORS; for (i, blk) in blocks.iter().enumerate().take(MAX_CHECK_BLOCKS) { result.push_str(&blk.join("\n")); @@ -1197,7 +1241,7 @@ pub(crate) fn filter_cargo_test(output: &str) -> String { }); if has_compile_errors { - let build_filtered = filter_cargo_build_labeled(output, "test"); + let build_filtered = filter_cargo_build_labeled(output, "test", 0); if build_filtered.contains("cargo test:") { return build_filtered; } @@ -1387,12 +1431,12 @@ fn filter_cargo_clippy(output: &str) -> String { result.trim().to_string() } -fn filter_cargo_clippy_json(output: &str) -> String { +fn filter_cargo_clippy_json(output: &str, exit_code: i32) -> String { let json = extract_json_diagnostics(output); - if json.errors.is_empty() && json.warnings.is_empty() { + if json.errors.is_empty() && json.warnings.is_empty() && exit_code == 0 { return "cargo clippy: No issues found".to_string(); } - filter_cargo_build_labeled(output, "clippy") + filter_cargo_build_labeled(output, "clippy", exit_code) } pub fn run_passthrough(args: &[OsString], verbose: u8) -> Result { @@ -2385,7 +2429,7 @@ error: aborting due to 1 previous error r#"{"reason":"build-finished","success":false}"#, "\n", ); - let result = filter_cargo_build_labeled(input, "clippy"); + let result = filter_cargo_build_labeled(input, "clippy", 1); assert!(result.contains("cargo clippy:"), "got: {}", result); assert!(result.contains("1 errors"), "got: {}", result); assert!( @@ -2404,7 +2448,10 @@ error: aborting due to 1 previous error r#"{"reason":"build-finished","success":true}"#, "\n", ); - assert_eq!(filter_cargo_clippy_json(input), "cargo clippy: No issues found"); + assert_eq!( + filter_cargo_clippy_json(input, 0), + "cargo clippy: No issues found" + ); } #[test] @@ -2416,18 +2463,35 @@ error: aborting due to 1 previous error r#"{"reason":"build-finished","success":true}"#, "\n", ); - let result = filter_cargo_clippy_json(input); + let result = filter_cargo_clippy_json(input, 0); assert!(result.contains("unused variable"), "got: {}", result); assert!(result.contains("cargo clippy: 0 errors, 1 warnings"), "got: {}", result); } + #[test] + fn test_batch_filter_nonzero_exit_without_diagnostics_is_not_compiled() { + // build-script failure: exit 101, no compiler-message diagnostics parsed. + let input = concat!( + " Compiling demo v0.1.0 (/tmp/demo)\n", + r#"{"reason":"build-finished","success":false}"#, + "\n", + ); + let result = filter_cargo_build_labeled(input, "build", 101); + assert!( + !result.contains("crates compiled"), + "a failed build must not be reported as compiled: {}", + result + ); + assert!(result.contains("cargo build: failed (exit 101)"), "got: {}", result); + } + #[test] fn test_failure_summary_line_is_first() { let json = JsonDiagnostics { errors: vec!["error[E0308]: mismatched types".to_string()], warnings: Vec::new(), }; - let out = cargo_build_failure_summary(1, 1, 0, &json, "build"); + let out = cargo_build_failure_summary(1, 1, 0, &json, "build", 101); assert!( out.starts_with("cargo build: 1 errors"), "summary line must come first: {}", @@ -2443,7 +2507,7 @@ error: aborting due to 1 previous error r#"{"reason":"build-finished","success":false}"#, "\n", ); - let result = filter_cargo_build_labeled(input, "check"); + let result = filter_cargo_build_labeled(input, "check", 1); assert!(result.contains("cargo check:"), "got: {}", result); assert!(!result.contains("cargo build:"), "got: {}", result); } From 39a4d28d21ca060fc6cd7930d86a5a29508da8b3 Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 19:44:45 +0200 Subject: [PATCH 27/29] refactor(cargo): drop dead json parse from the streamed build handler --- src/cmds/rust/cargo_cmd.rs | 60 +++++++++----------------------------- 1 file changed, 13 insertions(+), 47 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 2e7049e2ae..5768681192 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -104,22 +104,25 @@ impl BlockHandler for CargoBuildHandler { !(line.trim().is_empty() && block.len() > 3) } - fn format_summary(&self, exit_code: i32, raw: &str) -> Option { - let json = extract_json_diagnostics(raw); - let (errors, warnings) = merge_diag_counts(self.error_count, self.warnings, &json); - - if errors == 0 && warnings == 0 && exit_code == 0 { + fn format_summary(&self, exit_code: i32, _raw: &str) -> Option { + if self.error_count == 0 && self.warnings == 0 && exit_code == 0 { return Some(cargo_build_success_line( self.compiled, self.finished_line.as_deref(), self.label, )); } + // The streamed path only runs for non-json build/check; error blocks are + // emitted live, so the summary carries no rendered diagnostics. + let empty = JsonDiagnostics { + errors: Vec::new(), + warnings: Vec::new(), + }; Some(cargo_build_failure_summary( self.compiled, - errors, - warnings, - &json, + self.error_count, + self.warnings, + &empty, self.label, exit_code, )) @@ -2360,27 +2363,7 @@ error: aborting due to 1 previous error } #[test] - fn test_cargo_build_stream_json_failure_not_compiled() { - let input = concat!( - " Compiling v_cargo v0.1.0 (/tmp/v_cargo)\n", - r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"code":{"code":"E0308"},"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18"}}"#, - "\n", - r#"{"reason":"build-finished","success":false}"#, - "\n", - ); - let mut f = BlockStreamFilter::new(CargoBuildHandler::new()); - let result = run_block_filter(&mut f, input, 101); - assert!(result.contains("E0308"), "got: {}", result); - assert!(result.contains("1 errors"), "got: {}", result); - assert!( - !result.contains("crates compiled"), - "failed json build must not be reported as compiled: {}", - result - ); - } - - #[test] - fn test_cargo_build_stream_json_warning_rendered() { + fn test_filter_cargo_build_json_warning_rendered() { let input = concat!( " Compiling v_cargo v0.1.0 (/tmp/v_cargo)\n", r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"level":"warning","message":"unused variable: `x`","rendered":"warning: unused variable: `x`\n --> src/main.rs:2:9"}}"#, @@ -2388,8 +2371,7 @@ error: aborting due to 1 previous error r#"{"reason":"build-finished","success":true}"#, "\n", ); - let mut f = BlockStreamFilter::new(CargoBuildHandler::new()); - let result = run_block_filter(&mut f, input, 0); + let result = filter_cargo_build_labeled(input, "build", 0); assert!(result.contains("unused variable"), "got: {}", result); assert!(result.contains("1 warnings"), "got: {}", result); assert!(!result.contains("crates compiled"), "got: {}", result); @@ -2404,22 +2386,6 @@ error: aborting due to 1 previous error assert!(!result.contains("cargo build"), "got: {}", result); } - #[test] - fn test_cargo_check_stream_json_failure_label() { - let input = concat!( - " Checking v_cargo v0.1.0 (/tmp/v_cargo)\n", - r#"{"reason":"compiler-message","package_id":"v_cargo 0.1.0","message":{"code":{"code":"E0308"},"level":"error","message":"mismatched types","rendered":"error[E0308]: mismatched types\n --> src/main.rs:2:18"}}"#, - "\n", - r#"{"reason":"build-finished","success":false}"#, - "\n", - ); - let mut f = BlockStreamFilter::new(CargoBuildHandler::for_check()); - let result = run_block_filter(&mut f, input, 101); - assert!(result.contains("cargo check:"), "got: {}", result); - assert!(!result.contains("cargo build:"), "got: {}", result); - assert!(result.contains("1 errors"), "got: {}", result); - } - #[test] fn test_filter_cargo_build_labeled_clippy_json_not_clean() { let input = concat!( From 6f67c9e6af157194f0e472e7705d7998c341c08b Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 19:45:49 +0200 Subject: [PATCH 28/29] refactor(cargo): drop CargoBuildHandler new/for_check wrappers --- src/cmds/rust/cargo_cmd.rs | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 5768681192..45437314fd 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -44,14 +44,6 @@ struct CargoBuildHandler { } impl CargoBuildHandler { - fn new() -> Self { - Self::with_label("build") - } - - fn for_check() -> Self { - Self::with_label("check") - } - fn with_label(label: &'static str) -> Self { Self { compiled: 0, @@ -377,7 +369,7 @@ fn run_build(args: &[String], verbose: u8) -> Result { "build", args, verbose, - Box::new(BlockStreamFilter::new(CargoBuildHandler::new())), + Box::new(BlockStreamFilter::new(CargoBuildHandler::with_label("build"))), ) } @@ -407,7 +399,7 @@ fn run_check(args: &[String], verbose: u8) -> Result { "check", args, verbose, - Box::new(BlockStreamFilter::new(CargoBuildHandler::for_check())), + Box::new(BlockStreamFilter::new(CargoBuildHandler::with_label("check"))), ) } @@ -2318,7 +2310,7 @@ error: test run failed #[test] fn test_cargo_build_stream_success() { let input = " Compiling libc v0.2.153\n Compiling cfg-if v1.0.0\n Compiling rtk v0.5.0\n Finished dev [unoptimized + debuginfo] target(s) in 15.23s\n"; - let mut f = BlockStreamFilter::new(CargoBuildHandler::new()); + let mut f = BlockStreamFilter::new(CargoBuildHandler::with_label("build")); let result = run_block_filter(&mut f, input, 0); assert!(result.contains("3 crates compiled"), "got: {}", result); assert!(result.contains("Finished"), "got: {}", result); @@ -2335,7 +2327,7 @@ error: test run failed r#"{"reason":"build-finished","success":true}"#, "\n", ); - let mut f = BlockStreamFilter::new(CargoBuildHandler::new()); + let mut f = BlockStreamFilter::new(CargoBuildHandler::with_label("build")); let result = run_block_filter(&mut f, input, 0); assert!(result.contains("1 crates compiled"), "got: {}", result); assert!(result.contains("Finished"), "got: {}", result); @@ -2354,7 +2346,7 @@ error[E0308]: mismatched types error: aborting due to 1 previous error "#; - let mut f = BlockStreamFilter::new(CargoBuildHandler::new()); + let mut f = BlockStreamFilter::new(CargoBuildHandler::with_label("build")); let result = run_block_filter(&mut f, input, 1); assert!(result.contains("E0308"), "got: {}", result); assert!(result.contains("mismatched types"), "got: {}", result); @@ -2380,7 +2372,7 @@ error: aborting due to 1 previous error #[test] fn test_cargo_check_stream_success_label() { let input = " Checking demo v0.1.0 (/tmp/demo)\n Finished dev [unoptimized + debuginfo] target(s) in 0.42s\n"; - let mut f = BlockStreamFilter::new(CargoBuildHandler::for_check()); + let mut f = BlockStreamFilter::new(CargoBuildHandler::with_label("check")); let result = run_block_filter(&mut f, input, 0); assert!(result.contains("cargo check"), "got: {}", result); assert!(!result.contains("cargo build"), "got: {}", result); From 1143e6fb6353402f1a864ffb4f2463a2291ddd2a Mon Sep 17 00:00:00 2001 From: Luccin Masirika Date: Fri, 3 Jul 2026 19:48:56 +0200 Subject: [PATCH 29/29] docs(cargo): explain why run_test has no json branch --- src/cmds/rust/cargo_cmd.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cmds/rust/cargo_cmd.rs b/src/cmds/rust/cargo_cmd.rs index 45437314fd..7dc2a49785 100644 --- a/src/cmds/rust/cargo_cmd.rs +++ b/src/cmds/rust/cargo_cmd.rs @@ -374,6 +374,10 @@ fn run_build(args: &[String], verbose: u8) -> Result { } fn run_test(args: &[String], verbose: u8) -> Result { + // No json branch here on purpose: --message-format=json only reformats the + // build phase, the test harness output stays human-readable. CargoTestHandler + // reads both — it aggregates the `test result:` lines and, on a compile error, + // parses the json diagnostics in format_summary. run_cargo_streamed( "test", args,