@@ -95,7 +95,7 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) ->
95
95
. map_err ( |error| format ! ( "failed to create args file: {error:?}" ) ) ?;
96
96
97
97
// We now put the common arguments into the file we created.
98
- let mut content = vec ! [ "--crate-type=bin" . to_string ( ) ] ;
98
+ let mut content = vec ! [ ] ;
99
99
100
100
for cfg in & options. cfgs {
101
101
content. push ( format ! ( "--cfg={cfg}" ) ) ;
@@ -488,12 +488,18 @@ pub(crate) struct RunnableDocTest {
488
488
line : usize ,
489
489
edition : Edition ,
490
490
no_run : bool ,
491
- is_multiple_tests : bool ,
491
+ merged_test_code : Option < String > ,
492
492
}
493
493
494
494
impl RunnableDocTest {
495
- fn path_for_merged_doctest ( & self ) -> PathBuf {
496
- self . test_opts . outdir . path ( ) . join ( format ! ( "doctest_{}.rs" , self . edition) )
495
+ fn path_for_merged_doctest_bundle ( & self ) -> PathBuf {
496
+ self . test_opts . outdir . path ( ) . join ( format ! ( "doctest_bundle_{}.rs" , self . edition) )
497
+ }
498
+ fn path_for_merged_doctest_runner ( & self ) -> PathBuf {
499
+ self . test_opts . outdir . path ( ) . join ( format ! ( "doctest_runner_{}.rs" , self . edition) )
500
+ }
501
+ fn is_multiple_tests ( & self ) -> bool {
502
+ self . merged_test_code . is_some ( )
497
503
}
498
504
}
499
505
@@ -512,96 +518,108 @@ fn run_test(
512
518
let rust_out = add_exe_suffix ( "rust_out" . to_owned ( ) , & rustdoc_options. target ) ;
513
519
let output_file = doctest. test_opts . outdir . path ( ) . join ( rust_out) ;
514
520
515
- let rustc_binary = rustdoc_options
516
- . test_builder
517
- . as_deref ( )
518
- . unwrap_or_else ( || rustc_interface:: util:: rustc_path ( ) . expect ( "found rustc" ) ) ;
519
- let mut compiler = wrapped_rustc_command ( & rustdoc_options. test_builder_wrappers , rustc_binary) ;
521
+ // Common arguments used for compiling the doctest runner.
522
+ // On merged doctests, the compiler is invoked twice: once for the test code itself,
523
+ // and once for the runner wrapper (which needs to use `#![feature]` on stable).
524
+ let mut compiler_args = vec ! [ ] ;
520
525
521
- compiler . arg ( format ! ( "@{}" , doctest. global_opts. args_file. display( ) ) ) ;
526
+ compiler_args . push ( format ! ( "@{}" , doctest. global_opts. args_file. display( ) ) ) ;
522
527
523
528
if let Some ( sysroot) = & rustdoc_options. maybe_sysroot {
524
- compiler . arg ( format ! ( "--sysroot={}" , sysroot. display( ) ) ) ;
529
+ compiler_args . push ( format ! ( "--sysroot={}" , sysroot. display( ) ) ) ;
525
530
}
526
531
527
- compiler. arg ( "--edition" ) . arg ( doctest. edition . to_string ( ) ) ;
528
- if doctest. is_multiple_tests {
529
- // The merged test harness uses the `test` crate, so we need to actually allow it.
530
- // This will not expose nightly features on stable, because crate attrs disable
531
- // merging, and `#![feature]` is required to be a crate attr.
532
- compiler. env ( "RUSTC_BOOTSTRAP" , "1" ) ;
533
- } else {
534
- // Setting these environment variables is unneeded if this is a merged doctest.
535
- compiler. env ( "UNSTABLE_RUSTDOC_TEST_PATH" , & doctest. test_opts . path ) ;
536
- compiler. env (
537
- "UNSTABLE_RUSTDOC_TEST_LINE" ,
538
- format ! ( "{}" , doctest. line as isize - doctest. full_test_line_offset as isize ) ,
539
- ) ;
540
- }
541
- compiler. arg ( "-o" ) . arg ( & output_file) ;
532
+ compiler_args. extend_from_slice ( & [ "--edition" . to_owned ( ) , doctest. edition . to_string ( ) ] ) ;
542
533
if langstr. test_harness {
543
- compiler . arg ( "--test" ) ;
534
+ compiler_args . push ( "--test" . to_owned ( ) ) ;
544
535
}
545
536
if rustdoc_options. json_unused_externs . is_enabled ( ) && !langstr. compile_fail {
546
- compiler . arg ( "--error-format=json" ) ;
547
- compiler . arg ( "--json" ) . arg ( "unused-externs" ) ;
548
- compiler . arg ( "-W" ) . arg ( "unused_crate_dependencies" ) ;
549
- compiler . arg ( "-Z" ) . arg ( "unstable-options" ) ;
537
+ compiler_args . push ( "--error-format=json" . to_owned ( ) ) ;
538
+ compiler_args . extend_from_slice ( & [ "--json" . to_owned ( ) , "unused-externs" . to_owned ( ) ] ) ;
539
+ compiler_args . extend_from_slice ( & [ "-W" . to_owned ( ) , "unused_crate_dependencies" . to_owned ( ) ] ) ;
540
+ compiler_args . extend_from_slice ( & [ "-Z" . to_owned ( ) , "unstable-options" . to_owned ( ) ] ) ;
550
541
}
551
542
552
543
if doctest. no_run && !langstr. compile_fail && rustdoc_options. persist_doctests . is_none ( ) {
553
544
// FIXME: why does this code check if it *shouldn't* persist doctests
554
545
// -- shouldn't it be the negation?
555
- compiler . arg ( "--emit=metadata" ) ;
546
+ compiler_args . push ( "--emit=metadata" . to_owned ( ) ) ;
556
547
}
557
- compiler. arg ( "--target" ) . arg ( match & rustdoc_options. target {
558
- TargetTuple :: TargetTuple ( s) => s,
559
- TargetTuple :: TargetJson { path_for_rustdoc, .. } => {
560
- path_for_rustdoc. to_str ( ) . expect ( "target path must be valid unicode" )
561
- }
562
- } ) ;
548
+ compiler_args. extend_from_slice ( & [
549
+ "--target" . to_owned ( ) ,
550
+ match & rustdoc_options. target {
551
+ TargetTuple :: TargetTuple ( s) => s. clone ( ) ,
552
+ TargetTuple :: TargetJson { path_for_rustdoc, .. } => {
553
+ path_for_rustdoc. to_str ( ) . expect ( "target path must be valid unicode" ) . to_owned ( )
554
+ }
555
+ } ,
556
+ ] ) ;
563
557
if let ErrorOutputType :: HumanReadable ( kind, color_config) = rustdoc_options. error_format {
564
558
let short = kind. short ( ) ;
565
559
let unicode = kind == HumanReadableErrorType :: Unicode ;
566
560
567
561
if short {
568
- compiler . arg ( "--error-format" ) . arg ( "short" ) ;
562
+ compiler_args . extend_from_slice ( & [ "--error-format" . to_owned ( ) , "short" . to_owned ( ) ] ) ;
569
563
}
570
564
if unicode {
571
- compiler. arg ( "--error-format" ) . arg ( "human-unicode" ) ;
565
+ compiler_args
566
+ . extend_from_slice ( & [ "--error-format" . to_owned ( ) , "human-unicode" . to_owned ( ) ] ) ;
572
567
}
573
568
574
569
match color_config {
575
570
ColorConfig :: Never => {
576
- compiler . arg ( "--color" ) . arg ( "never" ) ;
571
+ compiler_args . extend_from_slice ( & [ "--color" . to_owned ( ) , "never" . to_owned ( ) ] ) ;
577
572
}
578
573
ColorConfig :: Always => {
579
- compiler . arg ( "--color" ) . arg ( "always" ) ;
574
+ compiler_args . extend_from_slice ( & [ "--color" . to_owned ( ) , "always" . to_owned ( ) ] ) ;
580
575
}
581
576
ColorConfig :: Auto => {
582
- compiler. arg ( "--color" ) . arg ( if supports_color { "always" } else { "never" } ) ;
577
+ compiler_args. extend_from_slice ( & [
578
+ "--color" . to_owned ( ) ,
579
+ if supports_color { "always" } else { "never" } . to_owned ( ) ,
580
+ ] ) ;
583
581
}
584
582
}
585
583
}
586
584
585
+ let rustc_binary = rustdoc_options
586
+ . test_builder
587
+ . as_deref ( )
588
+ . unwrap_or_else ( || rustc_interface:: util:: rustc_path ( ) . expect ( "found rustc" ) ) ;
589
+ let mut compiler = wrapped_rustc_command ( & rustdoc_options. test_builder_wrappers , rustc_binary) ;
590
+
591
+ compiler. args ( & compiler_args) ;
592
+
587
593
// If this is a merged doctest, we need to write it into a file instead of using stdin
588
594
// because if the size of the merged doctests is too big, it'll simply break stdin.
589
- if doctest. is_multiple_tests {
595
+ if doctest. is_multiple_tests ( ) {
590
596
// It makes the compilation failure much faster if it is for a combined doctest.
591
597
compiler. arg ( "--error-format=short" ) ;
592
- let input_file = doctest. path_for_merged_doctest ( ) ;
598
+ let input_file = doctest. path_for_merged_doctest_bundle ( ) ;
593
599
if std:: fs:: write ( & input_file, & doctest. full_test_code ) . is_err ( ) {
594
600
// If we cannot write this file for any reason, we leave. All combined tests will be
595
601
// tested as standalone tests.
596
602
return Err ( TestFailure :: CompileError ) ;
597
603
}
598
- compiler. arg ( input_file) ;
599
604
if !rustdoc_options. nocapture {
600
605
// If `nocapture` is disabled, then we don't display rustc's output when compiling
601
606
// the merged doctests.
602
607
compiler. stderr ( Stdio :: null ( ) ) ;
603
608
}
609
+ // bundled tests are an rlib, loaded by a separate runner executable
610
+ compiler
611
+ . arg ( "--crate-type=lib" )
612
+ . arg ( "--out-dir" )
613
+ . arg ( doctest. test_opts . outdir . path ( ) )
614
+ . arg ( input_file) ;
604
615
} else {
616
+ compiler. arg ( "--crate-type=bin" ) . arg ( "-o" ) . arg ( & output_file) ;
617
+ // Setting these environment variables is unneeded if this is a merged doctest.
618
+ compiler. env ( "UNSTABLE_RUSTDOC_TEST_PATH" , & doctest. test_opts . path ) ;
619
+ compiler. env (
620
+ "UNSTABLE_RUSTDOC_TEST_LINE" ,
621
+ format ! ( "{}" , doctest. line as isize - doctest. full_test_line_offset as isize ) ,
622
+ ) ;
605
623
compiler. arg ( "-" ) ;
606
624
compiler. stdin ( Stdio :: piped ( ) ) ;
607
625
compiler. stderr ( Stdio :: piped ( ) ) ;
@@ -610,8 +628,65 @@ fn run_test(
610
628
debug ! ( "compiler invocation for doctest: {compiler:?}" ) ;
611
629
612
630
let mut child = compiler. spawn ( ) . expect ( "Failed to spawn rustc process" ) ;
613
- let output = if doctest. is_multiple_tests {
631
+ let output = if let Some ( merged_test_code) = & doctest. merged_test_code {
632
+ // compile-fail tests never get merged, so this should always pass
614
633
let status = child. wait ( ) . expect ( "Failed to wait" ) ;
634
+
635
+ // the actual test runner is a separate component, built with nightly-only features;
636
+ // build it now
637
+ let runner_input_file = doctest. path_for_merged_doctest_runner ( ) ;
638
+
639
+ let mut runner_compiler =
640
+ wrapped_rustc_command ( & rustdoc_options. test_builder_wrappers , rustc_binary) ;
641
+ // the test runner does not contain any user-written code, so this doesn't allow
642
+ // the user to exploit nightly-only features on stable
643
+ runner_compiler. env ( "RUSTC_BOOTSTRAP" , "1" ) ;
644
+ runner_compiler. args ( compiler_args) ;
645
+ runner_compiler. args ( & [ "--crate-type=bin" , "-o" ] ) . arg ( & output_file) ;
646
+ let mut extern_path = std:: ffi:: OsString :: from ( format ! (
647
+ "--extern=doctest_bundle_{edition}=" ,
648
+ edition = doctest. edition
649
+ ) ) ;
650
+ for extern_str in & rustdoc_options. extern_strs {
651
+ if let Some ( ( _cratename, path) ) = extern_str. split_once ( '=' ) {
652
+ // Direct dependencies of the tests themselves are
653
+ // indirect dependencies of the test runner.
654
+ // They need to be in the library search path.
655
+ let dir = Path :: new ( path)
656
+ . parent ( )
657
+ . filter ( |x| x. components ( ) . count ( ) > 0 )
658
+ . unwrap_or ( Path :: new ( "." ) ) ;
659
+ runner_compiler. arg ( "-L" ) . arg ( dir) ;
660
+ }
661
+ }
662
+ let output_bundle_file = doctest
663
+ . test_opts
664
+ . outdir
665
+ . path ( )
666
+ . join ( format ! ( "libdoctest_bundle_{edition}.rlib" , edition = doctest. edition) ) ;
667
+ extern_path. push ( & output_bundle_file) ;
668
+ runner_compiler. arg ( extern_path) ;
669
+ runner_compiler. arg ( & runner_input_file) ;
670
+ if std:: fs:: write ( & runner_input_file, & merged_test_code) . is_err ( ) {
671
+ // If we cannot write this file for any reason, we leave. All combined tests will be
672
+ // tested as standalone tests.
673
+ return Err ( TestFailure :: CompileError ) ;
674
+ }
675
+ if !rustdoc_options. nocapture {
676
+ // If `nocapture` is disabled, then we don't display rustc's output when compiling
677
+ // the merged doctests.
678
+ runner_compiler. stderr ( Stdio :: null ( ) ) ;
679
+ }
680
+ runner_compiler. arg ( "--error-format=short" ) ;
681
+ debug ! ( "compiler invocation for doctest runner: {runner_compiler:?}" ) ;
682
+
683
+ let status = if !status. success ( ) {
684
+ status
685
+ } else {
686
+ let mut child_runner = runner_compiler. spawn ( ) . expect ( "Failed to spawn rustc process" ) ;
687
+ child_runner. wait ( ) . expect ( "Failed to wait" )
688
+ } ;
689
+
615
690
process:: Output { status, stdout : Vec :: new ( ) , stderr : Vec :: new ( ) }
616
691
} else {
617
692
let stdin = child. stdin . as_mut ( ) . expect ( "Failed to open stdin" ) ;
@@ -688,15 +763,15 @@ fn run_test(
688
763
cmd. arg ( & output_file) ;
689
764
} else {
690
765
cmd = Command :: new ( & output_file) ;
691
- if doctest. is_multiple_tests {
766
+ if doctest. is_multiple_tests ( ) {
692
767
cmd. env ( "RUSTDOC_DOCTEST_BIN_PATH" , & output_file) ;
693
768
}
694
769
}
695
770
if let Some ( run_directory) = & rustdoc_options. test_run_directory {
696
771
cmd. current_dir ( run_directory) ;
697
772
}
698
773
699
- let result = if doctest. is_multiple_tests || rustdoc_options. nocapture {
774
+ let result = if doctest. is_multiple_tests ( ) || rustdoc_options. nocapture {
700
775
cmd. status ( ) . map ( |status| process:: Output {
701
776
status,
702
777
stdout : Vec :: new ( ) ,
@@ -982,7 +1057,7 @@ fn doctest_run_fn(
982
1057
line : scraped_test. line ,
983
1058
edition : scraped_test. edition ( & rustdoc_options) ,
984
1059
no_run : scraped_test. no_run ( & rustdoc_options) ,
985
- is_multiple_tests : false ,
1060
+ merged_test_code : None ,
986
1061
} ;
987
1062
let res =
988
1063
run_test ( runnable_test, & rustdoc_options, doctest. supports_color , report_unused_externs) ;
0 commit comments