rustdoc/
doctest.rs

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