@@ -95,7 +95,7 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) ->
9595 . map_err ( |error| format ! ( "failed to create args file: {error:?}" ) ) ?;
9696
9797 // 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 ! [ ] ;
9999
100100 for cfg in & options. cfgs {
101101 content. push ( format ! ( "--cfg={cfg}" ) ) ;
@@ -488,12 +488,18 @@ pub(crate) struct RunnableDocTest {
488488 line : usize ,
489489 edition : Edition ,
490490 no_run : bool ,
491- is_multiple_tests : bool ,
491+ merged_test_code : Option < String > ,
492492}
493493
494494impl 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 ( )
497503 }
498504}
499505
@@ -512,96 +518,108 @@ fn run_test(
512518 let rust_out = add_exe_suffix ( "rust_out" . to_owned ( ) , & rustdoc_options. target ) ;
513519 let output_file = doctest. test_opts . outdir . path ( ) . join ( rust_out) ;
514520
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 ! [ ] ;
520525
521- compiler . arg ( format ! ( "@{}" , doctest. global_opts. args_file. display( ) ) ) ;
526+ compiler_args . push ( format ! ( "@{}" , doctest. global_opts. args_file. display( ) ) ) ;
522527
523528 if let Some ( sysroot) = & rustdoc_options. maybe_sysroot {
524- compiler . arg ( format ! ( "--sysroot={}" , sysroot. display( ) ) ) ;
529+ compiler_args . push ( format ! ( "--sysroot={}" , sysroot. display( ) ) ) ;
525530 }
526531
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 ( ) ] ) ;
542533 if langstr. test_harness {
543- compiler . arg ( "--test" ) ;
534+ compiler_args . push ( "--test" . to_owned ( ) ) ;
544535 }
545536 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 ( ) ] ) ;
550541 }
551542
552543 if doctest. no_run && !langstr. compile_fail && rustdoc_options. persist_doctests . is_none ( ) {
553544 // FIXME: why does this code check if it *shouldn't* persist doctests
554545 // -- shouldn't it be the negation?
555- compiler . arg ( "--emit=metadata" ) ;
546+ compiler_args . push ( "--emit=metadata" . to_owned ( ) ) ;
556547 }
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+ ] ) ;
563557 if let ErrorOutputType :: HumanReadable ( kind, color_config) = rustdoc_options. error_format {
564558 let short = kind. short ( ) ;
565559 let unicode = kind == HumanReadableErrorType :: Unicode ;
566560
567561 if short {
568- compiler . arg ( "--error-format" ) . arg ( "short" ) ;
562+ compiler_args . extend_from_slice ( & [ "--error-format" . to_owned ( ) , "short" . to_owned ( ) ] ) ;
569563 }
570564 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 ( ) ] ) ;
572567 }
573568
574569 match color_config {
575570 ColorConfig :: Never => {
576- compiler . arg ( "--color" ) . arg ( "never" ) ;
571+ compiler_args . extend_from_slice ( & [ "--color" . to_owned ( ) , "never" . to_owned ( ) ] ) ;
577572 }
578573 ColorConfig :: Always => {
579- compiler . arg ( "--color" ) . arg ( "always" ) ;
574+ compiler_args . extend_from_slice ( & [ "--color" . to_owned ( ) , "always" . to_owned ( ) ] ) ;
580575 }
581576 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+ ] ) ;
583581 }
584582 }
585583 }
586584
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+
587593 // If this is a merged doctest, we need to write it into a file instead of using stdin
588594 // 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 ( ) {
590596 // It makes the compilation failure much faster if it is for a combined doctest.
591597 compiler. arg ( "--error-format=short" ) ;
592- let input_file = doctest. path_for_merged_doctest ( ) ;
598+ let input_file = doctest. path_for_merged_doctest_bundle ( ) ;
593599 if std:: fs:: write ( & input_file, & doctest. full_test_code ) . is_err ( ) {
594600 // If we cannot write this file for any reason, we leave. All combined tests will be
595601 // tested as standalone tests.
596602 return Err ( TestFailure :: CompileError ) ;
597603 }
598- compiler. arg ( input_file) ;
599604 if !rustdoc_options. nocapture {
600605 // If `nocapture` is disabled, then we don't display rustc's output when compiling
601606 // the merged doctests.
602607 compiler. stderr ( Stdio :: null ( ) ) ;
603608 }
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) ;
604615 } 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+ ) ;
605623 compiler. arg ( "-" ) ;
606624 compiler. stdin ( Stdio :: piped ( ) ) ;
607625 compiler. stderr ( Stdio :: piped ( ) ) ;
@@ -610,8 +628,65 @@ fn run_test(
610628 debug ! ( "compiler invocation for doctest: {compiler:?}" ) ;
611629
612630 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
614633 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+
615690 process:: Output { status, stdout : Vec :: new ( ) , stderr : Vec :: new ( ) }
616691 } else {
617692 let stdin = child. stdin . as_mut ( ) . expect ( "Failed to open stdin" ) ;
@@ -688,15 +763,15 @@ fn run_test(
688763 cmd. arg ( & output_file) ;
689764 } else {
690765 cmd = Command :: new ( & output_file) ;
691- if doctest. is_multiple_tests {
766+ if doctest. is_multiple_tests ( ) {
692767 cmd. env ( "RUSTDOC_DOCTEST_BIN_PATH" , & output_file) ;
693768 }
694769 }
695770 if let Some ( run_directory) = & rustdoc_options. test_run_directory {
696771 cmd. current_dir ( run_directory) ;
697772 }
698773
699- let result = if doctest. is_multiple_tests || rustdoc_options. nocapture {
774+ let result = if doctest. is_multiple_tests ( ) || rustdoc_options. nocapture {
700775 cmd. status ( ) . map ( |status| process:: Output {
701776 status,
702777 stdout : Vec :: new ( ) ,
@@ -982,7 +1057,7 @@ fn doctest_run_fn(
9821057 line : scraped_test. line ,
9831058 edition : scraped_test. edition ( & rustdoc_options) ,
9841059 no_run : scraped_test. no_run ( & rustdoc_options) ,
985- is_multiple_tests : false ,
1060+ merged_test_code : None ,
9861061 } ;
9871062 let res =
9881063 run_test ( runnable_test, & rustdoc_options, doctest. supports_color , report_unused_externs) ;
0 commit comments