Skip to content

Commit ff13986

Browse files
CryptoKradclaude
authored andcommitted
fix(cargo): preserve compiler warnings in cargo test output on passing runs
What: CargoTestHandler (streaming) and filter_cargo_test (buffered/pipe path) previously dropped compile-phase 'warning:' blocks entirely on passing runs. Both now capture and emit warning blocks (mirroring CargoBuildHandler) and annotate the compact summary with '[N compiler warnings]'. The aggregate 'generated N warnings' count line stays stripped. Buffered path caps blocks at CAP_WARNINGS with an '+N more' marker and hard-terminates warning blocks on section markers (failures:/test result:/test /----/error) so short blocks cannot swallow subsequent sections. Why: exit-0 runs get no tee log, so dropped warnings were unrecoverable — an agent reading 'cargo test: N passed' concluded the build was clean when rustc had emitted warnings. Confirmed behaviorally pre-fix: raw 'cargo test' showed 3 warning lines; rtk showed none (review lane finding HIGH-2, docs/research/runs/rtk-review-2026-07-07 in Claude.md-improve). Verified: cargo test full suite 2394 passed / 0 failed (+ 6 integration suites green); 2 new tests (buffered + streamed handler) assert warning survival, warning-count annotation, and aggregate-line stripping; behavioral A/B on a real crate: passing-with-warnings now shows the block + count (exit 0), failing run still shows failure detail with exit 101 == raw. Open risk: pytest wrapper has the analogous success-path loss (warnings summary + stderr) — tracked as a follow-up, mitigated operationally by tee.mode=always in local config. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5bb4GKf9CNKP9uYhFDvsz
1 parent 95b24b7 commit ff13986

1 file changed

Lines changed: 183 additions & 3 deletions

File tree

‎src/cmds/rust/cargo_cmd.rs‎

Lines changed: 183 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ struct CargoTestHandler {
126126
in_failure_names: bool,
127127
summary_lines: Vec<String>,
128128
has_compile_errors: bool,
129+
warnings: usize,
130+
in_warning_block: bool,
129131
}
130132

