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_attr_ir::{Attribute, AttributeKind};
22use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxHasher, FxIndexMap, FxIndexSet};
23use rustc_errors::emitter::HumanReadableErrorType;
24use rustc_errors::{ColorConfig, DiagCtxtHandle};
25use rustc_hir::CRATE_HIR_ID;
26use rustc_hir::def_id::LOCAL_CRATE;
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 prefix in &options.remap_path_prefix_strs {
107 content.push(format!("--remap-path-prefix={prefix}"));
108 }
109 if let Some(scope) = &options.remap_path_scope_str {
110 content.push(format!("--remap-path-scope={scope}"));
111 }
112
113 for lib_str in &options.lib_strs {
114 content.push(format!("-L{lib_str}"));
115 }
116 for extern_str in &options.extern_strs {
117 content.push(format!("--extern={extern_str}"));
118 }
119 content.push("-Ccodegen-units=1".to_string());
120 for codegen_options_str in &options.codegen_options_strs {
121 content.push(format!("-C{codegen_options_str}"));
122 }
123 for unstable_option_str in &options.unstable_opts_strs {
124 content.push(format!("-Z{unstable_option_str}"));
125 }
126
127 content.extend(options.doctest_build_args.clone());
128
129 let content = content.join("\n");
130
131 file.write_all(content.as_bytes())
132 .map_err(|error| format!("failed to write arguments to temporary file: {error:?}"))?;
133 Ok(())
134}
135
136fn get_doctest_dir(opts: &RustdocOptions) -> io::Result<TempDir> {
137 let mut builder = TempFileBuilder::new();
138 builder.prefix("rustdoctest");
139 if opts.codegen_options.save_temps {
140 builder.disable_cleanup(true);
141 }
142 builder.tempdir()
143}
144
145pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions) {
146 let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
147
148 let allowed_lints = vec![
150 invalid_codeblock_attributes_name.to_owned(),
151 lint::builtin::UNKNOWN_LINTS.name.to_owned(),
152 lint::builtin::RENAMED_AND_REMOVED_LINTS.name.to_owned(),
153 ];
154
155 let (lint_opts, lint_caps) = init_lints(allowed_lints, options.lint_opts.clone(), |lint| {
156 if lint.name == invalid_codeblock_attributes_name {
157 None
158 } else {
159 Some((lint.name_lower(), lint::Allow))
160 }
161 });
162
163 debug!(?lint_opts);
164
165 let crate_types =
166 if options.proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] };
167
168 let sessopts = config::Options {
169 sysroot: options.sysroot.clone(),
170 search_paths: options.libs.clone(),
171 crate_types,
172 lint_opts,
173 lint_cap: Some(options.lint_cap.unwrap_or(lint::Forbid)),
174 cg: options.codegen_options.clone(),
175 externs: options.externs.clone(),
176 unstable_features: options.unstable_features,
177 actually_rustdoc: true,
178 edition: options.edition,
179 target_triple: options.target.clone(),
180 crate_name: options.crate_name.clone(),
181 remap_path_prefix: options.remap_path_prefix.clone(),
182 remap_path_scope: options.remap_path_scope.clone(),
183 unstable_opts: options.unstable_opts.clone(),
184 error_format: options.error_format.clone(),
185 target_modifiers: options.target_modifiers.clone(),
186 describe_lints: options.describe_lints,
187 ..config::Options::default()
188 };
189
190 let mut cfgs = options.cfgs.clone();
191 cfgs.push("doc".to_owned());
192 cfgs.push("doctest".to_owned());
193 let config = interface::Config {
194 opts: sessopts,
195 crate_cfg: cfgs,
196 crate_check_cfg: options.check_cfgs.clone(),
197 input: input.clone(),
198 output_file: None,
199 output_dir: None,
200 file_loader: None,
201 lint_caps,
202 psess_created: None,
203 track_state: None,
204 register_lints: Some(Box::new(crate::lint::register_lints)),
205 override_queries: None,
206 extra_symbols: Vec::new(),
207 make_codegen_backend: None,
208 ice_file: None,
209 using_internal_features: &rustc_driver::USING_INTERNAL_FEATURES,
210 };
211
212 let externs = options.externs.clone();
213 let json_unused_externs = options.json_unused_externs;
214
215 let temp_dir = match get_doctest_dir(&options)
216 .map_err(|error| format!("failed to create temporary directory: {error:?}"))
217 {
218 Ok(temp_dir) => temp_dir,
219 Err(error) => return crate::wrap_return(dcx, Err(error)),
220 };
221 let args_path = temp_dir.path().join("rustdoc-cfgs");
222 crate::wrap_return(dcx, generate_args_file(&args_path, &options));
223
224 let extract_doctests = options.output_format == OutputFormat::Doctest;
225 let save_temps = options.codegen_options.save_temps;
226 let registered_lints = config.register_lints.is_some();
227 let result = interface::run_compiler(config, |compiler| {
228 let sess = &compiler.sess;
229
230 if sess.opts.describe_lints {
232 rustc_driver::describe_lints(sess, registered_lints);
233 return Ok(None);
234 }
235
236 let krate = rustc_interface::passes::parse(sess);
237
238 let (collector, _incr_comp_session) =
239 rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
240 let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
241 let opts = scrape_test_config(tcx, crate_name, args_path);
242
243 let hir_collector = HirCollector::new(
244 ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
245 tcx,
246 );
247 let tests = hir_collector.collect_crate();
248 if extract_doctests {
249 let mut collector = extracted::ExtractedDocTests::new();
250 tests.into_iter().for_each(|t| collector.add_test(t, &opts, &options));
251
252 let stdout = std::io::stdout();
253 let mut stdout = stdout.lock();
254 if let Err(error) = serde_json::ser::to_writer(&mut stdout, &collector) {
255 eprintln!();
256 Err(format!("Failed to generate JSON output for doctests: {error:?}"))
257 } else {
258 Ok(None)
259 }
260 } else {
261 let mut collector = CreateRunnableDocTests::new(options, opts);
262 tests
263 .into_iter()
264 .for_each(|t| collector.add_test(t, Some(compiler.sess.dcx())));
265
266 Ok(Some(collector))
267 }
268 });
269 compiler.sess.dcx().abort_if_errors();
270
271 collector
272 });
273
274 let CreateRunnableDocTests {
275 standalone_tests,
276 mergeable_tests,
277 rustdoc_options,
278 opts,
279 unused_extern_reports,
280 compiling_test_count,
281 ..
282 } = match result {
283 Ok(Some(collector)) => collector,
284 Ok(None) => return,
285 Err(error) => {
286 eprintln!("{error}");
287 if !save_temps {
290 let _ = std::fs::remove_dir_all(temp_dir.path());
291 }
292 std::process::exit(1);
293 }
294 };
295
296 run_tests(
297 dcx,
298 opts,
299 &rustdoc_options,
300 &unused_extern_reports,
301 standalone_tests,
302 mergeable_tests,
303 Some(temp_dir),
304 );
305
306 let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
307
308 if json_unused_externs.is_enabled() {
311 let unused_extern_reports: Vec<_> =
312 std::mem::take(&mut unused_extern_reports.lock().unwrap());
313 if unused_extern_reports.len() == compiling_test_count {
314 let extern_names =
315 externs.iter().map(|(name, _)| name).collect::<FxIndexSet<&String>>();
316 let mut unused_extern_names = unused_extern_reports
317 .iter()
318 .map(|uexts| uexts.unused_extern_names.iter().collect::<FxIndexSet<&String>>())
319 .fold(extern_names, |uextsa, uextsb| {
320 uextsa.intersection(&uextsb).copied().collect::<FxIndexSet<&String>>()
321 })
322 .iter()
323 .map(|v| (*v).clone())
324 .collect::<Vec<String>>();
325 unused_extern_names.sort();
326 let lint_level = unused_extern_reports
328 .iter()
329 .map(|uexts| uexts.lint_level.as_str())
330 .max_by_key(|v| match *v {
331 "warn" => 1,
332 "deny" => 2,
333 "forbid" => 3,
334 v => unreachable!("Invalid lint level '{v}'"),
338 })
339 .unwrap_or("warn")
340 .to_string();
341 let uext = UnusedExterns { lint_level, unused_extern_names };
342 let unused_extern_json = serde_json::to_string(&uext).unwrap();
343 eprintln!("{unused_extern_json}");
344 }
345 }
346}
347
348pub(crate) fn run_tests(
349 dcx: DiagCtxtHandle<'_>,
350 opts: GlobalTestOptions,
351 rustdoc_options: &Arc<RustdocOptions>,
352 unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
353 mut standalone_tests: Vec<test::TestDescAndFn>,
354 mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,
355 mut temp_dir: Option<TempDir>,
357) {
358 let mut test_args = Vec::with_capacity(rustdoc_options.test_args.len() + 1);
359 test_args.insert(0, "rustdoctest".to_string());
360 test_args.extend_from_slice(&rustdoc_options.test_args);
361 if rustdoc_options.no_capture {
362 test_args.push("--no-capture".to_string());
363 }
364
365 let mut nb_errors = 0;
366 let mut ran_edition_tests = 0;
367 let mut times = MergedDoctestTimes::new();
368 let target_str = rustdoc_options.target.to_string();
369
370 for (MergeableTestKey { edition, global_crate_attrs_hash }, mut doctests) in mergeable_tests {
371 if doctests.is_empty() {
372 continue;
373 }
374 doctests.sort_by(|(_, a), (_, b)| a.name.cmp(&b.name));
375
376 let mut tests_runner = runner::DocTestRunner::new();
377
378 let rustdoc_test_options = IndividualTestOptions::new(
379 rustdoc_options,
380 &Some(format!("merged_doctest_{edition}_{global_crate_attrs_hash}")),
381 PathBuf::from(format!("doctest_{edition}_{global_crate_attrs_hash}.rs")),
382 );
383
384 for (doctest, scraped_test) in &doctests {
385 tests_runner.add_test(doctest, scraped_test, &target_str);
386 }
387 let (duration, ret) = tests_runner.run_merged_tests(
388 rustdoc_test_options,
389 edition,
390 &opts,
391 &test_args,
392 rustdoc_options,
393 );
394 times.add_compilation_time(duration);
395 if let Ok(success) = ret {
396 ran_edition_tests += 1;
397 if !success {
398 nb_errors += 1;
399 }
400 continue;
401 }
402
403 if rustdoc_options.merge_doctests == MergeDoctests::Always {
404 let mut diag = dcx.struct_fatal("failed to merge doctests");
405 diag.note("requested explicitly on the command line with `--merge-doctests=yes`");
406 diag.emit();
407 }
408
409 debug!("Failed to compile compatible doctests for edition {} all at once", edition);
412 for (doctest, scraped_test) in doctests {
413 doctest.generate_unique_doctest(
414 &scraped_test.text,
415 scraped_test.langstr.test_harness,
416 &opts,
417 Some(&opts.crate_name),
418 );
419 standalone_tests.push(generate_test_desc_and_fn(
420 doctest,
421 scraped_test,
422 opts.clone(),
423 Arc::clone(rustdoc_options),
424 unused_extern_reports.clone(),
425 ));
426 }
427 }
428
429 if ran_edition_tests == 0 || !standalone_tests.is_empty() {
432 standalone_tests.sort_by(|a, b| a.desc.name.as_slice().cmp(b.desc.name.as_slice()));
433 let standalone_test_refs = &standalone_tests.iter().collect::<Vec<_>>();
435 let exit = test::test_main(&test_args, standalone_test_refs);
436 let times = times.times_in_secs();
437 std::mem::drop(standalone_tests);
439 std::mem::drop(temp_dir.take());
440 if let Some((total_time, compilation_time)) = times {
441 test::print_merged_doctests_times(&test_args, total_time, compilation_time);
442 }
443 if exit != std::process::ExitCode::SUCCESS {
445 exit.exit_process();
446 }
447 } else {
448 if let Some((total_time, compilation_time)) = times.times_in_secs() {
452 test::print_merged_doctests_times(&test_args, total_time, compilation_time);
453 }
454 }
455 std::mem::drop(temp_dir);
457 if nb_errors != 0 {
458 std::process::exit(test::ERROR_EXIT_CODE.into());
459 }
460}
461
462fn scrape_test_config(
464 tcx: TyCtxt<'_>,
465 crate_name: String,
466 args_file: PathBuf,
467) -> GlobalTestOptions {
468 let mut opts = GlobalTestOptions {
469 crate_name,
470 no_crate_inject: false,
471 insert_indent_space: false,
472 args_file,
473 };
474
475 let source_map = tcx.sess.source_map();
476 'main: for attr in tcx.hir_attrs(CRATE_HIR_ID) {
477 let Attribute::Parsed(AttributeKind::Doc(d)) = attr else { continue };
478 for attr_span in &d.test_attrs {
479 if let Ok(snippet) = source_map.span_to_snippet(*attr_span)
481 && let Ok(stream) = TokenStream::from_str(&snippet)
482 {
483 if stream.into_iter().any(|token| {
485 matches!(
486 token,
487 TokenTree::Ident(i) if i.to_string() == "no_crate_inject",
488 )
489 }) {
490 opts.no_crate_inject = true;
491 break 'main;
492 }
493 }
494 }
495 }
496
497 opts
498}
499
500enum TestFailure {
502 CompileError,
504 UnexpectedCompilePass,
506 MissingErrorCodes(Vec<String>),
509 ExecutionError(io::Error),
511 ExecutionFailure(process::Output),
515 UnexpectedRunPass,
517}
518
519enum DirState {
520 Temp(TempDir),
521 Perm(PathBuf),
522}
523
524impl DirState {
525 fn path(&self) -> &std::path::Path {
526 match self {
527 DirState::Temp(t) => t.path(),
528 DirState::Perm(p) => p.as_path(),
529 }
530 }
531}
532
533#[derive(serde::Serialize, serde::Deserialize)]
538pub(crate) struct UnusedExterns {
539 lint_level: String,
541 unused_extern_names: Vec<String>,
543}
544
545fn add_exe_suffix(input: String, target: &TargetTuple) -> String {
546 let exe_suffix = match target {
547 TargetTuple::TargetTuple(_) => Target::expect_builtin(target).options.exe_suffix,
548 TargetTuple::TargetJson { contents, .. } => {
549 Target::from_json(contents).unwrap().0.options.exe_suffix
550 }
551 };
552 input + &exe_suffix
553}
554
555fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Command {
556 let mut args = rustc_wrappers.iter().map(PathBuf::as_path).chain([rustc_binary]);
557
558 let exe = args.next().expect("unable to create rustc command");
559 let mut command = Command::new(exe);
560 for arg in args {
561 command.arg(arg);
562 }
563
564 command
565}
566
567pub(crate) struct RunnableDocTest<'a> {
574 full_test_code: String,
577 full_test_line_offset: usize,
578 test_opts: IndividualTestOptions,
579 global_opts: &'a GlobalTestOptions,
580 langstr: LangString,
581 line: usize,
582 edition: Edition,
583 no_run: bool,
584 merged_test_runner_code: Option<String>,
587}
588
589impl RunnableDocTest<'_> {
590 fn path_for_merged_doctest_bundle(&self) -> PathBuf {
591 self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition))
592 }
593 fn path_for_merged_doctest_runner(&self) -> PathBuf {
594 self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition))
595 }
596 fn is_multiple_tests(&self) -> bool {
597 self.merged_test_runner_code.is_some()
598 }
599}
600
601fn run_test(
608 doctest: RunnableDocTest<'_>,
609 rustdoc_options: &RustdocOptions,
610 supports_color: bool,
611 report_unused_externs: impl Fn(UnusedExterns),
612) -> (Duration, Result<(), TestFailure>) {
613 let langstr = &doctest.langstr;
614 let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
616 let output_file = doctest.test_opts.outdir.path().join(rust_out);
617 let instant = Instant::now();
618
619 let mut compiler_args = vec![];
623
624 compiler_args.push(format!("@{}", doctest.global_opts.args_file.display()));
625
626 let sysroot = &rustdoc_options.sysroot;
627 if let Some(explicit_sysroot) = &sysroot.explicit {
628 compiler_args.push(format!("--sysroot={}", explicit_sysroot.display()));
629 }
630
631 compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]);
632 if langstr.test_harness {
633 compiler_args.push("--test".to_owned());
634 }
635 if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail {
636 compiler_args.push("--error-format=json".to_owned());
637 compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]);
638 compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]);
639 compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]);
640 }
641
642 if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
643 compiler_args.push("--emit=metadata".to_owned());
646 }
647 compiler_args.extend_from_slice(&[
648 "--target".to_owned(),
649 match &rustdoc_options.target {
650 TargetTuple::TargetTuple(s) => s.clone(),
651 TargetTuple::TargetJson { path_for_rustdoc, .. } => {
652 path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned()
653 }
654 },
655 ]);
656 if let ErrorOutputType::HumanReadable { kind, color_config } = rustdoc_options.error_format {
657 let short = kind.short();
658 let unicode = kind == HumanReadableErrorType { unicode: true, short };
659
660 if short {
661 compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]);
662 }
663 if unicode {
664 compiler_args
665 .extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]);
666 }
667
668 match color_config {
669 ColorConfig::Never => {
670 compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]);
671 }
672 ColorConfig::Always => {
673 compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]);
674 }
675 ColorConfig::Auto => {
676 compiler_args.extend_from_slice(&[
677 "--color".to_owned(),
678 if supports_color { "always" } else { "never" }.to_owned(),
679 ]);
680 }
681 }
682 }
683
684 let rustc_binary = rustdoc_options
685 .test_builder
686 .as_deref()
687 .unwrap_or_else(|| rustc_interface::util::rustc_path(sysroot).expect("found rustc"));
688 let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
689
690 compiler.args(&compiler_args);
691
692 if doctest.is_multiple_tests() {
695 compiler.arg("--error-format=short");
697 let input_file = doctest.path_for_merged_doctest_bundle();
698 if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
699 return (Duration::default(), Err(TestFailure::CompileError));
702 }
703 if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto {
704 compiler.stderr(Stdio::null());
707 }
708 compiler
710 .arg("--crate-type=lib")
711 .arg("--out-dir")
712 .arg(doctest.test_opts.outdir.path())
713 .arg(input_file);
714 } else {
715 compiler.arg("--crate-type=bin").arg("-o").arg(&output_file);
716 compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
718 compiler.env(
719 "UNSTABLE_RUSTDOC_TEST_LINE",
720 format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
721 );
722 compiler.arg("-");
723 compiler.stdin(Stdio::piped());
724 compiler.stderr(Stdio::piped());
725 }
726
727 info!("compiler invocation for doctest: {compiler:?}");
728
729 let mut child = match compiler.spawn() {
730 Ok(child) => child,
731 Err(error) => {
732 eprintln!("Failed to spawn {:?}: {error:?}", compiler.get_program());
733 return (Duration::default(), Err(TestFailure::CompileError));
734 }
735 };
736 let output = if let Some(merged_test_runner_code) = &doctest.merged_test_runner_code {
737 let status = child.wait().expect("Failed to wait");
739
740 let runner_input_file = doctest.path_for_merged_doctest_runner();
743
744 let mut runner_compiler =
745 wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
746 runner_compiler.env("RUSTC_BOOTSTRAP", "1");
749 runner_compiler.args(compiler_args);
750 runner_compiler.args(["--crate-type=bin", "-o"]).arg(&output_file);
751 let mut extern_path = std::ffi::OsString::from(format!(
752 "--extern=doctest_bundle_{edition}=",
753 edition = doctest.edition
754 ));
755
756 let mut seen_search_dirs = FxHashSet::default();
759 for extern_str in &rustdoc_options.extern_strs {
760 if let Some((_cratename, path)) = extern_str.split_once('=') {
761 let dir = Path::new(path)
765 .parent()
766 .filter(|x| x.components().count() > 0)
767 .unwrap_or(Path::new("."));
768 if seen_search_dirs.insert(dir) {
769 runner_compiler.arg("-L").arg(dir);
770 }
771 }
772 }
773 let output_bundle_file = doctest
774 .test_opts
775 .outdir
776 .path()
777 .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition));
778 extern_path.push(&output_bundle_file);
779 runner_compiler.arg(extern_path);
780 runner_compiler.arg(&runner_input_file);
781 if std::fs::write(&runner_input_file, merged_test_runner_code).is_err() {
782 return (instant.elapsed(), Err(TestFailure::CompileError));
785 }
786 if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto {
787 runner_compiler.stderr(Stdio::null());
790 } else {
791 runner_compiler.stderr(Stdio::inherit());
792 }
793 runner_compiler.arg("--error-format=short");
794 info!("compiler invocation for doctest runner: {runner_compiler:?}");
795
796 let status = if !status.success() {
797 status
798 } else {
799 let mut child_runner = match runner_compiler.spawn() {
800 Ok(child) => child,
801 Err(error) => {
802 eprintln!("Failed to spawn {:?}: {error:?}", runner_compiler.get_program());
803 return (Duration::default(), Err(TestFailure::CompileError));
804 }
805 };
806 child_runner.wait().expect("Failed to wait")
807 };
808
809 process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
810 } else {
811 let stdin = child.stdin.as_mut().expect("Failed to open stdin");
812 stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources");
813 child.wait_with_output().expect("Failed to read stdout")
814 };
815
816 struct Bomb<'a>(&'a str);
817 impl Drop for Bomb<'_> {
818 fn drop(&mut self) {
819 eprint!("{}", self.0);
820 }
821 }
822 let mut out = str::from_utf8(&output.stderr)
823 .unwrap()
824 .lines()
825 .filter(|l| {
826 if let Ok(uext) = serde_json::from_str::<UnusedExterns>(l) {
827 report_unused_externs(uext);
828 false
829 } else {
830 true
831 }
832 })
833 .intersperse_with(|| "\n")
834 .collect::<String>();
835
836 if !out.is_empty() {
839 out.push('\n');
840 }
841
842 let _bomb = Bomb(&out);
843 match (output.status.success(), langstr.compile_fail) {
844 (true, true) => {
845 return (instant.elapsed(), Err(TestFailure::UnexpectedCompilePass));
846 }
847 (true, false) => {}
848 (false, true) => {
849 if !langstr.error_codes.is_empty() {
850 let missing_codes: Vec<String> = langstr
854 .error_codes
855 .iter()
856 .filter(|err| !out.contains(&format!("error[{err}]")))
857 .cloned()
858 .collect();
859
860 if !missing_codes.is_empty() {
861 return (instant.elapsed(), Err(TestFailure::MissingErrorCodes(missing_codes)));
862 }
863 }
864 }
865 (false, false) => {
866 return (instant.elapsed(), Err(TestFailure::CompileError));
867 }
868 }
869
870 let duration = instant.elapsed();
871 if doctest.no_run {
872 return (duration, Ok(()));
873 }
874
875 let mut cmd;
877
878 let output_file = make_maybe_absolute_path(output_file);
879 if let Some(tool) = &rustdoc_options.test_runtool {
880 let tool = make_maybe_absolute_path(tool.into());
881 cmd = Command::new(tool);
882 cmd.args(&rustdoc_options.test_runtool_args);
883 cmd.arg(&output_file);
884 } else {
885 cmd = Command::new(&output_file);
886 if doctest.is_multiple_tests() {
887 cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file);
888 }
889 }
890 if let Some(run_directory) = &rustdoc_options.test_run_directory {
891 cmd.current_dir(run_directory);
892 }
893
894 info!("running doctest executable: {cmd:?}");
895
896 let result = if doctest.is_multiple_tests() || rustdoc_options.no_capture {
897 cmd.status().map(|status| process::Output {
898 status,
899 stdout: Vec::new(),
900 stderr: Vec::new(),
901 })
902 } else {
903 cmd.output()
904 };
905 match result {
906 Err(e) => return (duration, Err(TestFailure::ExecutionError(e))),
907 Ok(out) => {
908 if langstr.should_panic && out.status.success() {
909 return (duration, Err(TestFailure::UnexpectedRunPass));
910 } else if !langstr.should_panic && !out.status.success() {
911 return (duration, Err(TestFailure::ExecutionFailure(out)));
912 }
913 }
914 }
915
916 (duration, Ok(()))
917}
918
919fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
925 if path.components().count() == 1 {
926 path
928 } else {
929 std::env::current_dir().map(|c| c.join(&path)).unwrap_or_else(|_| path)
930 }
931}
932struct IndividualTestOptions {
933 outdir: DirState,
934 path: PathBuf,
935}
936
937impl IndividualTestOptions {
938 fn new(options: &RustdocOptions, test_id: &Option<String>, test_path: PathBuf) -> Self {
939 let outdir = if let Some(ref path) = options.persist_doctests {
940 let mut path = path.clone();
941 path.push(test_id.as_deref().unwrap_or("<doctest>"));
942
943 if let Err(err) = std::fs::create_dir_all(&path) {
944 eprintln!("Couldn't create directory for doctest executables: {err}");
945 panic::resume_unwind(Box::new(()));
946 }
947
948 DirState::Perm(path)
949 } else {
950 DirState::Temp(get_doctest_dir(options).expect("rustdoc needs a tempdir"))
951 };
952
953 Self { outdir, path: test_path }
954 }
955}
956
957#[derive(Debug)]
967pub(crate) struct ScrapedDocTest {
968 filename: FileName,
969 line: usize,
970 langstr: LangString,
971 text: String,
972 name: String,
973 span: Span,
974 code_mappings: Vec<CodeLineMapping>,
975 global_crate_attrs: Vec<String>,
976}
977
978impl ScrapedDocTest {
979 fn new(
980 filename: FileName,
981 line: usize,
982 logical_path: Vec<String>,
983 langstr: LangString,
984 text: String,
985 span: Span,
986 code_mappings: Vec<CodeLineMapping>,
987 global_crate_attrs: Vec<String>,
988 ) -> Self {
989 let mut item_path = logical_path.join("::");
990 item_path.retain(|c| c != ' ');
991 if !item_path.is_empty() {
992 item_path.push(' ');
993 }
994 let name = format!(
995 "{} - {item_path}(line {line})",
996 filename.display(RemapPathScopeComponents::DIAGNOSTICS)
997 );
998
999 Self { filename, line, langstr, text, name, span, code_mappings, global_crate_attrs }
1000 }
1001 fn edition(&self, opts: &RustdocOptions) -> Edition {
1002 self.langstr.edition.unwrap_or(opts.edition)
1003 }
1004
1005 fn no_run(&self, opts: &RustdocOptions) -> bool {
1006 self.langstr.no_run || opts.no_run
1007 }
1008
1009 fn path(&self, scope: RemapPathScopeComponents) -> String {
1010 self.filename.display(scope).to_string()
1011 }
1012
1013 fn local_path(&self) -> PathBuf {
1014 self.filename.clone().into_local_path().unwrap_or_else(|| PathBuf::from(r"doctest.rs"))
1015 }
1016}
1017
1018pub(crate) trait DocTestVisitor {
1019 fn visit_test(
1020 &mut self,
1021 test: String,
1022 config: LangString,
1023 rel_line: MdRelLine,
1024 code_mappings: Vec<CodeLineMapping>,
1025 );
1026 fn visit_header(&mut self, _name: &str, _level: u32) {}
1027}
1028
1029#[derive(Clone, Debug, Hash, Eq, PartialEq)]
1030pub(crate) struct MergeableTestKey {
1031 edition: Edition,
1032 global_crate_attrs_hash: u64,
1033}
1034
1035struct CreateRunnableDocTests {
1036 standalone_tests: Vec<test::TestDescAndFn>,
1037 mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,
1038
1039 rustdoc_options: Arc<RustdocOptions>,
1040 opts: GlobalTestOptions,
1041 visited_tests: FxHashMap<(String, usize), usize>,
1042 unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
1043 compiling_test_count: AtomicUsize,
1044 can_merge_doctests: MergeDoctests,
1045}
1046
1047impl CreateRunnableDocTests {
1048 fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests {
1049 CreateRunnableDocTests {
1050 standalone_tests: Vec::new(),
1051 mergeable_tests: FxIndexMap::default(),
1052 opts,
1053 visited_tests: FxHashMap::default(),
1054 unused_extern_reports: Default::default(),
1055 compiling_test_count: AtomicUsize::new(0),
1056 can_merge_doctests: rustdoc_options.merge_doctests,
1057 rustdoc_options: Arc::new(rustdoc_options),
1058 }
1059 }
1060
1061 fn add_test(&mut self, scraped_test: ScrapedDocTest, dcx: Option<DiagCtxtHandle<'_>>) {
1062 let file = scraped_test
1067 .filename
1068 .display(RemapPathScopeComponents::MACRO)
1069 .to_string_lossy()
1070 .chars()
1071 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1072 .collect::<String>();
1073 let test_id = format!(
1074 "{file}_{line}_{number}",
1075 file = file,
1076 line = scraped_test.line,
1077 number = {
1078 self.visited_tests
1081 .entry((file.clone(), scraped_test.line))
1082 .and_modify(|v| *v += 1)
1083 .or_insert(0)
1084 },
1085 );
1086
1087 let edition = scraped_test.edition(&self.rustdoc_options);
1088 let doctest = BuildDocTestBuilder::new(&scraped_test.text)
1089 .crate_name(&self.opts.crate_name)
1090 .global_crate_attrs(scraped_test.global_crate_attrs.clone())
1091 .edition(edition)
1092 .can_merge_doctests(self.can_merge_doctests)
1093 .test_id(test_id)
1094 .lang_str(&scraped_test.langstr)
1095 .span(scraped_test.span)
1096 .code_mappings(&scraped_test.code_mappings)
1097 .build(dcx);
1098 let is_standalone = !doctest.can_be_merged
1099 || self.rustdoc_options.no_capture
1100 || self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output");
1101 if is_standalone {
1102 let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
1103 self.standalone_tests.push(test_desc);
1104 } else {
1105 self.mergeable_tests
1106 .entry(MergeableTestKey {
1107 edition,
1108 global_crate_attrs_hash: {
1109 let mut hasher = FxHasher::default();
1110 scraped_test.global_crate_attrs.hash(&mut hasher);
1111 hasher.finish()
1112 },
1113 })
1114 .or_default()
1115 .push((doctest, scraped_test));
1116 }
1117 }
1118
1119 fn generate_test_desc_and_fn(
1120 &mut self,
1121 test: DocTestBuilder,
1122 scraped_test: ScrapedDocTest,
1123 ) -> test::TestDescAndFn {
1124 if !scraped_test.langstr.compile_fail {
1125 self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
1126 }
1127
1128 generate_test_desc_and_fn(
1129 test,
1130 scraped_test,
1131 self.opts.clone(),
1132 Arc::clone(&self.rustdoc_options),
1133 self.unused_extern_reports.clone(),
1134 )
1135 }
1136}
1137
1138fn generate_test_desc_and_fn(
1139 test: DocTestBuilder,
1140 scraped_test: ScrapedDocTest,
1141 opts: GlobalTestOptions,
1142 rustdoc_options: Arc<RustdocOptions>,
1143 unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
1144) -> test::TestDescAndFn {
1145 let target_str = rustdoc_options.target.to_string();
1146
1147 debug!("creating test {}: {}", scraped_test.name, scraped_test.text);
1148 test::TestDescAndFn {
1149 desc: test::TestDesc {
1150 name: test::DynTestName(scraped_test.name.clone()),
1151 ignore: match scraped_test.langstr.ignore {
1152 Ignore::All => true,
1153 Ignore::None => false,
1154 Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
1155 },
1156 ignore_message: None,
1157 source_file: "",
1158 start_line: 0,
1159 start_col: 0,
1160 end_line: 0,
1161 end_col: 0,
1162 should_panic: test::ShouldPanic::No,
1164 compile_fail: scraped_test.langstr.compile_fail,
1165 no_run: scraped_test.no_run(&rustdoc_options),
1166 test_type: test::TestType::DocTest,
1167 },
1168 testfn: test::DynTestFn(Arc::new(move || {
1169 let rustdoc_test_options = IndividualTestOptions::new(
1170 &rustdoc_options,
1171 &test.test_id,
1172 scraped_test.local_path(),
1173 );
1174 doctest_run_fn(
1175 rustdoc_test_options,
1176 &opts,
1177 &test,
1178 &scraped_test,
1179 &rustdoc_options,
1180 &unused_externs,
1181 )
1182 })),
1183 }
1184}
1185
1186fn doctest_run_fn(
1187 test_opts: IndividualTestOptions,
1188 global_opts: &GlobalTestOptions,
1189 doctest: &DocTestBuilder,
1190 scraped_test: &ScrapedDocTest,
1191 rustdoc_options: &RustdocOptions,
1192 unused_externs: &Mutex<Vec<UnusedExterns>>,
1193) -> Result<(), String> {
1194 let report_unused_externs = |uext| {
1195 unused_externs.lock().unwrap().push(uext);
1196 };
1197 let (wrapped, full_test_line_offset) = doctest.generate_unique_doctest(
1198 &scraped_test.text,
1199 scraped_test.langstr.test_harness,
1200 &global_opts,
1201 Some(&global_opts.crate_name),
1202 );
1203 let runnable_test = RunnableDocTest {
1204 full_test_code: wrapped.to_string(),
1205 full_test_line_offset,
1206 test_opts,
1207 global_opts,
1208 langstr: scraped_test.langstr.clone(),
1209 line: scraped_test.line,
1210 edition: scraped_test.edition(&rustdoc_options),
1211 no_run: scraped_test.no_run(&rustdoc_options),
1212 merged_test_runner_code: None,
1213 };
1214 let (_, res) =
1215 run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
1216
1217 if let Err(err) = res {
1218 match err {
1219 TestFailure::CompileError => {
1220 eprint!("Couldn't compile the test.");
1221 }
1222 TestFailure::UnexpectedCompilePass => {
1223 eprint!("Test compiled successfully, but it's marked `compile_fail`.");
1224 }
1225 TestFailure::UnexpectedRunPass => {
1226 eprint!("Test executable succeeded, but it's marked `should_panic`.");
1227 }
1228 TestFailure::MissingErrorCodes(codes) => {
1229 eprint!("Some expected error codes were not found: {codes:?}");
1230 }
1231 TestFailure::ExecutionError(err) => {
1232 eprint!("Couldn't run the test: {err}");
1233 if err.kind() == io::ErrorKind::PermissionDenied {
1234 eprint!(" - maybe your tempdir is mounted with noexec?");
1235 }
1236 }
1237 TestFailure::ExecutionFailure(out) => {
1238 eprintln!("Test executable failed ({reason}).", reason = out.status);
1239
1240 let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
1250 let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
1251
1252 if !stdout.is_empty() || !stderr.is_empty() {
1253 eprintln!();
1254
1255 if !stdout.is_empty() {
1256 eprintln!("stdout:\n{stdout}");
1257 }
1258
1259 if !stderr.is_empty() {
1260 eprintln!("stderr:\n{stderr}");
1261 }
1262 }
1263 }
1264 }
1265
1266 panic::resume_unwind(Box::new(()));
1267 }
1268 Ok(())
1269}
1270
1271#[cfg(test)] impl DocTestVisitor for Vec<usize> {
1273 fn visit_test(
1274 &mut self,
1275 _test: String,
1276 _config: LangString,
1277 rel_line: MdRelLine,
1278 _code_mappings: Vec<CodeLineMapping>,
1279 ) {
1280 self.push(1 + rel_line.offset());
1281 }
1282}
1283
1284#[cfg(test)]
1285mod tests;