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