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