rustdoc/
doctest.rs

1mod extracted;
2mod make;
3mod markdown;
4mod runner;
5mod rust;
6
7use std::fs::File;
8use std::io::{self, Write};
9use std::path::{Path, PathBuf};
10use std::process::{self, Command, Stdio};
11use std::sync::atomic::{AtomicUsize, Ordering};
12use std::sync::{Arc, Mutex};
13use std::{panic, str};
14
15pub(crate) use make::DocTestBuilder;
16pub(crate) use markdown::test as test_markdown;
17use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet};
18use rustc_errors::emitter::HumanReadableErrorType;
19use rustc_errors::{ColorConfig, DiagCtxtHandle};
20use rustc_hir as hir;
21use rustc_hir::CRATE_HIR_ID;
22use rustc_hir::def_id::LOCAL_CRATE;
23use rustc_interface::interface;
24use rustc_session::config::{self, CrateType, ErrorOutputType, Input};
25use rustc_session::lint;
26use rustc_span::FileName;
27use rustc_span::edition::Edition;
28use rustc_span::symbol::sym;
29use rustc_target::spec::{Target, TargetTuple};
30use tempfile::{Builder as TempFileBuilder, TempDir};
31use tracing::debug;
32
33use self::rust::HirCollector;
34use crate::config::{Options as RustdocOptions, OutputFormat};
35use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine};
36use crate::lint::init_lints;
37
38/// Options that apply to all doctests in a crate or Markdown file (for `rustdoc foo.md`).
39#[derive(Clone)]
40pub(crate) struct GlobalTestOptions {
41    /// Name of the crate (for regular `rustdoc`) or Markdown file (for `rustdoc foo.md`).
42    pub(crate) crate_name: String,
43    /// Whether to disable the default `extern crate my_crate;` when creating doctests.
44    pub(crate) no_crate_inject: bool,
45    /// Whether inserting extra indent spaces in code block,
46    /// default is `false`, only `true` for generating code link of Rust playground
47    pub(crate) insert_indent_space: bool,
48    /// Additional crate-level attributes to add to doctests.
49    pub(crate) attrs: Vec<String>,
50    /// Path to file containing arguments for the invocation of rustc.
51    pub(crate) args_file: PathBuf,
52}
53
54/// Function used to split command line arguments just like a shell would.
55fn split_args(args: &str) -> Vec<String> {
56    let mut out = Vec::new();
57    let mut iter = args.chars();
58    let mut current = String::new();
59
60    while let Some(c) = iter.next() {
61        if c == '\\' {
62            if let Some(c) = iter.next() {
63                // If it's escaped, even a quote or a whitespace will be ignored.
64                current.push(c);
65            }
66        } else if c == '"' || c == '\'' {
67            while let Some(new_c) = iter.next() {
68                if new_c == c {
69                    break;
70                } else if new_c == '\\' {
71                    if let Some(c) = iter.next() {
72                        // If it's escaped, even a quote will be ignored.
73                        current.push(c);
74                    }
75                } else {
76                    current.push(new_c);
77                }
78            }
79        } else if " \n\t\r".contains(c) {
80            if !current.is_empty() {
81                out.push(current.clone());
82                current.clear();
83            }
84        } else {
85            current.push(c);
86        }
87    }
88    if !current.is_empty() {
89        out.push(current);
90    }
91    out
92}
93
94pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) -> Result<(), String> {
95    let mut file = File::create(file_path)
96        .map_err(|error| format!("failed to create args file: {error:?}"))?;
97
98    // We now put the common arguments into the file we created.
99    let mut content = vec![];
100
101    for cfg in &options.cfgs {
102        content.push(format!("--cfg={cfg}"));
103    }
104    for check_cfg in &options.check_cfgs {
105        content.push(format!("--check-cfg={check_cfg}"));
106    }
107
108    for lib_str in &options.lib_strs {
109        content.push(format!("-L{lib_str}"));
110    }
111    for extern_str in &options.extern_strs {
112        content.push(format!("--extern={extern_str}"));
113    }
114    content.push("-Ccodegen-units=1".to_string());
115    for codegen_options_str in &options.codegen_options_strs {
116        content.push(format!("-C{codegen_options_str}"));
117    }
118    for unstable_option_str in &options.unstable_opts_strs {
119        content.push(format!("-Z{unstable_option_str}"));
120    }
121
122    for compilation_args in &options.doctest_compilation_args {
123        content.extend(split_args(compilation_args));
124    }
125
126    let content = content.join("\n");
127
128    file.write_all(content.as_bytes())
129        .map_err(|error| format!("failed to write arguments to temporary file: {error:?}"))?;
130    Ok(())
131}
132
133fn get_doctest_dir() -> io::Result<TempDir> {
134    TempFileBuilder::new().prefix("rustdoctest").tempdir()
135}
136
137pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions) {
138    let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
139
140    // See core::create_config for what's going on here.
141    let allowed_lints = vec![
142        invalid_codeblock_attributes_name.to_owned(),
143        lint::builtin::UNKNOWN_LINTS.name.to_owned(),
144        lint::builtin::RENAMED_AND_REMOVED_LINTS.name.to_owned(),
145    ];
146
147    let (lint_opts, lint_caps) = init_lints(allowed_lints, options.lint_opts.clone(), |lint| {
148        if lint.name == invalid_codeblock_attributes_name {
149            None
150        } else {
151            Some((lint.name_lower(), lint::Allow))
152        }
153    });
154
155    debug!(?lint_opts);
156
157    let crate_types =
158        if options.proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] };
159
160    let sessopts = config::Options {
161        sysroot: options.sysroot.clone(),
162        search_paths: options.libs.clone(),
163        crate_types,
164        lint_opts,
165        lint_cap: Some(options.lint_cap.unwrap_or(lint::Forbid)),
166        cg: options.codegen_options.clone(),
167        externs: options.externs.clone(),
168        unstable_features: options.unstable_features,
169        actually_rustdoc: true,
170        edition: options.edition,
171        target_triple: options.target.clone(),
172        crate_name: options.crate_name.clone(),
173        remap_path_prefix: options.remap_path_prefix.clone(),
174        ..config::Options::default()
175    };
176
177    let mut cfgs = options.cfgs.clone();
178    cfgs.push("doc".to_owned());
179    cfgs.push("doctest".to_owned());
180    let config = interface::Config {
181        opts: sessopts,
182        crate_cfg: cfgs,
183        crate_check_cfg: options.check_cfgs.clone(),
184        input: input.clone(),
185        output_file: None,
186        output_dir: None,
187        file_loader: None,
188        locale_resources: rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
189        lint_caps,
190        psess_created: None,
191        hash_untracked_state: None,
192        register_lints: Some(Box::new(crate::lint::register_lints)),
193        override_queries: None,
194        extra_symbols: Vec::new(),
195        make_codegen_backend: None,
196        registry: rustc_driver::diagnostics_registry(),
197        ice_file: None,
198        using_internal_features: &rustc_driver::USING_INTERNAL_FEATURES,
199        expanded_args: options.expanded_args.clone(),
200    };
201
202    let externs = options.externs.clone();
203    let json_unused_externs = options.json_unused_externs;
204
205    let temp_dir = match get_doctest_dir()
206        .map_err(|error| format!("failed to create temporary directory: {error:?}"))
207    {
208        Ok(temp_dir) => temp_dir,
209        Err(error) => return crate::wrap_return(dcx, Err(error)),
210    };
211    let args_path = temp_dir.path().join("rustdoc-cfgs");
212    crate::wrap_return(dcx, generate_args_file(&args_path, &options));
213
214    let extract_doctests = options.output_format == OutputFormat::Doctest;
215    let result = interface::run_compiler(config, |compiler| {
216        let krate = rustc_interface::passes::parse(&compiler.sess);
217
218        let collector = rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
219            let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
220            let crate_attrs = tcx.hir_attrs(CRATE_HIR_ID);
221            let opts = scrape_test_config(crate_name, crate_attrs, args_path);
222
223            let hir_collector = HirCollector::new(
224                ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
225                tcx,
226            );
227            let tests = hir_collector.collect_crate();
228            if extract_doctests {
229                let mut collector = extracted::ExtractedDocTests::new();
230                tests.into_iter().for_each(|t| collector.add_test(t, &opts, &options));
231
232                let stdout = std::io::stdout();
233                let mut stdout = stdout.lock();
234                if let Err(error) = serde_json::ser::to_writer(&mut stdout, &collector) {
235                    eprintln!();
236                    Err(format!("Failed to generate JSON output for doctests: {error:?}"))
237                } else {
238                    Ok(None)
239                }
240            } else {
241                let mut collector = CreateRunnableDocTests::new(options, opts);
242                tests.into_iter().for_each(|t| collector.add_test(t));
243
244                Ok(Some(collector))
245            }
246        });
247        compiler.sess.dcx().abort_if_errors();
248
249        collector
250    });
251
252    let CreateRunnableDocTests {
253        standalone_tests,
254        mergeable_tests,
255        rustdoc_options,
256        opts,
257        unused_extern_reports,
258        compiling_test_count,
259        ..
260    } = match result {
261        Ok(Some(collector)) => collector,
262        Ok(None) => return,
263        Err(error) => {
264            eprintln!("{error}");
265            // Since some files in the temporary folder are still owned and alive, we need
266            // to manually remove the folder.
267            let _ = std::fs::remove_dir_all(temp_dir.path());
268            std::process::exit(1);
269        }
270    };
271
272    run_tests(
273        opts,
274        &rustdoc_options,
275        &unused_extern_reports,
276        standalone_tests,
277        mergeable_tests,
278        Some(temp_dir),
279    );
280
281    let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
282
283    // Collect and warn about unused externs, but only if we've gotten
284    // reports for each doctest
285    if json_unused_externs.is_enabled() {
286        let unused_extern_reports: Vec<_> =
287            std::mem::take(&mut unused_extern_reports.lock().unwrap());
288        if unused_extern_reports.len() == compiling_test_count {
289            let extern_names =
290                externs.iter().map(|(name, _)| name).collect::<FxIndexSet<&String>>();
291            let mut unused_extern_names = unused_extern_reports
292                .iter()
293                .map(|uexts| uexts.unused_extern_names.iter().collect::<FxIndexSet<&String>>())
294                .fold(extern_names, |uextsa, uextsb| {
295                    uextsa.intersection(&uextsb).copied().collect::<FxIndexSet<&String>>()
296                })
297                .iter()
298                .map(|v| (*v).clone())
299                .collect::<Vec<String>>();
300            unused_extern_names.sort();
301            // Take the most severe lint level
302            let lint_level = unused_extern_reports
303                .iter()
304                .map(|uexts| uexts.lint_level.as_str())
305                .max_by_key(|v| match *v {
306                    "warn" => 1,
307                    "deny" => 2,
308                    "forbid" => 3,
309                    // The allow lint level is not expected,
310                    // as if allow is specified, no message
311                    // is to be emitted.
312                    v => unreachable!("Invalid lint level '{v}'"),
313                })
314                .unwrap_or("warn")
315                .to_string();
316            let uext = UnusedExterns { lint_level, unused_extern_names };
317            let unused_extern_json = serde_json::to_string(&uext).unwrap();
318            eprintln!("{unused_extern_json}");
319        }
320    }
321}
322
323pub(crate) fn run_tests(
324    opts: GlobalTestOptions,
325    rustdoc_options: &Arc<RustdocOptions>,
326    unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
327    mut standalone_tests: Vec<test::TestDescAndFn>,
328    mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
329    // We pass this argument so we can drop it manually before using `exit`.
330    mut temp_dir: Option<TempDir>,
331) {
332    let mut test_args = Vec::with_capacity(rustdoc_options.test_args.len() + 1);
333    test_args.insert(0, "rustdoctest".to_string());
334    test_args.extend_from_slice(&rustdoc_options.test_args);
335    if rustdoc_options.nocapture {
336        test_args.push("--nocapture".to_string());
337    }
338
339    let mut nb_errors = 0;
340    let mut ran_edition_tests = 0;
341    let target_str = rustdoc_options.target.to_string();
342
343    for (edition, mut doctests) in mergeable_tests {
344        if doctests.is_empty() {
345            continue;
346        }
347        doctests.sort_by(|(_, a), (_, b)| a.name.cmp(&b.name));
348
349        let mut tests_runner = runner::DocTestRunner::new();
350
351        let rustdoc_test_options = IndividualTestOptions::new(
352            rustdoc_options,
353            &Some(format!("merged_doctest_{edition}")),
354            PathBuf::from(format!("doctest_{edition}.rs")),
355        );
356
357        for (doctest, scraped_test) in &doctests {
358            tests_runner.add_test(doctest, scraped_test, &target_str);
359        }
360        if let Ok(success) = tests_runner.run_merged_tests(
361            rustdoc_test_options,
362            edition,
363            &opts,
364            &test_args,
365            rustdoc_options,
366        ) {
367            ran_edition_tests += 1;
368            if !success {
369                nb_errors += 1;
370            }
371            continue;
372        }
373        // We failed to compile all compatible tests as one so we push them into the
374        // `standalone_tests` doctests.
375        debug!("Failed to compile compatible doctests for edition {} all at once", edition);
376        for (doctest, scraped_test) in doctests {
377            doctest.generate_unique_doctest(
378                &scraped_test.text,
379                scraped_test.langstr.test_harness,
380                &opts,
381                Some(&opts.crate_name),
382            );
383            standalone_tests.push(generate_test_desc_and_fn(
384                doctest,
385                scraped_test,
386                opts.clone(),
387                Arc::clone(rustdoc_options),
388                unused_extern_reports.clone(),
389            ));
390        }
391    }
392
393    // We need to call `test_main` even if there is no doctest to run to get the output
394    // `running 0 tests...`.
395    if ran_edition_tests == 0 || !standalone_tests.is_empty() {
396        standalone_tests.sort_by(|a, b| a.desc.name.as_slice().cmp(b.desc.name.as_slice()));
397        test::test_main_with_exit_callback(&test_args, standalone_tests, None, || {
398            // We ensure temp dir destructor is called.
399            std::mem::drop(temp_dir.take());
400        });
401    }
402    if nb_errors != 0 {
403        // We ensure temp dir destructor is called.
404        std::mem::drop(temp_dir);
405        // libtest::ERROR_EXIT_CODE is not public but it's the same value.
406        std::process::exit(101);
407    }
408}
409
410// Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade.
411fn scrape_test_config(
412    crate_name: String,
413    attrs: &[hir::Attribute],
414    args_file: PathBuf,
415) -> GlobalTestOptions {
416    use rustc_ast_pretty::pprust;
417
418    let mut opts = GlobalTestOptions {
419        crate_name,
420        no_crate_inject: false,
421        attrs: Vec::new(),
422        insert_indent_space: false,
423        args_file,
424    };
425
426    let test_attrs: Vec<_> = attrs
427        .iter()
428        .filter(|a| a.has_name(sym::doc))
429        .flat_map(|a| a.meta_item_list().unwrap_or_default())
430        .filter(|a| a.has_name(sym::test))
431        .collect();
432    let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[]));
433
434    for attr in attrs {
435        if attr.has_name(sym::no_crate_inject) {
436            opts.no_crate_inject = true;
437        }
438        if attr.has_name(sym::attr)
439            && let Some(l) = attr.meta_item_list()
440        {
441            for item in l {
442                opts.attrs.push(pprust::meta_list_item_to_string(item));
443            }
444        }
445    }
446
447    opts
448}
449
450/// Documentation test failure modes.
451enum TestFailure {
452    /// The test failed to compile.
453    CompileError,
454    /// The test is marked `compile_fail` but compiled successfully.
455    UnexpectedCompilePass,
456    /// The test failed to compile (as expected) but the compiler output did not contain all
457    /// expected error codes.
458    MissingErrorCodes(Vec<String>),
459    /// The test binary was unable to be executed.
460    ExecutionError(io::Error),
461    /// The test binary exited with a non-zero exit code.
462    ///
463    /// This typically means an assertion in the test failed or another form of panic occurred.
464    ExecutionFailure(process::Output),
465    /// The test is marked `should_panic` but the test binary executed successfully.
466    UnexpectedRunPass,
467}
468
469enum DirState {
470    Temp(TempDir),
471    Perm(PathBuf),
472}
473
474impl DirState {
475    fn path(&self) -> &std::path::Path {
476        match self {
477            DirState::Temp(t) => t.path(),
478            DirState::Perm(p) => p.as_path(),
479        }
480    }
481}
482
483// NOTE: Keep this in sync with the equivalent structs in rustc
484// and cargo.
485// We could unify this struct the one in rustc but they have different
486// ownership semantics, so doing so would create wasteful allocations.
487#[derive(serde::Serialize, serde::Deserialize)]
488pub(crate) struct UnusedExterns {
489    /// Lint level of the unused_crate_dependencies lint
490    lint_level: String,
491    /// List of unused externs by their names.
492    unused_extern_names: Vec<String>,
493}
494
495fn add_exe_suffix(input: String, target: &TargetTuple) -> String {
496    let exe_suffix = match target {
497        TargetTuple::TargetTuple(_) => Target::expect_builtin(target).options.exe_suffix,
498        TargetTuple::TargetJson { contents, .. } => {
499            Target::from_json(contents.parse().unwrap()).unwrap().0.options.exe_suffix
500        }
501    };
502    input + &exe_suffix
503}
504
505fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Command {
506    let mut args = rustc_wrappers.iter().map(PathBuf::as_path).chain([rustc_binary]);
507
508    let exe = args.next().expect("unable to create rustc command");
509    let mut command = Command::new(exe);
510    for arg in args {
511        command.arg(arg);
512    }
513
514    command
515}
516
517/// Information needed for running a bundle of doctests.
518///
519/// This data structure contains the "full" test code, including the wrappers
520/// (if multiple doctests are merged), `main` function,
521/// and everything needed to calculate the compiler's command-line arguments.
522/// The `# ` prefix on boring lines has also been stripped.
523pub(crate) struct RunnableDocTest {
524    full_test_code: String,
525    full_test_line_offset: usize,
526    test_opts: IndividualTestOptions,
527    global_opts: GlobalTestOptions,
528    langstr: LangString,
529    line: usize,
530    edition: Edition,
531    no_run: bool,
532    merged_test_code: Option<String>,
533}
534
535impl RunnableDocTest {
536    fn path_for_merged_doctest_bundle(&self) -> PathBuf {
537        self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition))
538    }
539    fn path_for_merged_doctest_runner(&self) -> PathBuf {
540        self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition))
541    }
542    fn is_multiple_tests(&self) -> bool {
543        self.merged_test_code.is_some()
544    }
545}
546
547/// Execute a `RunnableDoctest`.
548///
549/// This is the function that calculates the compiler command line, invokes the compiler, then
550/// invokes the test or tests in a separate executable (if applicable).
551fn run_test(
552    doctest: RunnableDocTest,
553    rustdoc_options: &RustdocOptions,
554    supports_color: bool,
555    report_unused_externs: impl Fn(UnusedExterns),
556) -> Result<(), TestFailure> {
557    let langstr = &doctest.langstr;
558    // Make sure we emit well-formed executable names for our target.
559    let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
560    let output_file = doctest.test_opts.outdir.path().join(rust_out);
561
562    // Common arguments used for compiling the doctest runner.
563    // On merged doctests, the compiler is invoked twice: once for the test code itself,
564    // and once for the runner wrapper (which needs to use `#![feature]` on stable).
565    let mut compiler_args = vec![];
566
567    compiler_args.push(format!("@{}", doctest.global_opts.args_file.display()));
568
569    if let Some(sysroot) = &rustdoc_options.maybe_sysroot {
570        compiler_args.push(format!("--sysroot={}", sysroot.display()));
571    }
572
573    compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]);
574    if langstr.test_harness {
575        compiler_args.push("--test".to_owned());
576    }
577    if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail {
578        compiler_args.push("--error-format=json".to_owned());
579        compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]);
580        compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]);
581        compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]);
582    }
583
584    if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
585        // FIXME: why does this code check if it *shouldn't* persist doctests
586        //        -- shouldn't it be the negation?
587        compiler_args.push("--emit=metadata".to_owned());
588    }
589    compiler_args.extend_from_slice(&[
590        "--target".to_owned(),
591        match &rustdoc_options.target {
592            TargetTuple::TargetTuple(s) => s.clone(),
593            TargetTuple::TargetJson { path_for_rustdoc, .. } => {
594                path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned()
595            }
596        },
597    ]);
598    if let ErrorOutputType::HumanReadable { kind, color_config } = rustdoc_options.error_format {
599        let short = kind.short();
600        let unicode = kind == HumanReadableErrorType::Unicode;
601
602        if short {
603            compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]);
604        }
605        if unicode {
606            compiler_args
607                .extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]);
608        }
609
610        match color_config {
611            ColorConfig::Never => {
612                compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]);
613            }
614            ColorConfig::Always => {
615                compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]);
616            }
617            ColorConfig::Auto => {
618                compiler_args.extend_from_slice(&[
619                    "--color".to_owned(),
620                    if supports_color { "always" } else { "never" }.to_owned(),
621                ]);
622            }
623        }
624    }
625
626    let rustc_binary = rustdoc_options
627        .test_builder
628        .as_deref()
629        .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
630    let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
631
632    compiler.args(&compiler_args);
633
634    // If this is a merged doctest, we need to write it into a file instead of using stdin
635    // because if the size of the merged doctests is too big, it'll simply break stdin.
636    if doctest.is_multiple_tests() {
637        // It makes the compilation failure much faster if it is for a combined doctest.
638        compiler.arg("--error-format=short");
639        let input_file = doctest.path_for_merged_doctest_bundle();
640        if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
641            // If we cannot write this file for any reason, we leave. All combined tests will be
642            // tested as standalone tests.
643            return Err(TestFailure::CompileError);
644        }
645        if !rustdoc_options.nocapture {
646            // If `nocapture` is disabled, then we don't display rustc's output when compiling
647            // the merged doctests.
648            compiler.stderr(Stdio::null());
649        }
650        // bundled tests are an rlib, loaded by a separate runner executable
651        compiler
652            .arg("--crate-type=lib")
653            .arg("--out-dir")
654            .arg(doctest.test_opts.outdir.path())
655            .arg(input_file);
656    } else {
657        compiler.arg("--crate-type=bin").arg("-o").arg(&output_file);
658        // Setting these environment variables is unneeded if this is a merged doctest.
659        compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
660        compiler.env(
661            "UNSTABLE_RUSTDOC_TEST_LINE",
662            format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
663        );
664        compiler.arg("-");
665        compiler.stdin(Stdio::piped());
666        compiler.stderr(Stdio::piped());
667    }
668
669    debug!("compiler invocation for doctest: {compiler:?}");
670
671    let mut child = compiler.spawn().expect("Failed to spawn rustc process");
672    let output = if let Some(merged_test_code) = &doctest.merged_test_code {
673        // compile-fail tests never get merged, so this should always pass
674        let status = child.wait().expect("Failed to wait");
675
676        // the actual test runner is a separate component, built with nightly-only features;
677        // build it now
678        let runner_input_file = doctest.path_for_merged_doctest_runner();
679
680        let mut runner_compiler =
681            wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
682        // the test runner does not contain any user-written code, so this doesn't allow
683        // the user to exploit nightly-only features on stable
684        runner_compiler.env("RUSTC_BOOTSTRAP", "1");
685        runner_compiler.args(compiler_args);
686        runner_compiler.args(&["--crate-type=bin", "-o"]).arg(&output_file);
687        let mut extern_path = std::ffi::OsString::from(format!(
688            "--extern=doctest_bundle_{edition}=",
689            edition = doctest.edition
690        ));
691        for extern_str in &rustdoc_options.extern_strs {
692            if let Some((_cratename, path)) = extern_str.split_once('=') {
693                // Direct dependencies of the tests themselves are
694                // indirect dependencies of the test runner.
695                // They need to be in the library search path.
696                let dir = Path::new(path)
697                    .parent()
698                    .filter(|x| x.components().count() > 0)
699                    .unwrap_or(Path::new("."));
700                runner_compiler.arg("-L").arg(dir);
701            }
702        }
703        let output_bundle_file = doctest
704            .test_opts
705            .outdir
706            .path()
707            .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition));
708        extern_path.push(&output_bundle_file);
709        runner_compiler.arg(extern_path);
710        runner_compiler.arg(&runner_input_file);
711        if std::fs::write(&runner_input_file, &merged_test_code).is_err() {
712            // If we cannot write this file for any reason, we leave. All combined tests will be
713            // tested as standalone tests.
714            return Err(TestFailure::CompileError);
715        }
716        if !rustdoc_options.nocapture {
717            // If `nocapture` is disabled, then we don't display rustc's output when compiling
718            // the merged doctests.
719            runner_compiler.stderr(Stdio::null());
720        }
721        runner_compiler.arg("--error-format=short");
722        debug!("compiler invocation for doctest runner: {runner_compiler:?}");
723
724        let status = if !status.success() {
725            status
726        } else {
727            let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process");
728            child_runner.wait().expect("Failed to wait")
729        };
730
731        process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
732    } else {
733        let stdin = child.stdin.as_mut().expect("Failed to open stdin");
734        stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources");
735        child.wait_with_output().expect("Failed to read stdout")
736    };
737
738    struct Bomb<'a>(&'a str);
739    impl Drop for Bomb<'_> {
740        fn drop(&mut self) {
741            eprint!("{}", self.0);
742        }
743    }
744    let mut out = str::from_utf8(&output.stderr)
745        .unwrap()
746        .lines()
747        .filter(|l| {
748            if let Ok(uext) = serde_json::from_str::<UnusedExterns>(l) {
749                report_unused_externs(uext);
750                false
751            } else {
752                true
753            }
754        })
755        .intersperse_with(|| "\n")
756        .collect::<String>();
757
758    // Add a \n to the end to properly terminate the last line,
759    // but only if there was output to be printed
760    if !out.is_empty() {
761        out.push('\n');
762    }
763
764    let _bomb = Bomb(&out);
765    match (output.status.success(), langstr.compile_fail) {
766        (true, true) => {
767            return Err(TestFailure::UnexpectedCompilePass);
768        }
769        (true, false) => {}
770        (false, true) => {
771            if !langstr.error_codes.is_empty() {
772                // We used to check if the output contained "error[{}]: " but since we added the
773                // colored output, we can't anymore because of the color escape characters before
774                // the ":".
775                let missing_codes: Vec<String> = langstr
776                    .error_codes
777                    .iter()
778                    .filter(|err| !out.contains(&format!("error[{err}]")))
779                    .cloned()
780                    .collect();
781
782                if !missing_codes.is_empty() {
783                    return Err(TestFailure::MissingErrorCodes(missing_codes));
784                }
785            }
786        }
787        (false, false) => {
788            return Err(TestFailure::CompileError);
789        }
790    }
791
792    if doctest.no_run {
793        return Ok(());
794    }
795
796    // Run the code!
797    let mut cmd;
798
799    let output_file = make_maybe_absolute_path(output_file);
800    if let Some(tool) = &rustdoc_options.test_runtool {
801        let tool = make_maybe_absolute_path(tool.into());
802        cmd = Command::new(tool);
803        cmd.args(&rustdoc_options.test_runtool_args);
804        cmd.arg(&output_file);
805    } else {
806        cmd = Command::new(&output_file);
807        if doctest.is_multiple_tests() {
808            cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file);
809        }
810    }
811    if let Some(run_directory) = &rustdoc_options.test_run_directory {
812        cmd.current_dir(run_directory);
813    }
814
815    let result = if doctest.is_multiple_tests() || rustdoc_options.nocapture {
816        cmd.status().map(|status| process::Output {
817            status,
818            stdout: Vec::new(),
819            stderr: Vec::new(),
820        })
821    } else {
822        cmd.output()
823    };
824    match result {
825        Err(e) => return Err(TestFailure::ExecutionError(e)),
826        Ok(out) => {
827            if langstr.should_panic && out.status.success() {
828                return Err(TestFailure::UnexpectedRunPass);
829            } else if !langstr.should_panic && !out.status.success() {
830                return Err(TestFailure::ExecutionFailure(out));
831            }
832        }
833    }
834
835    Ok(())
836}
837
838/// Converts a path intended to use as a command to absolute if it is
839/// relative, and not a single component.
840///
841/// This is needed to deal with relative paths interacting with
842/// `Command::current_dir` in a platform-specific way.
843fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
844    if path.components().count() == 1 {
845        // Look up process via PATH.
846        path
847    } else {
848        std::env::current_dir().map(|c| c.join(&path)).unwrap_or_else(|_| path)
849    }
850}
851struct IndividualTestOptions {
852    outdir: DirState,
853    path: PathBuf,
854}
855
856impl IndividualTestOptions {
857    fn new(options: &RustdocOptions, test_id: &Option<String>, test_path: PathBuf) -> Self {
858        let outdir = if let Some(ref path) = options.persist_doctests {
859            let mut path = path.clone();
860            path.push(test_id.as_deref().unwrap_or("<doctest>"));
861
862            if let Err(err) = std::fs::create_dir_all(&path) {
863                eprintln!("Couldn't create directory for doctest executables: {err}");
864                panic::resume_unwind(Box::new(()));
865            }
866
867            DirState::Perm(path)
868        } else {
869            DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir"))
870        };
871
872        Self { outdir, path: test_path }
873    }
874}
875
876/// A doctest scraped from the code, ready to be turned into a runnable test.
877///
878/// The pipeline goes: [`clean`] AST -> `ScrapedDoctest` -> `RunnableDoctest`.
879/// [`run_merged_tests`] converts a bunch of scraped doctests to a single runnable doctest,
880/// while [`generate_unique_doctest`] does the standalones.
881///
882/// [`clean`]: crate::clean
883/// [`run_merged_tests`]: crate::doctest::runner::DocTestRunner::run_merged_tests
884/// [`generate_unique_doctest`]: crate::doctest::make::DocTestBuilder::generate_unique_doctest
885#[derive(Debug)]
886pub(crate) struct ScrapedDocTest {
887    filename: FileName,
888    line: usize,
889    langstr: LangString,
890    text: String,
891    name: String,
892}
893
894impl ScrapedDocTest {
895    fn new(
896        filename: FileName,
897        line: usize,
898        logical_path: Vec<String>,
899        langstr: LangString,
900        text: String,
901    ) -> Self {
902        let mut item_path = logical_path.join("::");
903        item_path.retain(|c| c != ' ');
904        if !item_path.is_empty() {
905            item_path.push(' ');
906        }
907        let name =
908            format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
909
910        Self { filename, line, langstr, text, name }
911    }
912    fn edition(&self, opts: &RustdocOptions) -> Edition {
913        self.langstr.edition.unwrap_or(opts.edition)
914    }
915
916    fn no_run(&self, opts: &RustdocOptions) -> bool {
917        self.langstr.no_run || opts.no_run
918    }
919    fn path(&self) -> PathBuf {
920        match &self.filename {
921            FileName::Real(path) => {
922                if let Some(local_path) = path.local_path() {
923                    local_path.to_path_buf()
924                } else {
925                    // Somehow we got the filename from the metadata of another crate, should never happen
926                    unreachable!("doctest from a different crate");
927                }
928            }
929            _ => PathBuf::from(r"doctest.rs"),
930        }
931    }
932}
933
934pub(crate) trait DocTestVisitor {
935    fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine);
936    fn visit_header(&mut self, _name: &str, _level: u32) {}
937}
938
939struct CreateRunnableDocTests {
940    standalone_tests: Vec<test::TestDescAndFn>,
941    mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
942
943    rustdoc_options: Arc<RustdocOptions>,
944    opts: GlobalTestOptions,
945    visited_tests: FxHashMap<(String, usize), usize>,
946    unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
947    compiling_test_count: AtomicUsize,
948    can_merge_doctests: bool,
949}
950
951impl CreateRunnableDocTests {
952    fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests {
953        let can_merge_doctests = rustdoc_options.edition >= Edition::Edition2024;
954        CreateRunnableDocTests {
955            standalone_tests: Vec::new(),
956            mergeable_tests: FxIndexMap::default(),
957            rustdoc_options: Arc::new(rustdoc_options),
958            opts,
959            visited_tests: FxHashMap::default(),
960            unused_extern_reports: Default::default(),
961            compiling_test_count: AtomicUsize::new(0),
962            can_merge_doctests,
963        }
964    }
965
966    fn add_test(&mut self, scraped_test: ScrapedDocTest) {
967        // For example `module/file.rs` would become `module_file_rs`
968        let file = scraped_test
969            .filename
970            .prefer_local()
971            .to_string_lossy()
972            .chars()
973            .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
974            .collect::<String>();
975        let test_id = format!(
976            "{file}_{line}_{number}",
977            file = file,
978            line = scraped_test.line,
979            number = {
980                // Increases the current test number, if this file already
981                // exists or it creates a new entry with a test number of 0.
982                self.visited_tests
983                    .entry((file.clone(), scraped_test.line))
984                    .and_modify(|v| *v += 1)
985                    .or_insert(0)
986            },
987        );
988
989        let edition = scraped_test.edition(&self.rustdoc_options);
990        let doctest = DocTestBuilder::new(
991            &scraped_test.text,
992            Some(&self.opts.crate_name),
993            edition,
994            self.can_merge_doctests,
995            Some(test_id),
996            Some(&scraped_test.langstr),
997        );
998        let is_standalone = !doctest.can_be_merged
999            || scraped_test.langstr.compile_fail
1000            || scraped_test.langstr.test_harness
1001            || scraped_test.langstr.standalone_crate
1002            || self.rustdoc_options.nocapture
1003            || self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output");
1004        if is_standalone {
1005            let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
1006            self.standalone_tests.push(test_desc);
1007        } else {
1008            self.mergeable_tests.entry(edition).or_default().push((doctest, scraped_test));
1009        }
1010    }
1011
1012    fn generate_test_desc_and_fn(
1013        &mut self,
1014        test: DocTestBuilder,
1015        scraped_test: ScrapedDocTest,
1016    ) -> test::TestDescAndFn {
1017        if !scraped_test.langstr.compile_fail {
1018            self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
1019        }
1020
1021        generate_test_desc_and_fn(
1022            test,
1023            scraped_test,
1024            self.opts.clone(),
1025            Arc::clone(&self.rustdoc_options),
1026            self.unused_extern_reports.clone(),
1027        )
1028    }
1029}
1030
1031fn generate_test_desc_and_fn(
1032    test: DocTestBuilder,
1033    scraped_test: ScrapedDocTest,
1034    opts: GlobalTestOptions,
1035    rustdoc_options: Arc<RustdocOptions>,
1036    unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
1037) -> test::TestDescAndFn {
1038    let target_str = rustdoc_options.target.to_string();
1039    let rustdoc_test_options =
1040        IndividualTestOptions::new(&rustdoc_options, &test.test_id, scraped_test.path());
1041
1042    debug!("creating test {}: {}", scraped_test.name, scraped_test.text);
1043    test::TestDescAndFn {
1044        desc: test::TestDesc {
1045            name: test::DynTestName(scraped_test.name.clone()),
1046            ignore: match scraped_test.langstr.ignore {
1047                Ignore::All => true,
1048                Ignore::None => false,
1049                Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
1050            },
1051            ignore_message: None,
1052            source_file: "",
1053            start_line: 0,
1054            start_col: 0,
1055            end_line: 0,
1056            end_col: 0,
1057            // compiler failures are test failures
1058            should_panic: test::ShouldPanic::No,
1059            compile_fail: scraped_test.langstr.compile_fail,
1060            no_run: scraped_test.no_run(&rustdoc_options),
1061            test_type: test::TestType::DocTest,
1062        },
1063        testfn: test::DynTestFn(Box::new(move || {
1064            doctest_run_fn(
1065                rustdoc_test_options,
1066                opts,
1067                test,
1068                scraped_test,
1069                rustdoc_options,
1070                unused_externs,
1071            )
1072        })),
1073    }
1074}
1075
1076fn doctest_run_fn(
1077    test_opts: IndividualTestOptions,
1078    global_opts: GlobalTestOptions,
1079    doctest: DocTestBuilder,
1080    scraped_test: ScrapedDocTest,
1081    rustdoc_options: Arc<RustdocOptions>,
1082    unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
1083) -> Result<(), String> {
1084    let report_unused_externs = |uext| {
1085        unused_externs.lock().unwrap().push(uext);
1086    };
1087    let (full_test_code, full_test_line_offset) = doctest.generate_unique_doctest(
1088        &scraped_test.text,
1089        scraped_test.langstr.test_harness,
1090        &global_opts,
1091        Some(&global_opts.crate_name),
1092    );
1093    let runnable_test = RunnableDocTest {
1094        full_test_code,
1095        full_test_line_offset,
1096        test_opts,
1097        global_opts,
1098        langstr: scraped_test.langstr.clone(),
1099        line: scraped_test.line,
1100        edition: scraped_test.edition(&rustdoc_options),
1101        no_run: scraped_test.no_run(&rustdoc_options),
1102        merged_test_code: None,
1103    };
1104    let res =
1105        run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
1106
1107    if let Err(err) = res {
1108        match err {
1109            TestFailure::CompileError => {
1110                eprint!("Couldn't compile the test.");
1111            }
1112            TestFailure::UnexpectedCompilePass => {
1113                eprint!("Test compiled successfully, but it's marked `compile_fail`.");
1114            }
1115            TestFailure::UnexpectedRunPass => {
1116                eprint!("Test executable succeeded, but it's marked `should_panic`.");
1117            }
1118            TestFailure::MissingErrorCodes(codes) => {
1119                eprint!("Some expected error codes were not found: {codes:?}");
1120            }
1121            TestFailure::ExecutionError(err) => {
1122                eprint!("Couldn't run the test: {err}");
1123                if err.kind() == io::ErrorKind::PermissionDenied {
1124                    eprint!(" - maybe your tempdir is mounted with noexec?");
1125                }
1126            }
1127            TestFailure::ExecutionFailure(out) => {
1128                eprintln!("Test executable failed ({reason}).", reason = out.status);
1129
1130                // FIXME(#12309): An unfortunate side-effect of capturing the test
1131                // executable's output is that the relative ordering between the test's
1132                // stdout and stderr is lost. However, this is better than the
1133                // alternative: if the test executable inherited the parent's I/O
1134                // handles the output wouldn't be captured at all, even on success.
1135                //
1136                // The ordering could be preserved if the test process' stderr was
1137                // redirected to stdout, but that functionality does not exist in the
1138                // standard library, so it may not be portable enough.
1139                let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
1140                let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
1141
1142                if !stdout.is_empty() || !stderr.is_empty() {
1143                    eprintln!();
1144
1145                    if !stdout.is_empty() {
1146                        eprintln!("stdout:\n{stdout}");
1147                    }
1148
1149                    if !stderr.is_empty() {
1150                        eprintln!("stderr:\n{stderr}");
1151                    }
1152                }
1153            }
1154        }
1155
1156        panic::resume_unwind(Box::new(()));
1157    }
1158    Ok(())
1159}
1160
1161#[cfg(test)] // used in tests
1162impl DocTestVisitor for Vec<usize> {
1163    fn visit_test(&mut self, _test: String, _config: LangString, rel_line: MdRelLine) {
1164        self.push(1 + rel_line.offset());
1165    }
1166}
1167
1168#[cfg(test)]
1169mod tests;