@@ -502,11 +502,18 @@ impl<'test> TestCx<'test> {
502
502
}
503
503
drop ( proc_res) ;
504
504
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
+
505
512
// Run `llvm-profdata merge` to index the raw coverage output.
506
513
let proc_res = self . run_llvm_tool ( "llvm-profdata" , |cmd| {
507
514
cmd. args ( [ "merge" , "--sparse" , "--output" ] ) ;
508
515
cmd. arg ( & profdata_path) ;
509
- cmd. arg ( & profraw_path ) ;
516
+ cmd. args ( & profraw_paths ) ;
510
517
} ) ;
511
518
if !proc_res. status . success ( ) {
512
519
self . fatal_proc_rec ( "llvm-profdata merge failed!" , & proc_res) ;
@@ -523,8 +530,10 @@ impl<'test> TestCx<'test> {
523
530
cmd. arg ( "--instr-profile" ) ;
524
531
cmd. arg ( & profdata_path) ;
525
532
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
+ }
528
537
} ) ;
529
538
if !proc_res. status . success ( ) {
530
539
self . fatal_proc_rec ( "llvm-cov show failed!" , & proc_res) ;
@@ -553,6 +562,82 @@ impl<'test> TestCx<'test> {
553
562
}
554
563
}
555
564
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
+
556
641
fn run_llvm_tool ( & self , name : & str , configure_cmd_fn : impl FnOnce ( & mut Command ) ) -> ProcRes {
557
642
let tool_path = self
558
643
. config
@@ -582,12 +667,39 @@ impl<'test> TestCx<'test> {
582
667
583
668
let mut lines = normalized. lines ( ) . collect :: < Vec < _ > > ( ) ;
584
669
670
+ Self :: sort_coverage_file_sections ( & mut lines) ?;
585
671
Self :: sort_coverage_subviews ( & mut lines) ?;
586
672
587
673
let joined_lines = lines. iter ( ) . flat_map ( |line| [ line, "\n " ] ) . collect :: < String > ( ) ;
588
674
Ok ( joined_lines)
589
675
}
590
676
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
+
591
703
fn sort_coverage_subviews ( coverage_lines : & mut Vec < & str > ) -> Result < ( ) , String > {
592
704
let mut output_lines = Vec :: new ( ) ;
593
705
0 commit comments