Skip to main content

rustdoc/doctest/
runner.rs

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
13/// Convenient type to merge compatible doctests into one.
14pub(crate) struct DocTestRunner {
15    crate_attrs: FxIndexSet<String>,
16    global_crate_attrs: FxIndexSet<String>,
17    /// A comma-separated list of references to test descriptors.
18    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    /// Returns a tuple containing the `Duration` of the compilation and the `Result` of the test.
73    ///
74    /// If compilation failed, it will return `Err`, otherwise it will return `Ok` containing if
75    /// the test ran successfully.
76    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            // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
101            // lints that are commonly triggered in doctests. The crate-level test attributes are
102            // commonly used to make tests fail in case they trigger warnings, so having this there in
103            // that case may cause some tests to pass when they shouldn't have.
104            code_prefix.push_str("#![allow(unused)]\n");
105        }
106
107        // Next, any attributes that came from #![doc(test(attr(...)))].
108        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
214/// Push new doctest content into `output`. Returns the test ID for this doctest.
215fn 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        // We generate nothing else.
227        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        // Setting `no_run` to `true` in `TestDesc` still makes the test run, so we simply
272        // don't give it the function to run.
273        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}