1use std::fmt::Write;
2use std::time::Duration;
3
4use rustc_data_structures::fx::FxIndexSet;
5use rustc_span::RemapPathScopeComponents;
6use rustc_span::edition::Edition;
7
8use crate::doctest::{
9 DocTestBuilder, GlobalTestOptions, IndividualTestOptions, RunnableDocTest, RustdocOptions,
10 ScrapedDocTest, TestFailure, UnusedExterns, run_test,
11};
12use crate::html::markdown::{Ignore, LangString};
13
14pub(crate) struct DocTestRunner {
16 crate_attrs: FxIndexSet<String>,
17 global_crate_attrs: FxIndexSet<String>,
18 ids: String,
20 output: String,
21 output_merged_tests: String,
22 supports_color: bool,
23 nb_tests: usize,
24}
25
26impl DocTestRunner {
27 pub(crate) fn new() -> Self {
28 Self {
29 crate_attrs: FxIndexSet::default(),
30 global_crate_attrs: FxIndexSet::default(),
31 ids: String::new(),
32 output: String::new(),
33 output_merged_tests: String::new(),
34 supports_color: true,
35 nb_tests: 0,
36 }
37 }
38
39 pub(crate) fn add_test(
40 &mut self,
41 doctest: &DocTestBuilder,
42 scraped_test: &ScrapedDocTest,
43 target_str: &str,
44 ) {
45 let ignore = match scraped_test.langstr.ignore {
46 Ignore::All => true,
47 Ignore::None => false,
48 Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
49 };
50 if !ignore {
51 for line in doctest.crate_attrs.split('\n') {
52 self.crate_attrs.insert(line.to_string());
53 }
54 for line in &doctest.global_crate_attrs {
55 self.global_crate_attrs.insert(line.to_string());
56 }
57 }
58 self.ids.push_str(&format!(
59 "&{}::TEST,\n",
60 generate_mergeable_doctest(
61 doctest,
62 scraped_test,
63 ignore,
64 self.nb_tests,
65 &mut self.output,
66 &mut self.output_merged_tests,
67 ),
68 ));
69 self.supports_color &= doctest.supports_color;
70 self.nb_tests += 1;
71 }
72
73 pub(crate) fn run_merged_tests(
78 &mut self,
79 test_options: IndividualTestOptions,
80 edition: Edition,
81 opts: &GlobalTestOptions,
82 test_args: &[String],
83 rustdoc_options: &RustdocOptions,
84 ) -> (Duration, Result<bool, ()>) {
85 let mut runner_code = "\
86#![allow(unused_extern_crates)]
87#![allow(internal_features)]
88#![feature(test)]
89#![feature(rustc_attrs)]
90"
91 .to_string();
92
93 let mut code_prefix = String::new();
94
95 for crate_attr in &self.crate_attrs {
96 code_prefix.push_str(crate_attr);
97 code_prefix.push('\n');
98 }
99
100 if self.global_crate_attrs.is_empty() {
101 code_prefix.push_str("#![allow(unused)]\n");
106 }
107
108 for attr in &self.global_crate_attrs {
110 code_prefix.push_str(&format!("#![{attr}]\n"));
111 }
112
113 runner_code.push_str("extern crate test;\n");
114 writeln!(runner_code, "extern crate doctest_bundle_{edition} as doctest_bundle;").unwrap();
115
116 let test_args = test_args.iter().fold(String::new(), |mut x, arg| {
117 write!(x, "{arg:?}.to_string(),").unwrap();
118 x
119 });
120 write!(
121 runner_code,
122 "\
123{output}
124
125mod __doctest_mod {{
126 use std::sync::OnceLock;
127 use std::path::PathBuf;
128 use std::process::ExitCode;
129
130 pub static BINARY_PATH: OnceLock<PathBuf> = OnceLock::new();
131 pub const RUN_OPTION: &str = \"RUSTDOC_DOCTEST_RUN_NB_TEST\";
132
133 #[allow(unused)]
134 pub fn doctest_path() -> Option<&'static PathBuf> {{
135 self::BINARY_PATH.get()
136 }}
137
138 #[allow(unused)]
139 pub fn doctest_runner(bin: &std::path::Path, test_nb: usize) -> ExitCode {{
140 let out = std::process::Command::new(bin)
141 .env(self::RUN_OPTION, test_nb.to_string())
142 .args(std::env::args().skip(1).collect::<Vec<_>>())
143 .output()
144 .expect(\"failed to run command\");
145 if !out.status.success() {{
146 if let Some(code) = out.status.code() {{
147 eprintln!(\"Test executable failed (exit status: {{code}}).\");
148 }} else {{
149 eprintln!(\"Test executable failed (terminated by signal).\");
150 }}
151 if !out.stdout.is_empty() || !out.stderr.is_empty() {{
152 eprintln!();
153 }}
154 if !out.stdout.is_empty() {{
155 eprintln!(\"stdout:\");
156 eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stdout));
157 }}
158 if !out.stderr.is_empty() {{
159 eprintln!(\"stderr:\");
160 eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stderr));
161 }}
162 ExitCode::FAILURE
163 }} else {{
164 ExitCode::SUCCESS
165 }}
166 }}
167}}
168
169#[rustc_main]
170fn main() -> std::process::ExitCode {{
171let tests = &[{ids}];
172let test_args = &[{test_args}];
173const ENV_BIN: &'static str = \"RUSTDOC_DOCTEST_BIN_PATH\";
174
175if let Ok(binary) = std::env::var(ENV_BIN) {{
176 let _ = crate::__doctest_mod::BINARY_PATH.set(binary.into());
177 unsafe {{ std::env::remove_var(ENV_BIN); }}
178 return test::test_main(test_args, tests);
179}} else if let Ok(nb_test) = std::env::var(__doctest_mod::RUN_OPTION) {{
180 if let Ok(nb_test) = nb_test.parse::<usize>() {{
181 if let Some(test) = tests.get(nb_test) {{
182 if let test::StaticTestFn(f) = &test.testfn {{
183 return std::process::Termination::report(f());
184 }}
185 }}
186 }}
187 panic!(\"Unexpected value for `{{}}`\", __doctest_mod::RUN_OPTION);
188}}
189
190eprintln!(\"WARNING: No rustdoc doctest environment variable provided so doctests will be run in \
191the same process\");
192test::test_main(test_args, tests)
193}}",
194 output = self.output_merged_tests,
195 ids = self.ids,
196 )
197 .expect("failed to generate test code");
198 let runnable_test = RunnableDocTest {
199 full_test_code: format!("{code_prefix}{code}", code = self.output),
200 full_test_line_offset: 0,
201 test_opts: test_options,
202 global_opts: opts,
203 langstr: LangString::default(),
204 line: 0,
205 edition,
206 no_run: false,
207 merged_test_runner_code: Some(runner_code),
208 };
209 let (duration, ret) =
210 run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {});
211 (duration, if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) })
212 }
213}
214
215fn generate_mergeable_doctest(
217 doctest: &DocTestBuilder,
218 scraped_test: &ScrapedDocTest,
219 ignore: bool,
220 id: usize,
221 output: &mut String,
222 output_merged_tests: &mut String,
223) -> String {
224 let test_id = format!("__doctest_{id}");
225
226 if ignore {
227 writeln!(output, "pub mod {test_id} {{}}\n").unwrap();
229 } else {
230 writeln!(output, "pub mod {test_id} {{\n{}{}", doctest.crates, doctest.module_attrs)
231 .unwrap();
232 if doctest.has_main_fn {
233 output.push_str(&doctest.everything_else);
234 } else {
235 let returns_result = if doctest.everything_else.trim_end().ends_with("(())") {
236 "-> Result<(), impl core::fmt::Debug>"
237 } else {
238 ""
239 };
240 write!(
241 output,
242 "\
243fn main() {returns_result} {{
244{}
245}}",
246 doctest.everything_else
247 )
248 .unwrap();
249 }
250 writeln!(
251 output,
252 "\npub fn __main_fn() -> impl std::process::Termination {{ main() }} \n}}\n"
253 )
254 .unwrap();
255 }
256 let not_running = ignore || scraped_test.langstr.no_run;
257 writeln!(
258 output_merged_tests,
259 "
260mod {test_id} {{
261pub static TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest(
262{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic},
263test::StaticTestFn(
264 || {{{runner}}},
265));
266}}",
267 test_name = scraped_test.name,
268 file = scraped_test.path(RemapPathScopeComponents::MACRO),
269 line = scraped_test.line,
270 no_run = scraped_test.langstr.no_run,
271 should_panic = !scraped_test.langstr.no_run && scraped_test.langstr.should_panic,
272 runner = if not_running {
275 "test::assert_test_result(Ok::<(), String>(()))".to_string()
276 } else {
277 format!(
278 "
279if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
280 test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}))
281}} else {{
282 test::assert_test_result(doctest_bundle::{test_id}::__main_fn())
283}}
284",
285 )
286 },
287 )
288 .unwrap();
289 test_id
290}