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
43struct MergedDoctestTimes {
45 total_time: Instant,
46 compilation_time: Duration,
48 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 fn times_in_secs(&self) -> Option<(f64, f64)> {
68 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#[derive(Clone)]
80pub(crate) struct GlobalTestOptions {
81 pub(crate) crate_name: String,
83 pub(crate) no_crate_inject: bool,
85 pub(crate) insert_indent_space: bool,
88 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if exit != std::process::ExitCode::SUCCESS {
450 exit.exit_process();
451 }
452 }
453 }
454 } else {
455 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 std::mem::drop(temp_dir);
464 if nb_errors != 0 {
465 std::process::exit(test::ERROR_EXIT_CODE.into());
466 }
467}
468
469fn 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 if let Ok(snippet) = source_map.span_to_snippet(*attr_span)
488 && let Ok(stream) = TokenStream::from_str(&snippet)
489 {
490 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
507enum TestFailure {
509 CompileError,
511 UnexpectedCompilePass,
513 MissingErrorCodes(Vec<String>),
516 ExecutionError(io::Error),
518 ExecutionFailure(process::Output),
522 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#[derive(serde::Serialize, serde::Deserialize)]
545pub(crate) struct UnusedExterns {
546 lint_level: String,
548 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
574pub(crate) struct RunnableDocTest<'a> {
581 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 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
608fn 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 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 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 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 doctest.is_multiple_tests() {
702 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 return (Duration::default(), Err(TestFailure::CompileError));
709 }
710 if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto {
711 compiler.stderr(Stdio::null());
714 }
715 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 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 let status = child.wait().expect("Failed to wait");
746
747 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 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 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 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 return (instant.elapsed(), Err(TestFailure::CompileError));
792 }
793 if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto {
794 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 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 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 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
926fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
932 if path.components().count() == 1 {
933 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#[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 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 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 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 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)] impl 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;