1mod extracted;
2mod make;
3mod markdown;
4mod runner;
5mod rust;
6
7use std::fs::File;
8use std::io::{self, Write};
9use std::path::{Path, PathBuf};
10use std::process::{self, Command, Stdio};
11use std::sync::atomic::{AtomicUsize, Ordering};
12use std::sync::{Arc, Mutex};
13use std::{panic, str};
14
15pub(crate) use make::{BuildDocTestBuilder, DocTestBuilder};
16pub(crate) use markdown::test as test_markdown;
17use rustc_data_structures::fx::{FxHashMap, FxIndexMap, FxIndexSet};
18use rustc_errors::emitter::HumanReadableErrorType;
19use rustc_errors::{ColorConfig, DiagCtxtHandle};
20use rustc_hir as hir;
21use rustc_hir::CRATE_HIR_ID;
22use rustc_hir::def_id::LOCAL_CRATE;
23use rustc_interface::interface;
24use rustc_session::config::{self, CrateType, ErrorOutputType, Input};
25use rustc_session::lint;
26use rustc_span::edition::Edition;
27use rustc_span::symbol::sym;
28use rustc_span::{FileName, Span};
29use rustc_target::spec::{Target, TargetTuple};
30use tempfile::{Builder as TempFileBuilder, TempDir};
31use tracing::debug;
32
33use self::rust::HirCollector;
34use crate::config::{Options as RustdocOptions, OutputFormat};
35use crate::html::markdown::{ErrorCodes, Ignore, LangString, MdRelLine};
36use crate::lint::init_lints;
37
38#[derive(Clone)]
40pub(crate) struct GlobalTestOptions {
41 pub(crate) crate_name: String,
43 pub(crate) no_crate_inject: bool,
45 pub(crate) insert_indent_space: bool,
48 pub(crate) attrs: Vec<String>,
50 pub(crate) args_file: PathBuf,
52}
53
54pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) -> Result<(), String> {
55 let mut file = File::create(file_path)
56 .map_err(|error| format!("failed to create args file: {error:?}"))?;
57
58 let mut content = vec![];
60
61 for cfg in &options.cfgs {
62 content.push(format!("--cfg={cfg}"));
63 }
64 for check_cfg in &options.check_cfgs {
65 content.push(format!("--check-cfg={check_cfg}"));
66 }
67
68 for lib_str in &options.lib_strs {
69 content.push(format!("-L{lib_str}"));
70 }
71 for extern_str in &options.extern_strs {
72 content.push(format!("--extern={extern_str}"));
73 }
74 content.push("-Ccodegen-units=1".to_string());
75 for codegen_options_str in &options.codegen_options_strs {
76 content.push(format!("-C{codegen_options_str}"));
77 }
78 for unstable_option_str in &options.unstable_opts_strs {
79 content.push(format!("-Z{unstable_option_str}"));
80 }
81
82 content.extend(options.doctest_build_args.clone());
83
84 let content = content.join("\n");
85
86 file.write_all(content.as_bytes())
87 .map_err(|error| format!("failed to write arguments to temporary file: {error:?}"))?;
88 Ok(())
89}
90
91fn get_doctest_dir() -> io::Result<TempDir> {
92 TempFileBuilder::new().prefix("rustdoctest").tempdir()
93}
94
95pub(crate) fn run(dcx: DiagCtxtHandle<'_>, input: Input, options: RustdocOptions) {
96 let invalid_codeblock_attributes_name = crate::lint::INVALID_CODEBLOCK_ATTRIBUTES.name;
97
98 let allowed_lints = vec![
100 invalid_codeblock_attributes_name.to_owned(),
101 lint::builtin::UNKNOWN_LINTS.name.to_owned(),
102 lint::builtin::RENAMED_AND_REMOVED_LINTS.name.to_owned(),
103 ];
104
105 let (lint_opts, lint_caps) = init_lints(allowed_lints, options.lint_opts.clone(), |lint| {
106 if lint.name == invalid_codeblock_attributes_name {
107 None
108 } else {
109 Some((lint.name_lower(), lint::Allow))
110 }
111 });
112
113 debug!(?lint_opts);
114
115 let crate_types =
116 if options.proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] };
117
118 let sessopts = config::Options {
119 sysroot: options.sysroot.clone(),
120 search_paths: options.libs.clone(),
121 crate_types,
122 lint_opts,
123 lint_cap: Some(options.lint_cap.unwrap_or(lint::Forbid)),
124 cg: options.codegen_options.clone(),
125 externs: options.externs.clone(),
126 unstable_features: options.unstable_features,
127 actually_rustdoc: true,
128 edition: options.edition,
129 target_triple: options.target.clone(),
130 crate_name: options.crate_name.clone(),
131 remap_path_prefix: options.remap_path_prefix.clone(),
132 ..config::Options::default()
133 };
134
135 let mut cfgs = options.cfgs.clone();
136 cfgs.push("doc".to_owned());
137 cfgs.push("doctest".to_owned());
138 let config = interface::Config {
139 opts: sessopts,
140 crate_cfg: cfgs,
141 crate_check_cfg: options.check_cfgs.clone(),
142 input: input.clone(),
143 output_file: None,
144 output_dir: None,
145 file_loader: None,
146 locale_resources: rustc_driver::DEFAULT_LOCALE_RESOURCES.to_vec(),
147 lint_caps,
148 psess_created: None,
149 hash_untracked_state: None,
150 register_lints: Some(Box::new(crate::lint::register_lints)),
151 override_queries: None,
152 extra_symbols: Vec::new(),
153 make_codegen_backend: None,
154 registry: rustc_driver::diagnostics_registry(),
155 ice_file: None,
156 using_internal_features: &rustc_driver::USING_INTERNAL_FEATURES,
157 expanded_args: options.expanded_args.clone(),
158 };
159
160 let externs = options.externs.clone();
161 let json_unused_externs = options.json_unused_externs;
162
163 let temp_dir = match get_doctest_dir()
164 .map_err(|error| format!("failed to create temporary directory: {error:?}"))
165 {
166 Ok(temp_dir) => temp_dir,
167 Err(error) => return crate::wrap_return(dcx, Err(error)),
168 };
169 let args_path = temp_dir.path().join("rustdoc-cfgs");
170 crate::wrap_return(dcx, generate_args_file(&args_path, &options));
171
172 let extract_doctests = options.output_format == OutputFormat::Doctest;
173 let result = interface::run_compiler(config, |compiler| {
174 let krate = rustc_interface::passes::parse(&compiler.sess);
175
176 let collector = rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
177 let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
178 let crate_attrs = tcx.hir_attrs(CRATE_HIR_ID);
179 let opts = scrape_test_config(crate_name, crate_attrs, args_path);
180
181 let hir_collector = HirCollector::new(
182 ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
183 tcx,
184 );
185 let tests = hir_collector.collect_crate();
186 if extract_doctests {
187 let mut collector = extracted::ExtractedDocTests::new();
188 tests.into_iter().for_each(|t| collector.add_test(t, &opts, &options));
189
190 let stdout = std::io::stdout();
191 let mut stdout = stdout.lock();
192 if let Err(error) = serde_json::ser::to_writer(&mut stdout, &collector) {
193 eprintln!();
194 Err(format!("Failed to generate JSON output for doctests: {error:?}"))
195 } else {
196 Ok(None)
197 }
198 } else {
199 let mut collector = CreateRunnableDocTests::new(options, opts);
200 tests.into_iter().for_each(|t| collector.add_test(t, Some(compiler.sess.dcx())));
201
202 Ok(Some(collector))
203 }
204 });
205 compiler.sess.dcx().abort_if_errors();
206
207 collector
208 });
209
210 let CreateRunnableDocTests {
211 standalone_tests,
212 mergeable_tests,
213 rustdoc_options,
214 opts,
215 unused_extern_reports,
216 compiling_test_count,
217 ..
218 } = match result {
219 Ok(Some(collector)) => collector,
220 Ok(None) => return,
221 Err(error) => {
222 eprintln!("{error}");
223 let _ = std::fs::remove_dir_all(temp_dir.path());
226 std::process::exit(1);
227 }
228 };
229
230 run_tests(
231 opts,
232 &rustdoc_options,
233 &unused_extern_reports,
234 standalone_tests,
235 mergeable_tests,
236 Some(temp_dir),
237 );
238
239 let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
240
241 if json_unused_externs.is_enabled() {
244 let unused_extern_reports: Vec<_> =
245 std::mem::take(&mut unused_extern_reports.lock().unwrap());
246 if unused_extern_reports.len() == compiling_test_count {
247 let extern_names =
248 externs.iter().map(|(name, _)| name).collect::<FxIndexSet<&String>>();
249 let mut unused_extern_names = unused_extern_reports
250 .iter()
251 .map(|uexts| uexts.unused_extern_names.iter().collect::<FxIndexSet<&String>>())
252 .fold(extern_names, |uextsa, uextsb| {
253 uextsa.intersection(&uextsb).copied().collect::<FxIndexSet<&String>>()
254 })
255 .iter()
256 .map(|v| (*v).clone())
257 .collect::<Vec<String>>();
258 unused_extern_names.sort();
259 let lint_level = unused_extern_reports
261 .iter()
262 .map(|uexts| uexts.lint_level.as_str())
263 .max_by_key(|v| match *v {
264 "warn" => 1,
265 "deny" => 2,
266 "forbid" => 3,
267 v => unreachable!("Invalid lint level '{v}'"),
271 })
272 .unwrap_or("warn")
273 .to_string();
274 let uext = UnusedExterns { lint_level, unused_extern_names };
275 let unused_extern_json = serde_json::to_string(&uext).unwrap();
276 eprintln!("{unused_extern_json}");
277 }
278 }
279}
280
281pub(crate) fn run_tests(
282 opts: GlobalTestOptions,
283 rustdoc_options: &Arc<RustdocOptions>,
284 unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
285 mut standalone_tests: Vec<test::TestDescAndFn>,
286 mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
287 mut temp_dir: Option<TempDir>,
289) {
290 let mut test_args = Vec::with_capacity(rustdoc_options.test_args.len() + 1);
291 test_args.insert(0, "rustdoctest".to_string());
292 test_args.extend_from_slice(&rustdoc_options.test_args);
293 if rustdoc_options.nocapture {
294 test_args.push("--nocapture".to_string());
295 }
296
297 let mut nb_errors = 0;
298 let mut ran_edition_tests = 0;
299 let target_str = rustdoc_options.target.to_string();
300
301 for (edition, mut doctests) in mergeable_tests {
302 if doctests.is_empty() {
303 continue;
304 }
305 doctests.sort_by(|(_, a), (_, b)| a.name.cmp(&b.name));
306
307 let mut tests_runner = runner::DocTestRunner::new();
308
309 let rustdoc_test_options = IndividualTestOptions::new(
310 rustdoc_options,
311 &Some(format!("merged_doctest_{edition}")),
312 PathBuf::from(format!("doctest_{edition}.rs")),
313 );
314
315 for (doctest, scraped_test) in &doctests {
316 tests_runner.add_test(doctest, scraped_test, &target_str);
317 }
318 if let Ok(success) = tests_runner.run_merged_tests(
319 rustdoc_test_options,
320 edition,
321 &opts,
322 &test_args,
323 rustdoc_options,
324 ) {
325 ran_edition_tests += 1;
326 if !success {
327 nb_errors += 1;
328 }
329 continue;
330 }
331 debug!("Failed to compile compatible doctests for edition {} all at once", edition);
334 for (doctest, scraped_test) in doctests {
335 doctest.generate_unique_doctest(
336 &scraped_test.text,
337 scraped_test.langstr.test_harness,
338 &opts,
339 Some(&opts.crate_name),
340 );
341 standalone_tests.push(generate_test_desc_and_fn(
342 doctest,
343 scraped_test,
344 opts.clone(),
345 Arc::clone(rustdoc_options),
346 unused_extern_reports.clone(),
347 ));
348 }
349 }
350
351 if ran_edition_tests == 0 || !standalone_tests.is_empty() {
354 standalone_tests.sort_by(|a, b| a.desc.name.as_slice().cmp(b.desc.name.as_slice()));
355 test::test_main_with_exit_callback(&test_args, standalone_tests, None, || {
356 std::mem::drop(temp_dir.take());
358 });
359 }
360 if nb_errors != 0 {
361 std::mem::drop(temp_dir);
363 std::process::exit(101);
365 }
366}
367
368fn scrape_test_config(
370 crate_name: String,
371 attrs: &[hir::Attribute],
372 args_file: PathBuf,
373) -> GlobalTestOptions {
374 use rustc_ast_pretty::pprust;
375
376 let mut opts = GlobalTestOptions {
377 crate_name,
378 no_crate_inject: false,
379 attrs: Vec::new(),
380 insert_indent_space: false,
381 args_file,
382 };
383
384 let test_attrs: Vec<_> = attrs
385 .iter()
386 .filter(|a| a.has_name(sym::doc))
387 .flat_map(|a| a.meta_item_list().unwrap_or_default())
388 .filter(|a| a.has_name(sym::test))
389 .collect();
390 let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[]));
391
392 for attr in attrs {
393 if attr.has_name(sym::no_crate_inject) {
394 opts.no_crate_inject = true;
395 }
396 if attr.has_name(sym::attr)
397 && let Some(l) = attr.meta_item_list()
398 {
399 for item in l {
400 opts.attrs.push(pprust::meta_list_item_to_string(item));
401 }
402 }
403 }
404
405 opts
406}
407
408enum TestFailure {
410 CompileError,
412 UnexpectedCompilePass,
414 MissingErrorCodes(Vec<String>),
417 ExecutionError(io::Error),
419 ExecutionFailure(process::Output),
423 UnexpectedRunPass,
425}
426
427enum DirState {
428 Temp(TempDir),
429 Perm(PathBuf),
430}
431
432impl DirState {
433 fn path(&self) -> &std::path::Path {
434 match self {
435 DirState::Temp(t) => t.path(),
436 DirState::Perm(p) => p.as_path(),
437 }
438 }
439}
440
441#[derive(serde::Serialize, serde::Deserialize)]
446pub(crate) struct UnusedExterns {
447 lint_level: String,
449 unused_extern_names: Vec<String>,
451}
452
453fn add_exe_suffix(input: String, target: &TargetTuple) -> String {
454 let exe_suffix = match target {
455 TargetTuple::TargetTuple(_) => Target::expect_builtin(target).options.exe_suffix,
456 TargetTuple::TargetJson { contents, .. } => {
457 Target::from_json(contents.parse().unwrap()).unwrap().0.options.exe_suffix
458 }
459 };
460 input + &exe_suffix
461}
462
463fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Command {
464 let mut args = rustc_wrappers.iter().map(PathBuf::as_path).chain([rustc_binary]);
465
466 let exe = args.next().expect("unable to create rustc command");
467 let mut command = Command::new(exe);
468 for arg in args {
469 command.arg(arg);
470 }
471
472 command
473}
474
475pub(crate) struct RunnableDocTest {
482 full_test_code: String,
483 full_test_line_offset: usize,
484 test_opts: IndividualTestOptions,
485 global_opts: GlobalTestOptions,
486 langstr: LangString,
487 line: usize,
488 edition: Edition,
489 no_run: bool,
490 merged_test_code: Option<String>,
491}
492
493impl RunnableDocTest {
494 fn path_for_merged_doctest_bundle(&self) -> PathBuf {
495 self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition))
496 }
497 fn path_for_merged_doctest_runner(&self) -> PathBuf {
498 self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition))
499 }
500 fn is_multiple_tests(&self) -> bool {
501 self.merged_test_code.is_some()
502 }
503}
504
505fn run_test(
510 doctest: RunnableDocTest,
511 rustdoc_options: &RustdocOptions,
512 supports_color: bool,
513 report_unused_externs: impl Fn(UnusedExterns),
514) -> Result<(), TestFailure> {
515 let langstr = &doctest.langstr;
516 let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
518 let output_file = doctest.test_opts.outdir.path().join(rust_out);
519
520 let mut compiler_args = vec![];
524
525 compiler_args.push(format!("@{}", doctest.global_opts.args_file.display()));
526
527 if let Some(sysroot) = &rustdoc_options.maybe_sysroot {
528 compiler_args.push(format!("--sysroot={}", sysroot.display()));
529 }
530
531 compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]);
532 if langstr.test_harness {
533 compiler_args.push("--test".to_owned());
534 }
535 if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail {
536 compiler_args.push("--error-format=json".to_owned());
537 compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]);
538 compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]);
539 compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]);
540 }
541
542 if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
543 compiler_args.push("--emit=metadata".to_owned());
546 }
547 compiler_args.extend_from_slice(&[
548 "--target".to_owned(),
549 match &rustdoc_options.target {
550 TargetTuple::TargetTuple(s) => s.clone(),
551 TargetTuple::TargetJson { path_for_rustdoc, .. } => {
552 path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned()
553 }
554 },
555 ]);
556 if let ErrorOutputType::HumanReadable { kind, color_config } = rustdoc_options.error_format {
557 let short = kind.short();
558 let unicode = kind == HumanReadableErrorType::Unicode;
559
560 if short {
561 compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]);
562 }
563 if unicode {
564 compiler_args
565 .extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]);
566 }
567
568 match color_config {
569 ColorConfig::Never => {
570 compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]);
571 }
572 ColorConfig::Always => {
573 compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]);
574 }
575 ColorConfig::Auto => {
576 compiler_args.extend_from_slice(&[
577 "--color".to_owned(),
578 if supports_color { "always" } else { "never" }.to_owned(),
579 ]);
580 }
581 }
582 }
583
584 let rustc_binary = rustdoc_options
585 .test_builder
586 .as_deref()
587 .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
588 let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
589
590 compiler.args(&compiler_args);
591
592 if doctest.is_multiple_tests() {
595 compiler.arg("--error-format=short");
597 let input_file = doctest.path_for_merged_doctest_bundle();
598 if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
599 return Err(TestFailure::CompileError);
602 }
603 if !rustdoc_options.nocapture {
604 compiler.stderr(Stdio::null());
607 }
608 compiler
610 .arg("--crate-type=lib")
611 .arg("--out-dir")
612 .arg(doctest.test_opts.outdir.path())
613 .arg(input_file);
614 } else {
615 compiler.arg("--crate-type=bin").arg("-o").arg(&output_file);
616 compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
618 compiler.env(
619 "UNSTABLE_RUSTDOC_TEST_LINE",
620 format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
621 );
622 compiler.arg("-");
623 compiler.stdin(Stdio::piped());
624 compiler.stderr(Stdio::piped());
625 }
626
627 debug!("compiler invocation for doctest: {compiler:?}");
628
629 let mut child = compiler.spawn().expect("Failed to spawn rustc process");
630 let output = if let Some(merged_test_code) = &doctest.merged_test_code {
631 let status = child.wait().expect("Failed to wait");
633
634 let runner_input_file = doctest.path_for_merged_doctest_runner();
637
638 let mut runner_compiler =
639 wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
640 runner_compiler.env("RUSTC_BOOTSTRAP", "1");
643 runner_compiler.args(compiler_args);
644 runner_compiler.args(&["--crate-type=bin", "-o"]).arg(&output_file);
645 let mut extern_path = std::ffi::OsString::from(format!(
646 "--extern=doctest_bundle_{edition}=",
647 edition = doctest.edition
648 ));
649 for extern_str in &rustdoc_options.extern_strs {
650 if let Some((_cratename, path)) = extern_str.split_once('=') {
651 let dir = Path::new(path)
655 .parent()
656 .filter(|x| x.components().count() > 0)
657 .unwrap_or(Path::new("."));
658 runner_compiler.arg("-L").arg(dir);
659 }
660 }
661 let output_bundle_file = doctest
662 .test_opts
663 .outdir
664 .path()
665 .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition));
666 extern_path.push(&output_bundle_file);
667 runner_compiler.arg(extern_path);
668 runner_compiler.arg(&runner_input_file);
669 if std::fs::write(&runner_input_file, &merged_test_code).is_err() {
670 return Err(TestFailure::CompileError);
673 }
674 if !rustdoc_options.nocapture {
675 runner_compiler.stderr(Stdio::null());
678 }
679 runner_compiler.arg("--error-format=short");
680 debug!("compiler invocation for doctest runner: {runner_compiler:?}");
681
682 let status = if !status.success() {
683 status
684 } else {
685 let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process");
686 child_runner.wait().expect("Failed to wait")
687 };
688
689 process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
690 } else {
691 let stdin = child.stdin.as_mut().expect("Failed to open stdin");
692 stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources");
693 child.wait_with_output().expect("Failed to read stdout")
694 };
695
696 struct Bomb<'a>(&'a str);
697 impl Drop for Bomb<'_> {
698 fn drop(&mut self) {
699 eprint!("{}", self.0);
700 }
701 }
702 let mut out = str::from_utf8(&output.stderr)
703 .unwrap()
704 .lines()
705 .filter(|l| {
706 if let Ok(uext) = serde_json::from_str::<UnusedExterns>(l) {
707 report_unused_externs(uext);
708 false
709 } else {
710 true
711 }
712 })
713 .intersperse_with(|| "\n")
714 .collect::<String>();
715
716 if !out.is_empty() {
719 out.push('\n');
720 }
721
722 let _bomb = Bomb(&out);
723 match (output.status.success(), langstr.compile_fail) {
724 (true, true) => {
725 return Err(TestFailure::UnexpectedCompilePass);
726 }
727 (true, false) => {}
728 (false, true) => {
729 if !langstr.error_codes.is_empty() {
730 let missing_codes: Vec<String> = langstr
734 .error_codes
735 .iter()
736 .filter(|err| !out.contains(&format!("error[{err}]")))
737 .cloned()
738 .collect();
739
740 if !missing_codes.is_empty() {
741 return Err(TestFailure::MissingErrorCodes(missing_codes));
742 }
743 }
744 }
745 (false, false) => {
746 return Err(TestFailure::CompileError);
747 }
748 }
749
750 if doctest.no_run {
751 return Ok(());
752 }
753
754 let mut cmd;
756
757 let output_file = make_maybe_absolute_path(output_file);
758 if let Some(tool) = &rustdoc_options.test_runtool {
759 let tool = make_maybe_absolute_path(tool.into());
760 cmd = Command::new(tool);
761 cmd.args(&rustdoc_options.test_runtool_args);
762 cmd.arg(&output_file);
763 } else {
764 cmd = Command::new(&output_file);
765 if doctest.is_multiple_tests() {
766 cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file);
767 }
768 }
769 if let Some(run_directory) = &rustdoc_options.test_run_directory {
770 cmd.current_dir(run_directory);
771 }
772
773 let result = if doctest.is_multiple_tests() || rustdoc_options.nocapture {
774 cmd.status().map(|status| process::Output {
775 status,
776 stdout: Vec::new(),
777 stderr: Vec::new(),
778 })
779 } else {
780 cmd.output()
781 };
782 match result {
783 Err(e) => return Err(TestFailure::ExecutionError(e)),
784 Ok(out) => {
785 if langstr.should_panic && out.status.success() {
786 return Err(TestFailure::UnexpectedRunPass);
787 } else if !langstr.should_panic && !out.status.success() {
788 return Err(TestFailure::ExecutionFailure(out));
789 }
790 }
791 }
792
793 Ok(())
794}
795
796fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
802 if path.components().count() == 1 {
803 path
805 } else {
806 std::env::current_dir().map(|c| c.join(&path)).unwrap_or_else(|_| path)
807 }
808}
809struct IndividualTestOptions {
810 outdir: DirState,
811 path: PathBuf,
812}
813
814impl IndividualTestOptions {
815 fn new(options: &RustdocOptions, test_id: &Option<String>, test_path: PathBuf) -> Self {
816 let outdir = if let Some(ref path) = options.persist_doctests {
817 let mut path = path.clone();
818 path.push(test_id.as_deref().unwrap_or("<doctest>"));
819
820 if let Err(err) = std::fs::create_dir_all(&path) {
821 eprintln!("Couldn't create directory for doctest executables: {err}");
822 panic::resume_unwind(Box::new(()));
823 }
824
825 DirState::Perm(path)
826 } else {
827 DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir"))
828 };
829
830 Self { outdir, path: test_path }
831 }
832}
833
834#[derive(Debug)]
844pub(crate) struct ScrapedDocTest {
845 filename: FileName,
846 line: usize,
847 langstr: LangString,
848 text: String,
849 name: String,
850 span: Span,
851}
852
853impl ScrapedDocTest {
854 fn new(
855 filename: FileName,
856 line: usize,
857 logical_path: Vec<String>,
858 langstr: LangString,
859 text: String,
860 span: Span,
861 ) -> Self {
862 let mut item_path = logical_path.join("::");
863 item_path.retain(|c| c != ' ');
864 if !item_path.is_empty() {
865 item_path.push(' ');
866 }
867 let name =
868 format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
869
870 Self { filename, line, langstr, text, name, span }
871 }
872 fn edition(&self, opts: &RustdocOptions) -> Edition {
873 self.langstr.edition.unwrap_or(opts.edition)
874 }
875
876 fn no_run(&self, opts: &RustdocOptions) -> bool {
877 self.langstr.no_run || opts.no_run
878 }
879 fn path(&self) -> PathBuf {
880 match &self.filename {
881 FileName::Real(path) => {
882 if let Some(local_path) = path.local_path() {
883 local_path.to_path_buf()
884 } else {
885 unreachable!("doctest from a different crate");
887 }
888 }
889 _ => PathBuf::from(r"doctest.rs"),
890 }
891 }
892}
893
894pub(crate) trait DocTestVisitor {
895 fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine);
896 fn visit_header(&mut self, _name: &str, _level: u32) {}
897}
898
899struct CreateRunnableDocTests {
900 standalone_tests: Vec<test::TestDescAndFn>,
901 mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
902
903 rustdoc_options: Arc<RustdocOptions>,
904 opts: GlobalTestOptions,
905 visited_tests: FxHashMap<(String, usize), usize>,
906 unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
907 compiling_test_count: AtomicUsize,
908 can_merge_doctests: bool,
909}
910
911impl CreateRunnableDocTests {
912 fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests {
913 let can_merge_doctests = rustdoc_options.edition >= Edition::Edition2024;
914 CreateRunnableDocTests {
915 standalone_tests: Vec::new(),
916 mergeable_tests: FxIndexMap::default(),
917 rustdoc_options: Arc::new(rustdoc_options),
918 opts,
919 visited_tests: FxHashMap::default(),
920 unused_extern_reports: Default::default(),
921 compiling_test_count: AtomicUsize::new(0),
922 can_merge_doctests,
923 }
924 }
925
926 fn add_test(&mut self, scraped_test: ScrapedDocTest, dcx: Option<DiagCtxtHandle<'_>>) {
927 let file = scraped_test
929 .filename
930 .prefer_local()
931 .to_string_lossy()
932 .chars()
933 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
934 .collect::<String>();
935 let test_id = format!(
936 "{file}_{line}_{number}",
937 file = file,
938 line = scraped_test.line,
939 number = {
940 self.visited_tests
943 .entry((file.clone(), scraped_test.line))
944 .and_modify(|v| *v += 1)
945 .or_insert(0)
946 },
947 );
948
949 let edition = scraped_test.edition(&self.rustdoc_options);
950 let doctest = BuildDocTestBuilder::new(&scraped_test.text)
951 .crate_name(&self.opts.crate_name)
952 .edition(edition)
953 .can_merge_doctests(self.can_merge_doctests)
954 .test_id(test_id)
955 .lang_str(&scraped_test.langstr)
956 .span(scraped_test.span)
957 .build(dcx);
958 let is_standalone = !doctest.can_be_merged
959 || scraped_test.langstr.compile_fail
960 || scraped_test.langstr.test_harness
961 || scraped_test.langstr.standalone_crate
962 || self.rustdoc_options.nocapture
963 || self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output");
964 if is_standalone {
965 let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
966 self.standalone_tests.push(test_desc);
967 } else {
968 self.mergeable_tests.entry(edition).or_default().push((doctest, scraped_test));
969 }
970 }
971
972 fn generate_test_desc_and_fn(
973 &mut self,
974 test: DocTestBuilder,
975 scraped_test: ScrapedDocTest,
976 ) -> test::TestDescAndFn {
977 if !scraped_test.langstr.compile_fail {
978 self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
979 }
980
981 generate_test_desc_and_fn(
982 test,
983 scraped_test,
984 self.opts.clone(),
985 Arc::clone(&self.rustdoc_options),
986 self.unused_extern_reports.clone(),
987 )
988 }
989}
990
991fn generate_test_desc_and_fn(
992 test: DocTestBuilder,
993 scraped_test: ScrapedDocTest,
994 opts: GlobalTestOptions,
995 rustdoc_options: Arc<RustdocOptions>,
996 unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
997) -> test::TestDescAndFn {
998 let target_str = rustdoc_options.target.to_string();
999 let rustdoc_test_options =
1000 IndividualTestOptions::new(&rustdoc_options, &test.test_id, scraped_test.path());
1001
1002 debug!("creating test {}: {}", scraped_test.name, scraped_test.text);
1003 test::TestDescAndFn {
1004 desc: test::TestDesc {
1005 name: test::DynTestName(scraped_test.name.clone()),
1006 ignore: match scraped_test.langstr.ignore {
1007 Ignore::All => true,
1008 Ignore::None => false,
1009 Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
1010 },
1011 ignore_message: None,
1012 source_file: "",
1013 start_line: 0,
1014 start_col: 0,
1015 end_line: 0,
1016 end_col: 0,
1017 should_panic: test::ShouldPanic::No,
1019 compile_fail: scraped_test.langstr.compile_fail,
1020 no_run: scraped_test.no_run(&rustdoc_options),
1021 test_type: test::TestType::DocTest,
1022 },
1023 testfn: test::DynTestFn(Box::new(move || {
1024 doctest_run_fn(
1025 rustdoc_test_options,
1026 opts,
1027 test,
1028 scraped_test,
1029 rustdoc_options,
1030 unused_externs,
1031 )
1032 })),
1033 }
1034}
1035
1036fn doctest_run_fn(
1037 test_opts: IndividualTestOptions,
1038 global_opts: GlobalTestOptions,
1039 doctest: DocTestBuilder,
1040 scraped_test: ScrapedDocTest,
1041 rustdoc_options: Arc<RustdocOptions>,
1042 unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
1043) -> Result<(), String> {
1044 let report_unused_externs = |uext| {
1045 unused_externs.lock().unwrap().push(uext);
1046 };
1047 let (full_test_code, full_test_line_offset) = doctest.generate_unique_doctest(
1048 &scraped_test.text,
1049 scraped_test.langstr.test_harness,
1050 &global_opts,
1051 Some(&global_opts.crate_name),
1052 );
1053 let runnable_test = RunnableDocTest {
1054 full_test_code,
1055 full_test_line_offset,
1056 test_opts,
1057 global_opts,
1058 langstr: scraped_test.langstr.clone(),
1059 line: scraped_test.line,
1060 edition: scraped_test.edition(&rustdoc_options),
1061 no_run: scraped_test.no_run(&rustdoc_options),
1062 merged_test_code: None,
1063 };
1064 let res =
1065 run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
1066
1067 if let Err(err) = res {
1068 match err {
1069 TestFailure::CompileError => {
1070 eprint!("Couldn't compile the test.");
1071 }
1072 TestFailure::UnexpectedCompilePass => {
1073 eprint!("Test compiled successfully, but it's marked `compile_fail`.");
1074 }
1075 TestFailure::UnexpectedRunPass => {
1076 eprint!("Test executable succeeded, but it's marked `should_panic`.");
1077 }
1078 TestFailure::MissingErrorCodes(codes) => {
1079 eprint!("Some expected error codes were not found: {codes:?}");
1080 }
1081 TestFailure::ExecutionError(err) => {
1082 eprint!("Couldn't run the test: {err}");
1083 if err.kind() == io::ErrorKind::PermissionDenied {
1084 eprint!(" - maybe your tempdir is mounted with noexec?");
1085 }
1086 }
1087 TestFailure::ExecutionFailure(out) => {
1088 eprintln!("Test executable failed ({reason}).", reason = out.status);
1089
1090 let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
1100 let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
1101
1102 if !stdout.is_empty() || !stderr.is_empty() {
1103 eprintln!();
1104
1105 if !stdout.is_empty() {
1106 eprintln!("stdout:\n{stdout}");
1107 }
1108
1109 if !stderr.is_empty() {
1110 eprintln!("stderr:\n{stderr}");
1111 }
1112 }
1113 }
1114 }
1115
1116 panic::resume_unwind(Box::new(()));
1117 }
1118 Ok(())
1119}
1120
1121#[cfg(test)] impl DocTestVisitor for Vec<usize> {
1123 fn visit_test(&mut self, _test: String, _config: LangString, rel_line: MdRelLine) {
1124 self.push(1 + rel_line.offset());
1125 }
1126}
1127
1128#[cfg(test)]
1129mod tests;