131133
impl CargoTestHandler {
@@ -135,6 +137,8 @@ impl CargoTestHandler {
135137
in_failure_names: false,
136138
summary_lines: Vec::new(),
137139
has_compile_errors: false,
140+
warnings: 0,
141+
in_warning_block: false,
138142
}
139143
}
140144
}
@@ -155,6 +159,11 @@ impl BlockHandler for CargoTestHandler {
155159
if line.starts_with("test ") && line.ends_with("... ok") {
156160
return true;
157161
}
162+
// Aggregate count line ("warning: `x` generated N warnings"); individual
163+
// warning blocks are already streamed via is_block_start below.
164+
if line.starts_with("warning:") && line.contains("generated") && line.contains("warning") {
165+
return true;
166+
}
158167
// Track compile errors for fallback
159168
if trimmed.starts_with("error[") || trimmed.starts_with("error:") {
160169
self.has_compile_errors = true;
@@ -187,10 +196,26 @@ impl BlockHandler for CargoTestHandler {
187196
}
188197

189198
fn is_block_start(&mut self, line: &str) -> bool {
190-
self.in_failure_section && line.starts_with("---- ")
199+
if self.in_failure_section && line.starts_with("---- ") {
200+
self.in_warning_block = false;
201+
return true;
202+
}
203+
// Compile-phase warnings on a passing run were previously dropped
204+
// (exit 0 → no tee → unrecoverable); stream them like CargoBuildHandler.
205+
if !self.in_failure_section
206+
&& (line.starts_with("warning:") || line.starts_with("warning["))
207+
{
208+
self.warnings += 1;
209+
self.in_warning_block = true;
210+
return true;
211+
}
212+
false
191213
}
192214

193-
fn is_block_continuation(&mut self, line: &str, _block: &[String]) -> bool {
215+
fn is_block_continuation(&mut self, line: &str, block: &[String]) -> bool {
216+
if self.in_warning_block {
217+
return !(line.trim().is_empty() && block.len() > 3);
218+
}
194219
self.in_failure_section && !line.starts_with("---- ")
195220
}
196221

@@ -234,6 +259,13 @@ impl BlockHandler for CargoTestHandler {
234259
if all_parsed {
235260
if let Some(agg) = aggregated {
236261
if agg.suites > 0 {
262+
if self.warnings > 0 {
263+
return Some(format!(
264+
"{} [{} compiler warnings]\n",
265+
agg.format_compact(),
266+
self.warnings
267+
));
268+
}
237269
return Some(format!("{}\n", agg.format_compact()));
238270
}
239271
}
@@ -1120,6 +1152,9 @@ pub(crate) fn filter_cargo_test(output: &str) -> String {
11201152
let mut summary_lines: Vec<String> = Vec::new();
11211153
let mut in_failure_section = false;
11221154
let mut current_failure = Vec::new();
1155+
let mut warnings: Vec<String> = Vec::new();
1156+
let mut current_warning: Vec<String> = Vec::new();
1157+
let mut in_warning_block = false;
11231158

11241159
for line in output.lines() {
11251160
// Skip compilation lines
@@ -1136,6 +1171,48 @@ pub(crate) fn filter_cargo_test(output: &str) -> String {
11361171
continue;
11371172
}
11381173

1174+
// Compile-phase warnings on a passing run were previously dropped
1175+
// (exit 0 → no tee → unrecoverable). Capture the blocks; skip the
1176+
// aggregate "warning: `x` generated N warnings" count line.
1177+
if line.starts_with("warning:") && line.contains("generated") && line.contains("warning") {
1178+
in_warning_block = false;
1179+
if !current_warning.is_empty() {
1180+
warnings.push(current_warning.join("\n"));
1181+
current_warning.clear();
1182+
}
1183+
continue;
1184+
}
1185+
if !in_failure_section && (line.starts_with("warning:") || line.starts_with("warning[")) {
1186+
if !current_warning.is_empty() {
1187+
warnings.push(current_warning.join("\n"));
1188+
current_warning.clear();
1189+
}
1190+
in_warning_block = true;
1191+
current_warning.push(line.to_string());
1192+
continue;
1193+
}
1194+
if in_warning_block {
1195+
// A section marker always ends the warning block (falls through to
1196+
// normal handling); a blank line ends a well-formed block.
1197+
let is_section = line == "failures:"
1198+
|| line.starts_with("test result:")
1199+
|| line.starts_with("test ")
1200+
|| line.starts_with("---- ")
1201+
|| line.starts_with("error[")
1202+
|| line.starts_with("error:");
1203+
if is_section || (line.trim().is_empty() && current_warning.len() > 3) {
1204+
in_warning_block = false;
1205+
warnings.push(current_warning.join("\n"));
1206+
current_warning.clear();
1207+
if !is_section {
1208+
continue;
1209+
}
1210+
} else {
1211+
current_warning.push(line.to_string());
1212+
continue;
1213+
}
1214+
}
1215+
11391216
// Detect failures section
11401217
if line == "failures:" {
11411218
in_failure_section = true;
@@ -1165,8 +1242,24 @@ pub(crate) fn filter_cargo_test(output: &str) -> String {
11651242
if !current_failure.is_empty() {
11661243
failures.push(current_failure.join("\n"));
11671244
}
1245+
if !current_warning.is_empty() {
1246+
warnings.push(current_warning.join("\n"));
1247+
}
1248+
1249+
let mut warnings_section = String::new();
1250+
if !warnings.is_empty() {
1251+
for block in warnings.iter().take(CAP_WARNINGS) {
1252+
warnings_section.push_str(block);
1253+
warnings_section.push('\n');
1254+
}
1255+
if warnings.len() > CAP_WARNINGS {
1256+
warnings_section
1257+
.push_str(&format!("… +{} more warnings\n", warnings.len() - CAP_WARNINGS));
1258+
}
1259+
}
11681260

11691261
let mut result = String::new();
1262+
result.push_str(&warnings_section);
11701263

11711264
if failures.is_empty() && !summary_lines.is_empty() {
11721265
// All passed - try to aggregate
@@ -1190,7 +1283,15 @@ pub(crate) fn filter_cargo_test(output: &str) -> String {
11901283
if all_parsed {
11911284
if let Some(agg) = aggregated {
11921285
if agg.suites > 0 {
1193-
return agg.format_compact();
1286+
if warnings.is_empty() {
1287+
return agg.format_compact();
1288+
}
1289+
return format!(
1290+
"{}{} [{} compiler warnings]",
1291+
warnings_section,
1292+
agg.format_compact(),
1293+
warnings.len()
1294+
);
11941295
}
11951296
}
11961297
}
@@ -1597,6 +1698,85 @@ test result: ok. 15 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; fin
15971698
assert!(!result.contains("test utils"));
15981699
}
15991700

1701+
#[test]
1702+
fn test_filter_cargo_test_all_pass_preserves_compiler_warnings() {
1703+
let output = r#" Compiling failproj v0.1.0
1704+
warning: function `unused_helper` is never used
1705+
--> src/lib.rs:9:4
1706+
|
1707+
9 | fn unused_helper() -> i32 { 42 }
1708+
| ^^^^^^^^^^^^^
1709+
|
1710+
= note: `#[warn(dead_code)]` (part of `#[warn(unused)]`) on by default
1711+
1712+
warning: `failproj` (lib) generated 1 warning
1713+
Finished test [unoptimized + debuginfo] target(s) in 0.05s
1714+
Running unittests src/lib.rs (target/debug/deps/failproj-abc123)
1715+
1716+
running 3 tests
1717+
test tests::test_pass ... ok
1718+
test tests::test_pass2 ... ok
1719+
test tests::test_pass3 ... ok
1720+
1721+
test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
1722+
"#;
1723+
let result = filter_cargo_test(output);
1724+
assert!(
1725+
result.contains("unused_helper"),
1726+
"compiler warning block must survive a passing run, got: {}",
1727+
result
1728+
);
1729+
assert!(
1730+
result.contains("[1 compiler warnings]"),
1731+
"summary must carry the warning count, got: {}",
1732+
result
1733+
);
1734+
assert!(result.contains("3 passed"));
1735+
// Aggregate count line stays stripped
1736+
assert!(!result.contains("generated 1 warning"));
1737+
}
1738+
1739+
#[test]
1740+
fn test_streamed_cargo_test_handler_preserves_compiler_warnings() {
1741+
use crate::core::stream::{BlockStreamFilter, StreamFilter};
1742+
let lines = [
1743+
" Compiling failproj v0.1.0",
1744+
"warning: function `unused_helper` is never used",
1745+
" --> src/lib.rs:9:4",
1746+
" |",
1747+
"9 | fn unused_helper() -> i32 { 42 }",
1748+
" | ^^^^^^^^^^^^^",
1749+
"",
1750+
"warning: `failproj` (lib) generated 1 warning",
1751+
" Finished test [unoptimized + debuginfo] target(s) in 0.05s",
1752+
"",
1753+
"running 3 tests",
1754+
"test tests::test_pass ... ok",
1755+
"",
1756+
"test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s",
1757+
];
1758+
let mut filter = BlockStreamFilter::new(CargoTestHandler::new());
1759+
let mut emitted = String::new();
1760+
for line in lines {
1761+
if let Some(out) = filter.feed_line(line) {
1762+
emitted.push_str(&out);
1763+
}
1764+
}
1765+
emitted.push_str(&filter.flush());
1766+
let summary = filter.on_exit(0, "").unwrap_or_default();
1767+
assert!(
1768+
emitted.contains("unused_helper"),
1769+
"warning block must stream through, got: {}",
1770+
emitted
1771+
);
1772+
assert!(
1773+
summary.contains("[1 compiler warnings]"),
1774+
"summary must carry warning count, got: {}",
1775+
summary
1776+
);
1777+
assert!(summary.contains("3 passed"));
1778+
}
1779+
16001780
#[test]
16011781
fn test_filter_cargo_test_failures() {
16021782
let output = r#"running 5 tests

0 commit comments

Comments
 (0)