Skip to content

Commit bcdcb6b

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 bcdcb6b

File tree

7 files changed

+195
-59
lines changed

7 files changed

+195
-59
lines changed

src/librustdoc/doctest.rs

+98-45
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

@@ -543,90 +549,100 @@ fn run_test(
543549
.unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
544550
let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
545551

546-
compiler.arg(format!("@{}", doctest.global_opts.args_file.display()));
552+
let mut compiler_args = vec![];
553+
554+
compiler_args.push(format!("@{}", doctest.global_opts.args_file.display()));
547555

548556
if let Some(sysroot) = &rustdoc_options.maybe_sysroot {
549-
compiler.arg(format!("--sysroot={}", sysroot.display()));
557+
compiler_args.push(format!("--sysroot={}", sysroot.display()));
550558
}
551559

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);
560+
compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]);
567561
if langstr.test_harness {
568-
compiler.arg("--test");
562+
compiler_args.push("--test".to_owned());
569563
}
570564
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");
565+
compiler_args.push("--error-format=json".to_owned());
566+
compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]);
567+
compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]);
568+
compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]);
575569
}
576570

577571
if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
578572
// FIXME: why does this code check if it *shouldn't* persist doctests
579573
// -- shouldn't it be the negation?
580-
compiler.arg("--emit=metadata");
574+
compiler_args.push("--emit=metadata".to_owned());
581575
}
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-
});
576+
compiler_args.extend_from_slice(&[
577+
"--target".to_owned(),
578+
match &rustdoc_options.target {
579+
TargetTuple::TargetTuple(s) => s.clone(),
580+
TargetTuple::TargetJson { path_for_rustdoc, .. } => {
581+
path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned()
582+
}
583+
},
584+
]);
588585
if let ErrorOutputType::HumanReadable(kind, color_config) = rustdoc_options.error_format {
589586
let short = kind.short();
590587
let unicode = kind == HumanReadableErrorType::Unicode;
591588

592589
if short {
593-
compiler.arg("--error-format").arg("short");
590+
compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]);
594591
}
595592
if unicode {
596-
compiler.arg("--error-format").arg("human-unicode");
593+
compiler_args
594+
.extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]);
597595
}
598596

599597
match color_config {
600598
ColorConfig::Never => {
601-
compiler.arg("--color").arg("never");
599+
compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]);
602600
}
603601
ColorConfig::Always => {
604-
compiler.arg("--color").arg("always");
602+
compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]);
605603
}
606604
ColorConfig::Auto => {
607-
compiler.arg("--color").arg(if supports_color { "always" } else { "never" });
605+
compiler_args.extend_from_slice(&[
606+
"--color".to_owned(),
607+
if supports_color { "always" } else { "never" }.to_owned(),
608+
]);
608609
}
609610
}
610611
}
611612

