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