Skip to content

Commit cf175d7

Browse files
committed
doctests: build test bundle and harness separately
This prevents the included test case from getting at nightly-only features when run on stable. The harness builds with RUSTC_BOOTSTRAP, but the bundle doesn't.
1 parent d7727ef commit cf175d7

File tree

7 files changed

+219
-64
lines changed

7 files changed

+219
-64
lines changed

src/librustdoc/doctest.rs

+123-50
Original file line numberDiff line numberDiff line change
@@ -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

519519
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()
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=doctest_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

Comments
 (0)