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