613+
compiler.args(&compiler_args);
614+
612615
// If this is a merged doctest, we need to write it into a file instead of using stdin
613616
// because if the size of the merged doctests is too big, it'll simply break stdin.
614-
if doctest.is_multiple_tests {
617+
let output_bundle_file = doctest
618+
.test_opts
619+
.outdir
620+
.path()
621+
.join(format!("librustdoc_tests_merged_{edition}.rlib", edition = doctest.edition));
622+
if doctest.is_multiple_tests() {
615623
// It makes the compilation failure much faster if it is for a combined doctest.
616624
compiler.arg("--error-format=short");
617-
let input_file = doctest.path_for_merged_doctest();
625+
let input_file = doctest.path_for_merged_doctest_bundle();
618626
if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
619627
// If we cannot write this file for any reason, we leave. All combined tests will be
620628
// tested as standalone tests.
621629
return Err(TestFailure::CompileError);
622630
}
623-
compiler.arg(input_file);
624631
if !rustdoc_options.nocapture {
625632
// If `nocapture` is disabled, then we don't display rustc's output when compiling
626633
// the merged doctests.
627634
compiler.stderr(Stdio::null());
628635
}
636+
// bundled tests are an rlib, loaded by a separate runner executable
637+
compiler.arg("--crate-type=lib").arg("-o").arg(&output_bundle_file).arg(input_file);
629638
} else {
639+
compiler.arg("--crate-type=bin").arg("-o").arg(&output_file);
640+
// Setting these environment variables is unneeded if this is a merged doctest.
641+
compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
642+
compiler.env(
643+
"UNSTABLE_RUSTDOC_TEST_LINE",
644+
format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
645+
);
630646
compiler.arg("-");
631647
compiler.stdin(Stdio::piped());
632648
compiler.stderr(Stdio::piped());
@@ -635,8 +651,45 @@ fn run_test(
635651
debug!("compiler invocation for doctest: {compiler:?}");
636652

637653
let mut child = compiler.spawn().expect("Failed to spawn rustc process");
638-
let output = if doctest.is_multiple_tests {
654+
let output = if let Some(merged_test_code) = &doctest.merged_test_code {
655+
// compile-fail tests never get merged, so this should always pass
639656
let status = child.wait().expect("Failed to wait");
657+
658+
// the actual test runner is a separate component, built with nightly-only features;
659+
// build it now
660+
let runner_input_file = doctest.path_for_merged_doctest_runner();
661+
662+
let mut compiler_runner =
663+
wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
664+
compiler_runner.args(compiler_args);
665+
compiler_runner.args(&["--crate-type=bin", "-o"]).arg(&output_file);
666+
let mut extern_path = std::ffi::OsString::from(format!(
667+
"--extern=rustdoc_tests_merged_{edition}=",
668+
edition = doctest.edition
669+
));
670+
extern_path.push(&output_bundle_file);
671+
compiler_runner.arg(extern_path);
672+
compiler_runner.arg(&runner_input_file);
673+
if std::fs::write(&runner_input_file, &merged_test_code).is_err() {
674+
// If we cannot write this file for any reason, we leave. All combined tests will be
675+
// tested as standalone tests.
676+
return Err(TestFailure::CompileError);
677+
}
678+
if !rustdoc_options.nocapture {
679+
// If `nocapture` is disabled, then we don't display rustc's output when compiling
680+
// the merged doctests.
681+
compiler_runner.stderr(Stdio::null());
682+
}
683+
compiler_runner.arg("--error-format=short");
684+
debug!("compiler invocation for doctest bundle: {compiler_runner:?}");
685+
686+
let status = if !status.success() {
687+
status
688+
} else {
689+
let mut child_runner = compiler_runner.spawn().expect("Failed to spawn rustc process");
690+
child_runner.wait().expect("Failed to wait")
691+
};
692+
640693
process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
641694
} else {
642695
let stdin = child.stdin.as_mut().expect("Failed to open stdin");
@@ -713,15 +766,15 @@ fn run_test(
713766
cmd.arg(&output_file);
714767
} else {
715768
cmd = Command::new(&output_file);
716-
if doctest.is_multiple_tests {
769+
if doctest.is_multiple_tests() {
717770
cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file);
718771
}
719772
}
720773
if let Some(run_directory) = &rustdoc_options.test_run_directory {
721774
cmd.current_dir(run_directory);
722775
}
723776

724-
let result = if doctest.is_multiple_tests || rustdoc_options.nocapture {
777+
let result = if doctest.is_multiple_tests() || rustdoc_options.nocapture {
725778
cmd.status().map(|status| process::Output {
726779
status,
727780
stdout: Vec::new(),
@@ -1008,7 +1061,7 @@ fn doctest_run_fn(
10081061
line: scraped_test.line,
10091062
edition: scraped_test.edition(&rustdoc_options),
10101063
no_run: scraped_test.no_run(&rustdoc_options),
1011-
is_multiple_tests: false,
1064+
merged_test_code: None,
10121065
};
10131066
let res =
10141067
run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);

src/librustdoc/doctest/runner.rs

+26-12
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pub(crate) struct DocTestRunner {
1414
crate_attrs: FxIndexSet<String>,
1515
ids: String,
1616
output: String,
17+
output_merged_tests: String,
1718
supports_color: bool,
1819
nb_tests: usize,
1920
}
@@ -24,6 +25,7 @@ impl DocTestRunner {
2425
crate_attrs: FxIndexSet::default(),
2526
ids: String::new(),
2627
output: String::new(),
28+
output_merged_tests: String::new(),
2729
supports_color: true,
2830
nb_tests: 0,
2931
}
@@ -55,7 +57,8 @@ impl DocTestRunner {
5557
scraped_test,
5658
ignore,
5759
self.nb_tests,
58-
&mut self.output
60+
&mut self.output,
61+
&mut self.output_merged_tests,
5962
),
6063
));
6164
self.supports_color &= doctest.supports_color;
@@ -78,25 +81,29 @@ impl DocTestRunner {
7881
"
7982
.to_string();
8083

84+
let mut code_prefix = String::new();
85+
8186
for crate_attr in &self.crate_attrs {
82-
code.push_str(crate_attr);
83-
code.push('\n');
87+
code_prefix.push_str(crate_attr);
88+
code_prefix.push('\n');
8489
}
8590

8691
if opts.attrs.is_empty() {
8792
// If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
8893
// lints that are commonly triggered in doctests. The crate-level test attributes are
8994
// commonly used to make tests fail in case they trigger warnings, so having this there in
9095
// that case may cause some tests to pass when they shouldn't have.
91-
code.push_str("#![allow(unused)]\n");
96+
code_prefix.push_str("#![allow(unused)]\n");
9297
}
9398

9499
// Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
95100
for attr in &opts.attrs {
96-
code.push_str(&format!("#![{attr}]\n"));
101+
code_prefix.push_str(&format!("#![{attr}]\n"));
97102
}
98103

99104
code.push_str("extern crate test;\n");
105+
writeln!(code, "extern crate rustdoc_tests_merged_{edition} as rustdoc_tests_merged;")
106+
.unwrap();
100107

101108
let test_args = test_args.iter().fold(String::new(), |mut x, arg| {
102109
write!(x, "{arg:?}.to_string(),").unwrap();
@@ -161,20 +168,20 @@ the same process\");
161168
std::process::Termination::report(test::test_main(test_args, Vec::from(TESTS), None))
162169
}}",
163170
nb_tests = self.nb_tests,
164-
output = self.output,
171+
output = self.output_merged_tests,
165172
ids = self.ids,
166173
)
167174
.expect("failed to generate test code");
168175
let runnable_test = RunnableDocTest {
169-
full_test_code: code,
176+
full_test_code: format!("{code_prefix}{code}", code = self.output),
170177
full_test_line_offset: 0,
171178
test_opts: test_options,
172179
global_opts: opts.clone(),
173180
langstr: LangString::default(),
174181
line: 0,
175182
edition,
176183
no_run: false,
177-
is_multiple_tests: true,
184+
merged_test_code: Some(code),
178185
};
179186
let ret =
180187
run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {});
@@ -189,14 +196,15 @@ fn generate_mergeable_doctest(
189196
ignore: bool,
190197
id: usize,
191198
output: &mut String,
199+
output_merged_tests: &mut String,
192200
) -> String {
193201
let test_id = format!("__doctest_{id}");
194202

195203
if ignore {
196204
// We generate nothing else.
197-
writeln!(output, "mod {test_id} {{\n").unwrap();
205+
writeln!(output, "pub mod {test_id} {{}}\n").unwrap();
198206
} else {
199-
writeln!(output, "mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
207+
writeln!(output, "pub mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
200208
.unwrap();
201209
if doctest.has_main_fn {
202210
output.push_str(&doctest.everything_else);
@@ -216,11 +224,17 @@ fn main() {returns_result} {{
216224
)
217225
.unwrap();
218226
}
227+
writeln!(
228+
output,
229+
"\npub fn __main_fn() -> impl std::process::Termination {{ main() }} \n}}\n"
230+
)
231+
.unwrap();
219232
}
220233
let not_running = ignore || scraped_test.langstr.no_run;
221234
writeln!(
222-
output,
235+
output_merged_tests,
223236
"
237+
mod {test_id} {{
224238
pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest(
225239
{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic},
226240
test::StaticTestFn(
@@ -242,7 +256,7 @@ test::StaticTestFn(
242256
if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
243257
test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}))
244258
}} else {{
245-
test::assert_test_result(self::main())
259+
test::assert_test_result(rustdoc_tests_merged::{test_id}::__main_fn())
246260
}}
247261
",
248262
)

tests/run-make/doctests-merge/rmake.rs

-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ fn test_and_compare(input_file: &str, stdout_file: &str, edition: &str, dep: &Pa
88
let output = cmd
99
.input(input_file)
1010
.arg("--test")
11-
.arg("-Zunstable-options")
1211
.edition(edition)
1312
.arg("--test-args=--test-threads=1")
1413
.extern_("foo", dep.display().to_string())

tests/rustdoc-ui/doctest/doctest-output.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//@[edition2015]edition:2015
33
//@[edition2015]aux-build:extern_macros.rs
44
//@[edition2015]compile-flags:--test --test-args=--test-threads=1
5-
//@[edition2024]edition:2015
5+
//@[edition2024]edition:2024
66
//@[edition2024]aux-build:extern_macros.rs
77
//@[edition2024]compile-flags:--test --test-args=--test-threads=1
88
//@ normalize-stdout: "tests/rustdoc-ui/doctest" -> "$$DIR"

0 commit comments

Comments
 (0)