Skip to content

Commit 23322f3

Browse files
notriddlecuviper
authored andcommitted
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. (cherry picked from commit 9cf531d)
1 parent b30cc1e commit 23322f3

File tree

7 files changed

+221
-64
lines changed

7 files changed

+221
-64
lines changed

src/librustdoc/doctest.rs

Lines changed: 125 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -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

494494
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()
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

Comments
 (0)