@@ -502,11 +502,18 @@ impl<'test> TestCx<'test> {
502502 }
503503 drop ( proc_res) ;
504504
505+ let mut profraw_paths = vec ! [ profraw_path] ;
506+ let mut bin_paths = vec ! [ self . make_exe_name( ) ] ;
507+
508+ if self . config . suite == "run-coverage-rustdoc" {
509+ self . run_doctests_for_coverage ( & mut profraw_paths, & mut bin_paths) ;
510+ }
511+
505512 // Run `llvm-profdata merge` to index the raw coverage output.
506513 let proc_res = self . run_llvm_tool ( "llvm-profdata" , |cmd| {
507514 cmd. args ( [ "merge" , "--sparse" , "--output" ] ) ;
508515 cmd. arg ( & profdata_path) ;
509- cmd. arg ( & profraw_path ) ;
516+ cmd. args ( & profraw_paths ) ;
510517 } ) ;
511518 if !proc_res. status . success ( ) {
512519 self . fatal_proc_rec ( "llvm-profdata merge failed!" , & proc_res) ;
@@ -523,8 +530,10 @@ impl<'test> TestCx<'test> {
523530 cmd. arg ( "--instr-profile" ) ;
524531 cmd. arg ( & profdata_path) ;
525532
526- cmd. arg ( "--object" ) ;
527- cmd. arg ( & self . make_exe_name ( ) ) ;
533+ for bin in & bin_paths {
534+ cmd. arg ( "--object" ) ;
535+ cmd. arg ( bin) ;
536+ }
528537 } ) ;
529538 if !proc_res. status . success ( ) {
530539 self . fatal_proc_rec ( "llvm-cov show failed!" , & proc_res) ;
@@ -553,6 +562,82 @@ impl<'test> TestCx<'test> {
553562 }
554563 }
555564
565+ /// Run any doctests embedded in this test file, and add any resulting
566+ /// `.profraw` files and doctest executables to the given vectors.
567+ fn run_doctests_for_coverage (
568+ & self ,
569+ profraw_paths : & mut Vec < PathBuf > ,
570+ bin_paths : & mut Vec < PathBuf > ,
571+ ) {
572+ // Put .profraw files and doctest executables in dedicated directories,
573+ // to make it easier to glob them all later.
574+ let profraws_dir = self . output_base_dir ( ) . join ( "doc_profraws" ) ;
575+ let bins_dir = self . output_base_dir ( ) . join ( "doc_bins" ) ;
576+
577+ // Remove existing directories to prevent cross-run interference.
578+ if profraws_dir. try_exists ( ) . unwrap ( ) {
579+ std:: fs:: remove_dir_all ( & profraws_dir) . unwrap ( ) ;
580+ }
581+ if bins_dir. try_exists ( ) . unwrap ( ) {
582+ std:: fs:: remove_dir_all ( & bins_dir) . unwrap ( ) ;
583+ }
584+
585+ let mut rustdoc_cmd =
586+ Command :: new ( self . config . rustdoc_path . as_ref ( ) . expect ( "--rustdoc-path not passed" ) ) ;
587+
588+ // In general there will be multiple doctest binaries running, so we
589+ // tell the profiler runtime to write their coverage data into separate
590+ // profraw files.
591+ rustdoc_cmd. env ( "LLVM_PROFILE_FILE" , profraws_dir. join ( "%p-%m.profraw" ) ) ;
592+
593+ rustdoc_cmd. args ( [ "--test" , "-Cinstrument-coverage" ] ) ;
594+
595+ // Without this, the doctests complain about not being able to find
596+ // their enclosing file's crate for some reason.
597+ rustdoc_cmd. args ( [ "--crate-name" , "workaround_for_79771" ] ) ;
598+
599+ // Persist the doctest binaries so that `llvm-cov show` can read their
600+ // embedded coverage mappings later.
601+ rustdoc_cmd. arg ( "-Zunstable-options" ) ;
602+ rustdoc_cmd. arg ( "--persist-doctests" ) ;
603+ rustdoc_cmd. arg ( & bins_dir) ;
604+
605+ rustdoc_cmd. arg ( "-L" ) ;
606+ rustdoc_cmd. arg ( self . aux_output_dir_name ( ) ) ;
607+
608+ rustdoc_cmd. arg ( & self . testpaths . file ) ;
609+
610+ let proc_res = self . compose_and_run_compiler ( rustdoc_cmd, None ) ;
611+ if !proc_res. status . success ( ) {
612+ self . fatal_proc_rec ( "rustdoc --test failed!" , & proc_res)
613+ }
614+
615+ fn glob_iter ( path : impl AsRef < Path > ) -> impl Iterator < Item = PathBuf > {
616+ let path_str = path. as_ref ( ) . to_str ( ) . unwrap ( ) ;
617+ let iter = glob ( path_str) . unwrap ( ) ;
618+ iter. map ( Result :: unwrap)
619+ }
620+
621+ // Find all profraw files in the profraw directory.
622+ for p in glob_iter ( profraws_dir. join ( "*.profraw" ) ) {
623+ profraw_paths. push ( p) ;
624+ }
625+ // Find all executables in the `--persist-doctests` directory, while
626+ // avoiding other file types (e.g. `.pdb` on Windows). This doesn't
627+ // need to be perfect, as long as it can handle the files actually
628+ // produced by `rustdoc --test`.
629+ for p in glob_iter ( bins_dir. join ( "**/*" ) ) {
630+ let is_bin = p. is_file ( )
631+ && match p. extension ( ) {
632+ None => true ,
633+ Some ( ext) => ext == OsStr :: new ( "exe" ) ,
634+ } ;
635+ if is_bin {
636+ bin_paths. push ( p) ;
637+ }
638+ }
639+ }
640+
556641 fn run_llvm_tool ( & self , name : & str , configure_cmd_fn : impl FnOnce ( & mut Command ) ) -> ProcRes {
557642 let tool_path = self
558643 . config
@@ -582,12 +667,39 @@ impl<'test> TestCx<'test> {
582667
583668 let mut lines = normalized. lines ( ) . collect :: < Vec < _ > > ( ) ;
584669
670+ Self :: sort_coverage_file_sections ( & mut lines) ?;
585671 Self :: sort_coverage_subviews ( & mut lines) ?;
586672
587673 let joined_lines = lines. iter ( ) . flat_map ( |line| [ line, "\n " ] ) . collect :: < String > ( ) ;
588674 Ok ( joined_lines)
589675 }
590676
677+ /// Coverage reports can describe multiple source files, separated by
678+ /// blank lines. The order of these files is unpredictable (since it
679+ /// depends on implementation details), so we need to sort the file
680+ /// sections into a consistent order before comparing against a snapshot.
681+ fn sort_coverage_file_sections ( coverage_lines : & mut Vec < & str > ) -> Result < ( ) , String > {
682+ // Group the lines into file sections, separated by blank lines.
683+ let mut sections = coverage_lines. split ( |line| line. is_empty ( ) ) . collect :: < Vec < _ > > ( ) ;
684+
685+ // The last section should be empty, representing an extra trailing blank line.
686+ if !sections. last ( ) . is_some_and ( |last| last. is_empty ( ) ) {
687+ return Err ( "coverage report should end with an extra blank line" . to_owned ( ) ) ;
688+ }
689+
690+ // Sort the file sections (not including the final empty "section").
691+ let except_last = sections. len ( ) - 1 ;
692+ ( & mut sections[ ..except_last] ) . sort ( ) ;
693+
694+ // Join the file sections back into a flat list of lines, with
695+ // sections separated by blank lines.
696+ let joined = sections. join ( & [ "" ] as & [ _ ] ) ;
697+ assert_eq ! ( joined. len( ) , coverage_lines. len( ) ) ;
698+ * coverage_lines = joined;
699+
700+ Ok ( ( ) )
701+ }
702+
591703 fn sort_coverage_subviews ( coverage_lines : & mut Vec < & str > ) -> Result < ( ) , String > {
592704 let mut output_lines = Vec :: new ( ) ;
593705
0 commit comments