@@ -96,7 +96,7 @@ pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) ->
9696 . map_err ( |error| format ! ( "failed to create args file: {error:?}" ) ) ?;
9797
9898 // 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 ! [ ] ;
100100
101101 for cfg in & options. cfgs {
102102 content. push ( format ! ( "--cfg={cfg}" ) ) ;
@@ -513,12 +513,18 @@ pub(crate) struct RunnableDocTest {
513513 line : usize ,
514514 edition : Edition ,
515515 no_run : bool ,
516- is_multiple_tests : bool ,
516+ merged_test_code : Option < String > ,
517517}
518518
519519impl 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 ( )
522528 }
523529}
524530
@@ -537,96 +543,108 @@ fn run_test(
537543 let rust_out = add_exe_suffix ( "rust_out" . to_owned ( ) , & rustdoc_options. target ) ;
538544 let output_file = doctest. test_opts . outdir . path ( ) . join ( rust_out) ;
539545
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 ! [ ] ;
545550
546- compiler . arg ( format ! ( "@{}" , doctest. global_opts. args_file. display( ) ) ) ;
551+ compiler_args . push ( format ! ( "@{}" , doctest. global_opts. args_file. display( ) ) ) ;
547552
548553 if let Some ( sysroot) = & rustdoc_options. maybe_sysroot {
549- compiler . arg ( format ! ( "--sysroot={}" , sysroot. display( ) ) ) ;
554+ compiler_args . push ( format ! ( "--sysroot={}" , sysroot. display( ) ) ) ;
550555 }
551556
552- compiler. arg ( "--edition" ) . arg ( doctest. edition . to_string ( ) ) ;
553- if doctest. is_multiple_tests {
554- // The merged test harness uses the `test` crate, so we need to actually allow it.
555- // This will not expose nightly features on stable, because crate attrs disable
556- // merging, and `#![feature]` is required to be a crate attr.
557- compiler. env ( "RUSTC_BOOTSTRAP" , "1" ) ;
558- } else {
559- // Setting these environment variables is unneeded if this is a merged doctest.
560- compiler. env ( "UNSTABLE_RUSTDOC_TEST_PATH" , & doctest. test_opts . path ) ;
561- compiler. env (
562- "UNSTABLE_RUSTDOC_TEST_LINE" ,
563- format ! ( "{}" , doctest. line as isize - doctest. full_test_line_offset as isize ) ,
564- ) ;
565- }
566- compiler. arg ( "-o" ) . arg ( & output_file) ;
557+ compiler_args. extend_from_slice ( & [ "--edition" . to_owned ( ) , doctest. edition . to_string ( ) ] ) ;
567558 if langstr. test_harness {
568- compiler . arg ( "--test" ) ;
559+ compiler_args . push ( "--test" . to_owned ( ) ) ;
569560 }
570561 if rustdoc_options. json_unused_externs . is_enabled ( ) && !langstr. compile_fail {
571- compiler . arg ( "--error-format=json" ) ;
572- compiler . arg ( "--json" ) . arg ( "unused-externs" ) ;
573- compiler . arg ( "-W" ) . arg ( "unused_crate_dependencies" ) ;
574- 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 ( ) ] ) ;
575566 }
576567
577568 if doctest. no_run && !langstr. compile_fail && rustdoc_options. persist_doctests . is_none ( ) {
578569 // FIXME: why does this code check if it *shouldn't* persist doctests
579570 // -- shouldn't it be the negation?
580- compiler . arg ( "--emit=metadata" ) ;
571+ compiler_args . push ( "--emit=metadata" . to_owned ( ) ) ;
581572 }
582- compiler. arg ( "--target" ) . arg ( match & rustdoc_options. target {
583- TargetTuple :: TargetTuple ( s) => s,
584- TargetTuple :: TargetJson { path_for_rustdoc, .. } => {
585- path_for_rustdoc. to_str ( ) . expect ( "target path must be valid unicode" )
586- }
587- } ) ;
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+ ] ) ;
588582 if let ErrorOutputType :: HumanReadable ( kind, color_config) = rustdoc_options. error_format {
589583 let short = kind. short ( ) ;
590584 let unicode = kind == HumanReadableErrorType :: Unicode ;
591585
592586 if short {
593- compiler . arg ( "--error-format" ) . arg ( "short" ) ;
587+ compiler_args . extend_from_slice ( & [ "--error-format" . to_owned ( ) , "short" . to_owned ( ) ] ) ;
594588 }
595589 if unicode {
596- compiler. arg ( "--error-format" ) . arg ( "human-unicode" ) ;
590+ compiler_args
591+ . extend_from_slice ( & [ "--error-format" . to_owned ( ) , "human-unicode" . to_owned ( ) ] ) ;
597592 }
598593
599594 match color_config {
600595 ColorConfig :: Never => {
601- compiler . arg ( "--color" ) . arg ( "never" ) ;
596+ compiler_args . extend_from_slice ( & [ "--color" . to_owned ( ) , "never" . to_owned ( ) ] ) ;
602597 }
603598 ColorConfig :: Always => {
604- compiler . arg ( "--color" ) . arg ( "always" ) ;
599+ compiler_args . extend_from_slice ( & [ "--color" . to_owned ( ) , "always" . to_owned ( ) ] ) ;
605600 }
606601 ColorConfig :: Auto => {
607- 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+ ] ) ;
608606 }
609607 }
610608 }
611609
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+
612618 // If this is a merged doctest, we need to write it into a file instead of using stdin
613619 // because if the size of the merged doctests is too big, it'll simply break stdin.
614- if doctest. is_multiple_tests {
620+ if doctest. is_multiple_tests ( ) {
615621 // It makes the compilation failure much faster if it is for a combined doctest.
616622 compiler. arg ( "--error-format=short" ) ;
617- let input_file = doctest. path_for_merged_doctest ( ) ;
623+ let input_file = doctest. path_for_merged_doctest_bundle ( ) ;
618624 if std:: fs:: write ( & input_file, & doctest. full_test_code ) . is_err ( ) {
619625 // If we cannot write this file for any reason, we leave. All combined tests will be
620626 // tested as standalone tests.
621627 return Err ( TestFailure :: CompileError ) ;
622628 }
623- compiler. arg ( input_file) ;
624629 if !rustdoc_options. nocapture {
625630 // If `nocapture` is disabled, then we don't display rustc's output when compiling
626631 // the merged doctests.
627632 compiler. stderr ( Stdio :: null ( ) ) ;
628633 }
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) ;
629640 } 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+ ) ;
630648 compiler. arg ( "-" ) ;
631649 compiler. stdin ( Stdio :: piped ( ) ) ;
632650 compiler. stderr ( Stdio :: piped ( ) ) ;
@@ -635,8 +653,63 @@ fn run_test(
635653 debug ! ( "compiler invocation for doctest: {compiler:?}" ) ;
636654
637655 let mut child = compiler. spawn ( ) . expect ( "Failed to spawn rustc process" ) ;
638- 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
639658 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+ runner_compiler. env ( "RUSTC_BOOTSTRAP" , "1" ) ;
667+ runner_compiler. args ( compiler_args) ;
668+ runner_compiler. args ( & [ "--crate-type=bin" , "-o" ] ) . arg ( & output_file) ;
669+ let mut extern_path = std:: ffi:: OsString :: from ( format ! (
670+ "--extern=libdoctest_bundle_{edition}=" ,
671+ edition = doctest. edition
672+ ) ) ;
673+ for extern_str in & rustdoc_options. extern_strs {
674+ if let Some ( ( _cratename, path) ) = extern_str. split_once ( '=' ) {
675+ // Direct dependencies of the tests themselves are
676+ // indirect dependencies of the test runner.
677+ // They need to be in the library search path.
678+ let dir = Path :: new ( path)
679+ . parent ( )
680+ . filter ( |x| x. components ( ) . count ( ) > 0 )
681+ . unwrap_or ( Path :: new ( "." ) ) ;
682+ runner_compiler. arg ( "-L" ) . arg ( dir) ;
683+ }
684+ }
685+ let output_bundle_file = doctest
686+ . test_opts
687+ . outdir
688+ . path ( )
689+ . join ( format ! ( "libdoctest_bundle_{edition}.rlib" , edition = doctest. edition) ) ;
690+ extern_path. push ( & output_bundle_file) ;
691+ runner_compiler. arg ( extern_path) ;
692+ runner_compiler. arg ( & runner_input_file) ;
693+ if std:: fs:: write ( & runner_input_file, & merged_test_code) . is_err ( ) {
694+ // If we cannot write this file for any reason, we leave. All combined tests will be
695+ // tested as standalone tests.
696+ return Err ( TestFailure :: CompileError ) ;
697+ }
698+ if !rustdoc_options. nocapture {
699+ // If `nocapture` is disabled, then we don't display rustc's output when compiling
700+ // the merged doctests.
701+ runner_compiler. stderr ( Stdio :: null ( ) ) ;
702+ }
703+ runner_compiler. arg ( "--error-format=short" ) ;
704+ debug ! ( "compiler invocation for doctest runner: {runner_compiler:?}" ) ;
705+
706+ let status = if !status. success ( ) {
707+ status
708+ } else {
709+ let mut child_runner = runner_compiler. spawn ( ) . expect ( "Failed to spawn rustc process" ) ;
710+ child_runner. wait ( ) . expect ( "Failed to wait" )
711+ } ;
712+
640713 process:: Output { status, stdout : Vec :: new ( ) , stderr : Vec :: new ( ) }
641714 } else {
642715 let stdin = child. stdin . as_mut ( ) . expect ( "Failed to open stdin" ) ;
@@ -713,15 +786,15 @@ fn run_test(
713786 cmd. arg ( & output_file) ;
714787 } else {
715788 cmd = Command :: new ( & output_file) ;
716- if doctest. is_multiple_tests {
789+ if doctest. is_multiple_tests ( ) {
717790 cmd. env ( "RUSTDOC_DOCTEST_BIN_PATH" , & output_file) ;
718791 }
719792 }
720793 if let Some ( run_directory) = & rustdoc_options. test_run_directory {
721794 cmd. current_dir ( run_directory) ;
722795 }
723796
724- let result = if doctest. is_multiple_tests || rustdoc_options. nocapture {
797+ let result = if doctest. is_multiple_tests ( ) || rustdoc_options. nocapture {
725798 cmd. status ( ) . map ( |status| process:: Output {
726799 status,
727800 stdout : Vec :: new ( ) ,
@@ -1008,7 +1081,7 @@ fn doctest_run_fn(
10081081 line : scraped_test. line ,
10091082 edition : scraped_test. edition ( & rustdoc_options) ,
10101083 no_run : scraped_test. no_run ( & rustdoc_options) ,
1011- is_multiple_tests : false ,
1084+ merged_test_code : None ,
10121085 } ;
10131086 let res =
10141087 run_test ( runnable_test, & rustdoc_options, doctest. supports_color , report_unused_externs) ;
0 commit comments