Skip to main content

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