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