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