runner.rs - source (original) (raw)
rustdoc/doctest/
runner.rs
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
12/// Convenient type to merge compatible doctests into one.
13pub(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 self.ids.push_str(&format!(
51 "tests.push({}::TEST);\n",
52 generate_mergeable_doctest(
53 doctest,
54 scraped_test,
55 ignore,
56 self.nb_tests,
57 &mut self.output,
58 &mut self.output_merged_tests,
59 ),
60 ));
61 self.supports_color &= doctest.supports_color;
62 self.nb_tests += 1;
63 }
64
65 pub(crate) fn run_merged_tests(
66 &mut self,
67 test_options: IndividualTestOptions,
68 edition: Edition,
69 opts: &GlobalTestOptions,
70 test_args: &[String],
71 rustdoc_options: &RustdocOptions,
72 ) -> Result<bool, ()> {
73 let mut code = "\
74#![allow(unused_extern_crates)]
75#![allow(internal_features)]
76#![feature(test)]
77#![feature(rustc_attrs)]
78"
79 .to_string();
80
81 let mut code_prefix = String::new();
82
83 for crate_attr in &self.crate_attrs {
84 code_prefix.push_str(crate_attr);
85 code_prefix.push('\n');
86 }
87
88 if opts.attrs.is_empty() {
89 // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
90 // lints that are commonly triggered in doctests. The crate-level test attributes are
91 // commonly used to make tests fail in case they trigger warnings, so having this there in
92 // that case may cause some tests to pass when they shouldn't have.
93 code_prefix.push_str("#![allow(unused)]\n");
94 }
95
96 // Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
97 for attr in &opts.attrs {
98 code_prefix.push_str(&format!("#![{attr}]\n"));
99 }
100
101 code.push_str("extern crate test;\n");
102 writeln!(code, "extern crate doctest_bundle_{edition} as doctest_bundle;").unwrap();
103
104 let test_args = test_args.iter().fold(String::new(), |mut x, arg| {
105 write!(x, "{arg:?}.to_string(),").unwrap();
106 x
107 });
108 write!(
109 code,
110 "\
111{output}
112
113mod __doctest_mod {{
114 use std::sync::OnceLock;
115 use std::path::PathBuf;
116 use std::process::ExitCode;
117
118 pub static BINARY_PATH: OnceLock<PathBuf> = OnceLock::new();
119 pub const RUN_OPTION: &str = \"RUSTDOC_DOCTEST_RUN_NB_TEST\";
120
121 #[allow(unused)]
122 pub fn doctest_path() -> Option<&'static PathBuf> {{
123 self::BINARY_PATH.get()
124 }}
125
126 #[allow(unused)]
127 pub fn doctest_runner(bin: &std::path::Path, test_nb: usize) -> ExitCode {{
128 let out = std::process::Command::new(bin)
129 .env(self::RUN_OPTION, test_nb.to_string())
130 .args(std::env::args().skip(1).collect::<Vec<_>>())
131 .output()
132 .expect(\"failed to run command\");
133 if !out.status.success() {{
134 if let Some(code) = out.status.code() {{
135 eprintln!(\"Test executable failed (exit status: {{code}}).\");
136 }} else {{
137 eprintln!(\"Test executable failed (terminated by signal).\");
138 }}
139 if !out.stdout.is_empty() || !out.stderr.is_empty() {{
140 eprintln!();
141 }}
142 if !out.stdout.is_empty() {{
143 eprintln!(\"stdout:\");
144 eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stdout));
145 }}
146 if !out.stderr.is_empty() {{
147 eprintln!(\"stderr:\");
148 eprintln!(\"{{}}\", String::from_utf8_lossy(&out.stderr));
149 }}
150 ExitCode::FAILURE
151 }} else {{
152 ExitCode::SUCCESS
153 }}
154 }}
155}}
156
157#[rustc_main]
158fn main() -> std::process::ExitCode {{
159let tests = {{
160 let mut tests = Vec::with_capacity({nb_tests});
161 {ids}
162 tests
163}};
164let test_args = &[{test_args}];
165const ENV_BIN: &'static str = \"RUSTDOC_DOCTEST_BIN_PATH\";
166
167if let Ok(binary) = std::env::var(ENV_BIN) {{
168 let _ = crate::__doctest_mod::BINARY_PATH.set(binary.into());
169 unsafe {{ std::env::remove_var(ENV_BIN); }}
170 return std::process::Termination::report(test::test_main(test_args, tests, None));
171}} else if let Ok(nb_test) = std::env::var(__doctest_mod::RUN_OPTION) {{
172 if let Ok(nb_test) = nb_test.parse::<usize>() {{
173 if let Some(test) = tests.get(nb_test) {{
174 if let test::StaticTestFn(f) = &test.testfn {{
175 return std::process::Termination::report(f());
176 }}
177 }}
178 }}
179 panic!(\"Unexpected value for `{{}}`\", __doctest_mod::RUN_OPTION);
180}}
181
182eprintln!(\"WARNING: No rustdoc doctest environment variable provided so doctests will be run in \
183the same process\");
184std::process::Termination::report(test::test_main(test_args, tests, None))
185}}",
186 nb_tests = self.nb_tests,
187 output = self.output_merged_tests,
188 ids = self.ids,
189 )
190 .expect("failed to generate test code");
191 let runnable_test = RunnableDocTest {
192 full_test_code: format!("{code_prefix}{code}", code = self.output),
193 full_test_line_offset: 0,
194 test_opts: test_options,
195 global_opts: opts.clone(),
196 langstr: LangString::default(),
197 line: 0,
198 edition,
199 no_run: false,
200 merged_test_code: Some(code),
201 };
202 let ret =
203 run_test(runnable_test, rustdoc_options, self.supports_color, |_: UnusedExterns| {});
204 if let Err(TestFailure::CompileError) = ret { Err(()) } else { Ok(ret.is_ok()) }
205 }
206}
207
208/// Push new doctest content into `output`. Returns the test ID for this doctest.
209fn generate_mergeable_doctest(
210 doctest: &DocTestBuilder,
211 scraped_test: &ScrapedDocTest,
212 ignore: bool,
213 id: usize,
214 output: &mut String,
215 output_merged_tests: &mut String,
216) -> String {
217 let test_id = format!("__doctest_{id}");
218
219 if ignore {
220 // We generate nothing else.
221 writeln!(output, "pub mod {test_id} {{}}\n").unwrap();
222 } else {
223 writeln!(output, "pub mod {test_id} {{\n{}{}", doctest.crates, doctest.maybe_crate_attrs)
224 .unwrap();
225 if doctest.has_main_fn {
226 output.push_str(&doctest.everything_else);
227 } else {
228 let returns_result = if doctest.everything_else.trim_end().ends_with("(())") {
229 "-> Result<(), impl core::fmt::Debug>"
230 } else {
231 ""
232 };
233 write!(
234 output,
235 "\
236fn main() {returns_result} {{
237{}
238}}",
239 doctest.everything_else
240 )
241 .unwrap();
242 }
243 writeln!(
244 output,
245 "\npub fn __main_fn() -> impl std::process::Termination {{ main() }} \n}}\n"
246 )
247 .unwrap();
248 }
249 let not_running = ignore || scraped_test.langstr.no_run;
250 writeln!(
251 output_merged_tests,
252 "
253mod {test_id} {{
254pub const TEST: test::TestDescAndFn = test::TestDescAndFn::new_doctest(
255{test_name:?}, {ignore}, {file:?}, {line}, {no_run}, {should_panic},
256test::StaticTestFn(
257 || {{{runner}}},
258));
259}}",
260 test_name = scraped_test.name,
261 file = scraped_test.path(),
262 line = scraped_test.line,
263 no_run = scraped_test.langstr.no_run,
264 should_panic = !scraped_test.langstr.no_run && scraped_test.langstr.should_panic,
265 // Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply
266 // don't give it the function to run.
267 runner = if not_running {
268 "test::assert_test_result(Ok::<(), String>(()))".to_string()
269 } else {
270 format!(
271 "
272if let Some(bin_path) = crate::__doctest_mod::doctest_path() {{
273 test::assert_test_result(crate::__doctest_mod::doctest_runner(bin_path, {id}))
274}} else {{
275 test::assert_test_result(doctest_bundle::{test_id}::__main_fn())
276}}
277",
278 )
279 },
280 )
281 .unwrap();
282 test_id
283}