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