Skip to content
This repository was archived by the owner on May 28, 2025. It is now read-only.

Commit 0f0681e

Browse files
Make merged doctests run in their own process
1 parent dcc77b4 commit 0f0681e

File tree

2 files changed

+87
-17
lines changed

2 files changed

+87
-17
lines changed

src/librustdoc/doctest.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -629,9 +629,13 @@ fn run_test(
629629
let tool = make_maybe_absolute_path(tool.into());
630630
cmd = Command::new(tool);
631631
cmd.args(&rustdoc_options.runtool_args);
632-
cmd.arg(output_file);
632+
cmd.arg(&output_file);
633633
} else {
634-
cmd = Command::new(output_file);
634+
cmd = Command::new(&output_file);
635+
if is_multiple_tests {
636+
cmd.arg("*doctest-bin-path");
637+
cmd.arg(&output_file);
638+
}
635639
}
636640
if let Some(run_directory) = &rustdoc_options.test_run_directory {
637641
cmd.current_dir(run_directory);

src/librustdoc/doctest/runner.rs

Lines changed: 81 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,9 @@ impl DocTestRunner {
7575
#![allow(internal_features)]
7676
#![feature(test)]
7777
#![feature(rustc_attrs)]
78-
#![feature(coverage_attribute)]\n"
79-
.to_string();
78+
#![feature(coverage_attribute)]
79+
"
80+
.to_string();
8081

8182
for crate_attr in &self.crate_attrs {
8283
code.push_str(crate_attr);
@@ -104,15 +105,67 @@ impl DocTestRunner {
104105
code,
105106
"\
106107
{output}
108+
109+
mod __doctest_mod {{
110+
pub static mut BINARY_PATH: Option<std::path::PathBuf> = None;
111+
pub const RUN_OPTION: &str = \"*doctest-inner-test\";
112+
pub const BIN_OPTION: &str = \"*doctest-bin-path\";
113+
114+
#[allow(unused)]
115+
pub fn get_doctest_path() -> Option<&'static std::path::Path> {{
116+
unsafe {{ self::BINARY_PATH.as_deref() }}
117+
}}
118+
119+
#[allow(unused)]
120+
pub fn doctest_runner(bin: &std::path::Path, test_nb: usize) -> Result<(), String> {{
121+
let out = std::process::Command::new(bin)
122+
.arg(self::RUN_OPTION)
123+
.arg(test_nb.to_string())
124+
.output()
125+
.expect(\"failed to run command\");
126+
if !out.status.success() {{
127+
Err(String::from_utf8_lossy(&out.stderr).to_string())
128+
}} else {{
129+
Ok(())
130+
}}
131+
}}
132+
}}
133+
107134
#[rustc_main]
108135
#[coverage(off)]
109-
fn main() {{
136+
fn main() -> std::process::ExitCode {{
110137
const TESTS: [test::TestDescAndFn; {nb_tests}] = [{ids}];
111-
test::test_main(
112-
&[{test_args}],
113-
Vec::from(TESTS),
114-
None,
115-
);
138+
let bin_marker = std::ffi::OsStr::new(__doctest_mod::BIN_OPTION);
139+
let test_marker = std::ffi::OsStr::new(__doctest_mod::RUN_OPTION);
140+
141+
let mut args = std::env::args_os().skip(1);
142+
while let Some(arg) = args.next() {{
143+
if arg == bin_marker {{
144+
let Some(binary) = args.next() else {{
145+
panic!(\"missing argument after `{{}}`\", __doctest_mod::BIN_OPTION);
146+
}};
147+
unsafe {{ crate::__doctest_mod::BINARY_PATH = Some(binary.into()); }}
148+
return std::process::Termination::report(test::test_main(
149+
&[{test_args}],
150+
Vec::from(TESTS),
151+
None,
152+
));
153+
}} else if arg == test_marker {{
154+
let Some(nb_test) = args.next() else {{
155+
panic!(\"missing argument after `{{}}`\", __doctest_mod::RUN_OPTION);
156+
}};
157+
if let Some(nb_test) = nb_test.to_str().and_then(|nb| nb.parse::<usize>().ok()) {{
158+
if let Some(test) = TESTS.get(nb_test) {{
159+
if let test::StaticTestFn(f) = test.testfn {{
160+
return std::process::Termination::report(f());
161+
}}
162+
}}
163+
}}
164+
panic!(\"Unexpected value after `{{}}`\", __doctest_mod::RUN_OPTION);
165+
}}
166+
}}
167+
168+
panic!(\"missing argument for merged doctest binary\");
116169
}}",
117170
nb_tests = self.nb_tests,
118171
output = self.output,
@@ -156,6 +209,10 @@ fn generate_mergeable_doctest(
156209
} else {
157210
writeln!(output, "mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
158211
.unwrap();
212+
if scraped_test.langstr.no_run {
213+
// To prevent having warnings about unused items since they're not called.
214+
writeln!(output, "#![allow(unused)]").unwrap();
215+
}
159216
if doctest.has_main_fn {
160217
output.push_str(&doctest.everything_else);
161218
} else {
@@ -167,14 +224,15 @@ fn generate_mergeable_doctest(
167224
write!(
168225
output,
169226
"\
170-
fn main() {returns_result} {{
171-
{}
172-
}}",
227+
fn main() {returns_result} {{
228+
{}
229+
}}",
173230
doctest.everything_else
174231
)
175232
.unwrap();
176233
}
177234
}
235+
let not_running = ignore || scraped_test.langstr.no_run;
178236
writeln!(
179237
output,
180238
"
@@ -196,7 +254,7 @@ pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{
196254
}},
197255
testfn: test::StaticTestFn(
198256
#[coverage(off)]
199-
|| test::assert_test_result({runner}),
257+
|| {{{runner}}},
200258
)
201259
}};
202260
}}",
@@ -211,10 +269,18 @@ pub const TEST: test::TestDescAndFn = test::TestDescAndFn {{
211269
},
212270
// Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply
213271
// don't give it the function to run.
214-
runner = if ignore || scraped_test.langstr.no_run {
215-
"Ok::<(), String>(())"
272+
runner = if not_running {
273+
"test::assert_test_result(Ok::<(), String>(()))".to_string()
216274
} else {
217-
"self::main()"
275+
format!(
276+
"
277+
if let Some(bin_path) = crate::__doctest_mod::get_doctest_path() {{
278+
test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}))
279+
}} else {{
280+
test::assert_test_result(self::main())
281+
}}
282+
",
283+
)
218284
},
219285
)
220286
.unwrap();

0 commit comments

Comments
 (0)