@@ -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
131133impl 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