@@ -84,15 +84,31 @@ pub mod xtask_port {
8484 use super :: * ;
8585
8686 /// Run comprehensive coverage analysis (ported from xtask coverage)
87- pub fn run_coverage_analysis ( config : & BuildConfig ) -> BuildResult < ( ) > {
87+ pub fn run_coverage_analysis ( config : & BuildConfig , html : bool ) -> BuildResult < ( ) > {
8888 println ! (
8989 "{} Running comprehensive coverage analysis..." ,
9090 "📊" . bright_blue( )
9191 ) ;
9292
93- // Build with coverage flags
93+ let coverage_dir = PathBuf :: from ( "target/coverage" ) ;
94+ // Ensure coverage directory exists
95+ std:: fs:: create_dir_all ( & coverage_dir) . map_err ( |e| {
96+ BuildError :: Build ( format ! ( "Failed to create coverage directory: {}" , e) )
97+ } ) ?;
98+
99+ // Clean old profraw files
100+ if let Ok ( entries) = std:: fs:: read_dir ( & coverage_dir) {
101+ for entry in entries. flatten ( ) {
102+ let path = entry. path ( ) ;
103+ if path. extension ( ) . and_then ( |e| e. to_str ( ) ) == Some ( "profraw" ) {
104+ let _ = std:: fs:: remove_file ( & path) ;
105+ }
106+ }
107+ }
108+
109+ // Build with coverage flags and capture test binary paths
94110 let mut cmd = Command :: new ( "cargo" ) ;
95- cmd. args ( [ "test" , "--no-run" , "--workspace" ] )
111+ cmd. args ( [ "test" , "--no-run" , "--workspace" , "--message-format=json" ] )
96112 . env ( "RUSTFLAGS" , "-C instrument-coverage" )
97113 . env ( "LLVM_PROFILE_FILE" , "target/coverage/profile-%p-%m.profraw" ) ;
98114
@@ -102,6 +118,31 @@ pub mod xtask_port {
102118 return Err ( BuildError :: Build ( "Coverage build failed" . to_string ( ) ) ) ;
103119 }
104120
121+ // Extract test binary paths from cargo's JSON output
122+ let stdout = String :: from_utf8_lossy ( & output. stdout ) ;
123+ let test_binaries: Vec < String > = stdout
124+ . lines ( )
125+ . filter_map ( |line| {
126+ // Parse JSON lines looking for compiler artifacts with executables
127+ if let Ok ( json) = serde_json:: from_str :: < serde_json:: Value > ( line) {
128+ if json. get ( "reason" ) . and_then ( |r| r. as_str ( ) ) == Some ( "compiler-artifact" ) {
129+ if let Some ( executable) = json. get ( "executable" ) . and_then ( |e| e. as_str ( ) ) {
130+ return Some ( executable. to_string ( ) ) ;
131+ }
132+ }
133+ }
134+ None
135+ } )
136+ . collect ( ) ;
137+
138+ if config. verbose {
139+ println ! (
140+ "{} Found {} test binaries" ,
141+ "ℹ️" . bright_blue( ) ,
142+ test_binaries. len( )
143+ ) ;
144+ }
145+
105146 // Run tests with coverage
106147 let mut test_cmd = Command :: new ( "cargo" ) ;
107148 test_cmd
@@ -116,10 +157,233 @@ pub mod xtask_port {
116157 return Err ( BuildError :: Test ( "Coverage tests failed" . to_string ( ) ) ) ;
117158 }
118159
160+ // Find llvm tools
161+ let llvm_profdata = find_llvm_tool ( "llvm-profdata" ) ?;
162+ let llvm_cov = find_llvm_tool ( "llvm-cov" ) ?;
163+
164+ if config. verbose {
165+ println ! (
166+ "{} Using llvm-profdata: {}" ,
167+ "ℹ️" . bright_blue( ) ,
168+ llvm_profdata. display( )
169+ ) ;
170+ println ! (
171+ "{} Using llvm-cov: {}" ,
172+ "ℹ️" . bright_blue( ) ,
173+ llvm_cov. display( )
174+ ) ;
175+ }
176+
177+ // Collect profraw files
178+ let profraw_files: Vec < PathBuf > = std:: fs:: read_dir ( & coverage_dir)
179+ . map_err ( |e| {
180+ BuildError :: Build ( format ! ( "Failed to read coverage directory: {}" , e) )
181+ } ) ?
182+ . filter_map ( |entry| {
183+ let entry = entry. ok ( ) ?;
184+ let path = entry. path ( ) ;
185+ if path. extension ( ) . and_then ( |e| e. to_str ( ) ) == Some ( "profraw" ) {
186+ Some ( path)
187+ } else {
188+ None
189+ }
190+ } )
191+ . collect ( ) ;
192+
193+ if profraw_files. is_empty ( ) {
194+ return Err ( BuildError :: Build (
195+ "No .profraw files found. Tests may not have generated coverage data." . to_string ( ) ,
196+ ) ) ;
197+ }
198+
199+ println ! (
200+ "{} Merging {} profile data files..." ,
201+ "📊" . bright_blue( ) ,
202+ profraw_files. len( )
203+ ) ;
204+
205+ // Merge profraw files into a single profdata file
206+ let profdata_path = coverage_dir. join ( "coverage.profdata" ) ;
207+ let mut merge_cmd = Command :: new ( & llvm_profdata) ;
208+ merge_cmd. arg ( "merge" ) . arg ( "-sparse" ) ;
209+ for profraw in & profraw_files {
210+ merge_cmd. arg ( profraw) ;
211+ }
212+ merge_cmd. arg ( "-o" ) . arg ( & profdata_path) ;
213+
214+ let merge_output =
215+ super :: execute_command ( & mut merge_cmd, config, "Merging profile data" ) ?;
216+
217+ if !merge_output. status . success ( ) {
218+ let stderr = String :: from_utf8_lossy ( & merge_output. stderr ) ;
219+ return Err ( BuildError :: Build ( format ! (
220+ "Failed to merge profile data: {}" ,
221+ stderr
222+ ) ) ) ;
223+ }
224+
225+ // Generate lcov report
226+ let lcov_path = coverage_dir. join ( "lcov.info" ) ;
227+ println ! (
228+ "{} Generating lcov report..." ,
229+ "📊" . bright_blue( )
230+ ) ;
231+
232+ let mut lcov_cmd = Command :: new ( & llvm_cov) ;
233+ lcov_cmd
234+ . arg ( "export" )
235+ . arg ( "--format=lcov" )
236+ . arg ( format ! ( "--instr-profile={}" , profdata_path. display( ) ) )
237+ . arg ( "--ignore-filename-regex=\\ .cargo|rustc|target" ) ;
238+ // Add test binaries as objects
239+ if let Some ( first) = test_binaries. first ( ) {
240+ lcov_cmd. arg ( first) ;
241+ for binary in test_binaries. iter ( ) . skip ( 1 ) {
242+ lcov_cmd. arg ( format ! ( "--object={}" , binary) ) ;
243+ }
244+ }
245+
246+ let lcov_output =
247+ super :: execute_command ( & mut lcov_cmd, config, "Generating lcov report" ) ?;
248+
249+ if !lcov_output. status . success ( ) {
250+ let stderr = String :: from_utf8_lossy ( & lcov_output. stderr ) ;
251+ return Err ( BuildError :: Build ( format ! (
252+ "Failed to generate lcov report: {}" ,
253+ stderr
254+ ) ) ) ;
255+ }
256+
257+ // Write lcov data to file
258+ std:: fs:: write ( & lcov_path, & lcov_output. stdout ) . map_err ( |e| {
259+ BuildError :: Build ( format ! ( "Failed to write lcov report: {}" , e) )
260+ } ) ?;
261+
262+ println ! (
263+ "{} lcov report written to {}" ,
264+ "✅" . bright_green( ) ,
265+ lcov_path. display( )
266+ ) ;
267+
268+ // Generate HTML report if requested
269+ if html {
270+ let html_dir = coverage_dir. join ( "html" ) ;
271+ std:: fs:: create_dir_all ( & html_dir) . map_err ( |e| {
272+ BuildError :: Build ( format ! ( "Failed to create HTML output directory: {}" , e) )
273+ } ) ?;
274+
275+ println ! (
276+ "{} Generating HTML coverage report..." ,
277+ "📊" . bright_blue( )
278+ ) ;
279+
280+ let mut html_cmd = Command :: new ( & llvm_cov) ;
281+ html_cmd
282+ . arg ( "show" )
283+ . arg ( "--format=html" )
284+ . arg ( format ! ( "--instr-profile={}" , profdata_path. display( ) ) )
285+ . arg ( format ! ( "--output-dir={}" , html_dir. display( ) ) )
286+ . arg ( "--ignore-filename-regex=\\ .cargo|rustc|target" )
287+ . arg ( "--show-line-counts-or-regions" )
288+ . arg ( "--show-instantiations" ) ;
289+ // Add test binaries as objects
290+ if let Some ( first) = test_binaries. first ( ) {
291+ html_cmd. arg ( first) ;
292+ for binary in test_binaries. iter ( ) . skip ( 1 ) {
293+ html_cmd. arg ( format ! ( "--object={}" , binary) ) ;
294+ }
295+ }
296+
297+ let html_output =
298+ super :: execute_command ( & mut html_cmd, config, "Generating HTML report" ) ?;
299+
300+ if !html_output. status . success ( ) {
301+ let stderr = String :: from_utf8_lossy ( & html_output. stderr ) ;
302+ return Err ( BuildError :: Build ( format ! (
303+ "Failed to generate HTML coverage report: {}" ,
304+ stderr
305+ ) ) ) ;
306+ }
307+
308+ println ! (
309+ "{} HTML coverage report written to {}" ,
310+ "✅" . bright_green( ) ,
311+ html_dir. display( )
312+ ) ;
313+ }
314+
119315 println ! ( "{} Coverage analysis completed" , "✅" . bright_green( ) ) ;
120316 Ok ( ( ) )
121317 }
122318
319+ /// Find an LLVM tool binary from the Rust toolchain
320+ fn find_llvm_tool ( tool_name : & str ) -> BuildResult < PathBuf > {
321+ // Get the sysroot
322+ let sysroot_output = Command :: new ( "rustc" )
323+ . arg ( "--print" )
324+ . arg ( "sysroot" )
325+ . output ( )
326+ . map_err ( |e| BuildError :: Tool ( format ! ( "Failed to run rustc --print sysroot: {}" , e) ) ) ?;
327+
328+ if !sysroot_output. status . success ( ) {
329+ return Err ( BuildError :: Tool (
330+ "Failed to determine Rust sysroot" . to_string ( ) ,
331+ ) ) ;
332+ }
333+
334+ let sysroot = String :: from_utf8_lossy ( & sysroot_output. stdout )
335+ . trim ( )
336+ . to_string ( ) ;
337+
338+ // Get the host triple
339+ let version_output = Command :: new ( "rustc" )
340+ . arg ( "-vV" )
341+ . output ( )
342+ . map_err ( |e| BuildError :: Tool ( format ! ( "Failed to run rustc -vV: {}" , e) ) ) ?;
343+
344+ let version_str = String :: from_utf8_lossy ( & version_output. stdout ) ;
345+ let host_triple = version_str
346+ . lines ( )
347+ . find_map ( |line| {
348+ if line. starts_with ( "host:" ) {
349+ Some ( line. trim_start_matches ( "host:" ) . trim ( ) . to_string ( ) )
350+ } else {
351+ None
352+ }
353+ } )
354+ . ok_or_else ( || {
355+ BuildError :: Tool ( "Failed to determine host triple from rustc -vV" . to_string ( ) )
356+ } ) ?;
357+
358+ let tool_path = PathBuf :: from ( & sysroot)
359+ . join ( "lib" )
360+ . join ( "rustlib" )
361+ . join ( & host_triple)
362+ . join ( "bin" )
363+ . join ( tool_name) ;
364+
365+ if tool_path. exists ( ) {
366+ return Ok ( tool_path) ;
367+ }
368+
369+ // Fallback: check if the tool is on PATH (e.g., installed via package manager)
370+ let which_output = Command :: new ( "which" )
371+ . arg ( tool_name)
372+ . output ( ) ;
373+
374+ if let Ok ( output) = which_output {
375+ if output. status . success ( ) {
376+ let path = String :: from_utf8_lossy ( & output. stdout ) . trim ( ) . to_string ( ) ;
377+ return Ok ( PathBuf :: from ( path) ) ;
378+ }
379+ }
380+
381+ Err ( BuildError :: Tool ( format ! (
382+ "Could not find '{}'. Install it with: rustup component add llvm-tools-preview" ,
383+ tool_name
384+ ) ) )
385+ }
386+
123387 /// Generate documentation (ported from xtask docs)
124388 pub fn generate_docs ( ) -> BuildResult < ( ) > {
125389 generate_docs_with_options ( false , false )
@@ -908,8 +1172,11 @@ impl BuildSystem {
9081172 }
9091173
9101174 /// Run comprehensive coverage analysis using ported xtask logic
911- pub fn run_coverage ( & self ) -> BuildResult < ( ) > {
912- xtask_port:: run_coverage_analysis ( & self . config )
1175+ ///
1176+ /// When `html` is true, generates an HTML coverage report in
1177+ /// `target/coverage/html/` in addition to the lcov report.
1178+ pub fn run_coverage ( & self , html : bool ) -> BuildResult < ( ) > {
1179+ xtask_port:: run_coverage_analysis ( & self . config , html)
9131180 }
9141181
9151182 /// Generate documentation using ported xtask logic
0 commit comments