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