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