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 sysroot: options.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 extra_symbols: Vec::new(),
195 make_codegen_backend: None,
196 registry: rustc_driver::diagnostics_registry(),
197 ice_file: None,
198 using_internal_features: &rustc_driver::USING_INTERNAL_FEATURES,
199 expanded_args: options.expanded_args.clone(),
200 };
201
202 let externs = options.externs.clone();
203 let json_unused_externs = options.json_unused_externs;
204
205 let temp_dir = match get_doctest_dir()
206 .map_err(|error| format!("failed to create temporary directory: {error:?}"))
207 {
208 Ok(temp_dir) => temp_dir,
209 Err(error) => return crate::wrap_return(dcx, Err(error)),
210 };
211 let args_path = temp_dir.path().join("rustdoc-cfgs");
212 crate::wrap_return(dcx, generate_args_file(&args_path, &options));
213
214 let extract_doctests = options.output_format == OutputFormat::Doctest;
215 let result = interface::run_compiler(config, |compiler| {
216 let krate = rustc_interface::passes::parse(&compiler.sess);
217
218 let collector = rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
219 let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
220 let crate_attrs = tcx.hir_attrs(CRATE_HIR_ID);
221 let opts = scrape_test_config(crate_name, crate_attrs, args_path);
222
223 let hir_collector = HirCollector::new(
224 ErrorCodes::from(compiler.sess.opts.unstable_features.is_nightly_build()),
225 tcx,
226 );
227 let tests = hir_collector.collect_crate();
228 if extract_doctests {
229 let mut collector = extracted::ExtractedDocTests::new();
230 tests.into_iter().for_each(|t| collector.add_test(t, &opts, &options));
231
232 let stdout = std::io::stdout();
233 let mut stdout = stdout.lock();
234 if let Err(error) = serde_json::ser::to_writer(&mut stdout, &collector) {
235 eprintln!();
236 Err(format!("Failed to generate JSON output for doctests: {error:?}"))
237 } else {
238 Ok(None)
239 }
240 } else {
241 let mut collector = CreateRunnableDocTests::new(options, opts);
242 tests.into_iter().for_each(|t| collector.add_test(t));
243
244 Ok(Some(collector))
245 }
246 });
247 compiler.sess.dcx().abort_if_errors();
248
249 collector
250 });
251
252 let CreateRunnableDocTests {
253 standalone_tests,
254 mergeable_tests,
255 rustdoc_options,
256 opts,
257 unused_extern_reports,
258 compiling_test_count,
259 ..
260 } = match result {
261 Ok(Some(collector)) => collector,
262 Ok(None) => return,
263 Err(error) => {
264 eprintln!("{error}");
265 let _ = std::fs::remove_dir_all(temp_dir.path());
268 std::process::exit(1);
269 }
270 };
271
272 run_tests(
273 opts,
274 &rustdoc_options,
275 &unused_extern_reports,
276 standalone_tests,
277 mergeable_tests,
278 Some(temp_dir),
279 );
280
281 let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
282
283 if json_unused_externs.is_enabled() {
286 let unused_extern_reports: Vec<_> =
287 std::mem::take(&mut unused_extern_reports.lock().unwrap());
288 if unused_extern_reports.len() == compiling_test_count {
289 let extern_names =
290 externs.iter().map(|(name, _)| name).collect::<FxIndexSet<&String>>();
291 let mut unused_extern_names = unused_extern_reports
292 .iter()
293 .map(|uexts| uexts.unused_extern_names.iter().collect::<FxIndexSet<&String>>())
294 .fold(extern_names, |uextsa, uextsb| {
295 uextsa.intersection(&uextsb).copied().collect::<FxIndexSet<&String>>()
296 })
297 .iter()
298 .map(|v| (*v).clone())
299 .collect::<Vec<String>>();
300 unused_extern_names.sort();
301 let lint_level = unused_extern_reports
303 .iter()
304 .map(|uexts| uexts.lint_level.as_str())
305 .max_by_key(|v| match *v {
306 "warn" => 1,
307 "deny" => 2,
308 "forbid" => 3,
309 v => unreachable!("Invalid lint level '{v}'"),
313 })
314 .unwrap_or("warn")
315 .to_string();
316 let uext = UnusedExterns { lint_level, unused_extern_names };
317 let unused_extern_json = serde_json::to_string(&uext).unwrap();
318 eprintln!("{unused_extern_json}");
319 }
320 }
321}
322
323pub(crate) fn run_tests(
324 opts: GlobalTestOptions,
325 rustdoc_options: &Arc<RustdocOptions>,
326 unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
327 mut standalone_tests: Vec<test::TestDescAndFn>,
328 mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
329 mut temp_dir: Option<TempDir>,
331) {
332 let mut test_args = Vec::with_capacity(rustdoc_options.test_args.len() + 1);
333 test_args.insert(0, "rustdoctest".to_string());
334 test_args.extend_from_slice(&rustdoc_options.test_args);
335 if rustdoc_options.nocapture {
336 test_args.push("--nocapture".to_string());
337 }
338
339 let mut nb_errors = 0;
340 let mut ran_edition_tests = 0;
341 let target_str = rustdoc_options.target.to_string();
342
343 for (edition, mut doctests) in mergeable_tests {
344 if doctests.is_empty() {
345 continue;
346 }
347 doctests.sort_by(|(_, a), (_, b)| a.name.cmp(&b.name));
348
349 let mut tests_runner = runner::DocTestRunner::new();
350
351 let rustdoc_test_options = IndividualTestOptions::new(
352 rustdoc_options,
353 &Some(format!("merged_doctest_{edition}")),
354 PathBuf::from(format!("doctest_{edition}.rs")),
355 );
356
357 for (doctest, scraped_test) in &doctests {
358 tests_runner.add_test(doctest, scraped_test, &target_str);
359 }
360 if let Ok(success) = tests_runner.run_merged_tests(
361 rustdoc_test_options,
362 edition,
363 &opts,
364 &test_args,
365 rustdoc_options,
366 ) {
367 ran_edition_tests += 1;
368 if !success {
369 nb_errors += 1;
370 }
371 continue;
372 }
373 debug!("Failed to compile compatible doctests for edition {} all at once", edition);
376 for (doctest, scraped_test) in doctests {
377 doctest.generate_unique_doctest(
378 &scraped_test.text,
379 scraped_test.langstr.test_harness,
380 &opts,
381 Some(&opts.crate_name),
382 );
383 standalone_tests.push(generate_test_desc_and_fn(
384 doctest,
385 scraped_test,
386 opts.clone(),
387 Arc::clone(rustdoc_options),
388 unused_extern_reports.clone(),
389 ));
390 }
391 }
392
393 if ran_edition_tests == 0 || !standalone_tests.is_empty() {
396 standalone_tests.sort_by(|a, b| a.desc.name.as_slice().cmp(b.desc.name.as_slice()));
397 test::test_main_with_exit_callback(&test_args, standalone_tests, None, || {
398 std::mem::drop(temp_dir.take());
400 });
401 }
402 if nb_errors != 0 {
403 std::mem::drop(temp_dir);
405 std::process::exit(101);
407 }
408}
409
410fn scrape_test_config(
412 crate_name: String,
413 attrs: &[hir::Attribute],
414 args_file: PathBuf,
415) -> GlobalTestOptions {
416 use rustc_ast_pretty::pprust;
417
418 let mut opts = GlobalTestOptions {
419 crate_name,
420 no_crate_inject: false,
421 attrs: Vec::new(),
422 insert_indent_space: false,
423 args_file,
424 };
425
426 let test_attrs: Vec<_> = attrs
427 .iter()
428 .filter(|a| a.has_name(sym::doc))
429 .flat_map(|a| a.meta_item_list().unwrap_or_default())
430 .filter(|a| a.has_name(sym::test))
431 .collect();
432 let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[]));
433
434 for attr in attrs {
435 if attr.has_name(sym::no_crate_inject) {
436 opts.no_crate_inject = true;
437 }
438 if attr.has_name(sym::attr)
439 && let Some(l) = attr.meta_item_list()
440 {
441 for item in l {
442 opts.attrs.push(pprust::meta_list_item_to_string(item));
443 }
444 }
445 }
446
447 opts
448}
449
450enum TestFailure {
452 CompileError,
454 UnexpectedCompilePass,
456 MissingErrorCodes(Vec<String>),
459 ExecutionError(io::Error),
461 ExecutionFailure(process::Output),
465 UnexpectedRunPass,
467}
468
469enum DirState {
470 Temp(TempDir),
471 Perm(PathBuf),
472}
473
474impl DirState {
475 fn path(&self) -> &std::path::Path {
476 match self {
477 DirState::Temp(t) => t.path(),
478 DirState::Perm(p) => p.as_path(),
479 }
480 }
481}
482
483#[derive(serde::Serialize, serde::Deserialize)]
488pub(crate) struct UnusedExterns {
489 lint_level: String,
491 unused_extern_names: Vec<String>,
493}
494
495fn add_exe_suffix(input: String, target: &TargetTuple) -> String {
496 let exe_suffix = match target {
497 TargetTuple::TargetTuple(_) => Target::expect_builtin(target).options.exe_suffix,
498 TargetTuple::TargetJson { contents, .. } => {
499 Target::from_json(contents.parse().unwrap()).unwrap().0.options.exe_suffix
500 }
501 };
502 input + &exe_suffix
503}
504
505fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Command {
506 let mut args = rustc_wrappers.iter().map(PathBuf::as_path).chain([rustc_binary]);
507
508 let exe = args.next().expect("unable to create rustc command");
509 let mut command = Command::new(exe);
510 for arg in args {
511 command.arg(arg);
512 }
513
514 command
515}
516
517pub(crate) struct RunnableDocTest {
524 full_test_code: String,
525 full_test_line_offset: usize,
526 test_opts: IndividualTestOptions,
527 global_opts: GlobalTestOptions,
528 langstr: LangString,
529 line: usize,
530 edition: Edition,
531 no_run: bool,
532 merged_test_code: Option<String>,
533}
534
535impl RunnableDocTest {
536 fn path_for_merged_doctest_bundle(&self) -> PathBuf {
537 self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition))
538 }
539 fn path_for_merged_doctest_runner(&self) -> PathBuf {
540 self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition))
541 }
542 fn is_multiple_tests(&self) -> bool {
543 self.merged_test_code.is_some()
544 }
545}
546
547fn run_test(
552 doctest: RunnableDocTest,
553 rustdoc_options: &RustdocOptions,
554 supports_color: bool,
555 report_unused_externs: impl Fn(UnusedExterns),
556) -> Result<(), TestFailure> {
557 let langstr = &doctest.langstr;
558 let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
560 let output_file = doctest.test_opts.outdir.path().join(rust_out);
561
562 let mut compiler_args = vec![];
566
567 compiler_args.push(format!("@{}", doctest.global_opts.args_file.display()));
568
569 if let Some(sysroot) = &rustdoc_options.maybe_sysroot {
570 compiler_args.push(format!("--sysroot={}", sysroot.display()));
571 }
572
573 compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]);
574 if langstr.test_harness {
575 compiler_args.push("--test".to_owned());
576 }
577 if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail {
578 compiler_args.push("--error-format=json".to_owned());
579 compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]);
580 compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]);
581 compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]);
582 }
583
584 if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
585 compiler_args.push("--emit=metadata".to_owned());
588 }
589 compiler_args.extend_from_slice(&[
590 "--target".to_owned(),
591 match &rustdoc_options.target {
592 TargetTuple::TargetTuple(s) => s.clone(),
593 TargetTuple::TargetJson { path_for_rustdoc, .. } => {
594 path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned()
595 }
596 },
597 ]);
598 if let ErrorOutputType::HumanReadable { kind, color_config } = rustdoc_options.error_format {
599 let short = kind.short();
600 let unicode = kind == HumanReadableErrorType::Unicode;
601
602 if short {
603 compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]);
604 }
605 if unicode {
606 compiler_args
607 .extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]);
608 }
609
610 match color_config {
611 ColorConfig::Never => {
612 compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]);
613 }
614 ColorConfig::Always => {
615 compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]);
616 }
617 ColorConfig::Auto => {
618 compiler_args.extend_from_slice(&[
619 "--color".to_owned(),
620 if supports_color { "always" } else { "never" }.to_owned(),
621 ]);
622 }
623 }
624 }
625
626 let rustc_binary = rustdoc_options
627 .test_builder
628 .as_deref()
629 .unwrap_or_else(|| rustc_interface::util::rustc_path().expect("found rustc"));
630 let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
631
632 compiler.args(&compiler_args);
633
634 if doctest.is_multiple_tests() {
637 compiler.arg("--error-format=short");
639 let input_file = doctest.path_for_merged_doctest_bundle();
640 if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
641 return Err(TestFailure::CompileError);
644 }
645 if !rustdoc_options.nocapture {
646 compiler.stderr(Stdio::null());
649 }
650 compiler
652 .arg("--crate-type=lib")
653 .arg("--out-dir")
654 .arg(doctest.test_opts.outdir.path())
655 .arg(input_file);
656 } else {
657 compiler.arg("--crate-type=bin").arg("-o").arg(&output_file);
658 compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
660 compiler.env(
661 "UNSTABLE_RUSTDOC_TEST_LINE",
662 format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
663 );
664 compiler.arg("-");
665 compiler.stdin(Stdio::piped());
666 compiler.stderr(Stdio::piped());
667 }
668
669 debug!("compiler invocation for doctest: {compiler:?}");
670
671 let mut child = compiler.spawn().expect("Failed to spawn rustc process");
672 let output = if let Some(merged_test_code) = &doctest.merged_test_code {
673 let status = child.wait().expect("Failed to wait");
675
676 let runner_input_file = doctest.path_for_merged_doctest_runner();
679
680 let mut runner_compiler =
681 wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
682 runner_compiler.env("RUSTC_BOOTSTRAP", "1");
685 runner_compiler.args(compiler_args);
686 runner_compiler.args(&["--crate-type=bin", "-o"]).arg(&output_file);
687 let mut extern_path = std::ffi::OsString::from(format!(
688 "--extern=doctest_bundle_{edition}=",
689 edition = doctest.edition
690 ));
691 for extern_str in &rustdoc_options.extern_strs {
692 if let Some((_cratename, path)) = extern_str.split_once('=') {
693 let dir = Path::new(path)
697 .parent()
698 .filter(|x| x.components().count() > 0)
699 .unwrap_or(Path::new("."));
700 runner_compiler.arg("-L").arg(dir);
701 }
702 }
703 let output_bundle_file = doctest
704 .test_opts
705 .outdir
706 .path()
707 .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition));
708 extern_path.push(&output_bundle_file);
709 runner_compiler.arg(extern_path);
710 runner_compiler.arg(&runner_input_file);
711 if std::fs::write(&runner_input_file, &merged_test_code).is_err() {
712 return Err(TestFailure::CompileError);
715 }
716 if !rustdoc_options.nocapture {
717 runner_compiler.stderr(Stdio::null());
720 }
721 runner_compiler.arg("--error-format=short");
722 debug!("compiler invocation for doctest runner: {runner_compiler:?}");
723
724 let status = if !status.success() {
725 status
726 } else {
727 let mut child_runner = runner_compiler.spawn().expect("Failed to spawn rustc process");
728 child_runner.wait().expect("Failed to wait")
729 };
730
731 process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
732 } else {
733 let stdin = child.stdin.as_mut().expect("Failed to open stdin");
734 stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources");
735 child.wait_with_output().expect("Failed to read stdout")
736 };
737
738 struct Bomb<'a>(&'a str);
739 impl Drop for Bomb<'_> {
740 fn drop(&mut self) {
741 eprint!("{}", self.0);
742 }
743 }
744 let mut out = str::from_utf8(&output.stderr)
745 .unwrap()
746 .lines()
747 .filter(|l| {
748 if let Ok(uext) = serde_json::from_str::<UnusedExterns>(l) {
749 report_unused_externs(uext);
750 false
751 } else {
752 true
753 }
754 })
755 .intersperse_with(|| "\n")
756 .collect::<String>();
757
758 if !out.is_empty() {
761 out.push('\n');
762 }
763
764 let _bomb = Bomb(&out);
765 match (output.status.success(), langstr.compile_fail) {
766 (true, true) => {
767 return Err(TestFailure::UnexpectedCompilePass);
768 }
769 (true, false) => {}
770 (false, true) => {
771 if !langstr.error_codes.is_empty() {
772 let missing_codes: Vec<String> = langstr
776 .error_codes
777 .iter()
778 .filter(|err| !out.contains(&format!("error[{err}]")))
779 .cloned()
780 .collect();
781
782 if !missing_codes.is_empty() {
783 return Err(TestFailure::MissingErrorCodes(missing_codes));
784 }
785 }
786 }
787 (false, false) => {
788 return Err(TestFailure::CompileError);
789 }
790 }
791
792 if doctest.no_run {
793 return Ok(());
794 }
795
796 let mut cmd;
798
799 let output_file = make_maybe_absolute_path(output_file);
800 if let Some(tool) = &rustdoc_options.test_runtool {
801 let tool = make_maybe_absolute_path(tool.into());
802 cmd = Command::new(tool);
803 cmd.args(&rustdoc_options.test_runtool_args);
804 cmd.arg(&output_file);
805 } else {
806 cmd = Command::new(&output_file);
807 if doctest.is_multiple_tests() {
808 cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file);
809 }
810 }
811 if let Some(run_directory) = &rustdoc_options.test_run_directory {
812 cmd.current_dir(run_directory);
813 }
814
815 let result = if doctest.is_multiple_tests() || rustdoc_options.nocapture {
816 cmd.status().map(|status| process::Output {
817 status,
818 stdout: Vec::new(),
819 stderr: Vec::new(),
820 })
821 } else {
822 cmd.output()
823 };
824 match result {
825 Err(e) => return Err(TestFailure::ExecutionError(e)),
826 Ok(out) => {
827 if langstr.should_panic && out.status.success() {
828 return Err(TestFailure::UnexpectedRunPass);
829 } else if !langstr.should_panic && !out.status.success() {
830 return Err(TestFailure::ExecutionFailure(out));
831 }
832 }
833 }
834
835 Ok(())
836}
837
838fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
844 if path.components().count() == 1 {
845 path
847 } else {
848 std::env::current_dir().map(|c| c.join(&path)).unwrap_or_else(|_| path)
849 }
850}
851struct IndividualTestOptions {
852 outdir: DirState,
853 path: PathBuf,
854}
855
856impl IndividualTestOptions {
857 fn new(options: &RustdocOptions, test_id: &Option<String>, test_path: PathBuf) -> Self {
858 let outdir = if let Some(ref path) = options.persist_doctests {
859 let mut path = path.clone();
860 path.push(test_id.as_deref().unwrap_or("<doctest>"));
861
862 if let Err(err) = std::fs::create_dir_all(&path) {
863 eprintln!("Couldn't create directory for doctest executables: {err}");
864 panic::resume_unwind(Box::new(()));
865 }
866
867 DirState::Perm(path)
868 } else {
869 DirState::Temp(get_doctest_dir().expect("rustdoc needs a tempdir"))
870 };
871
872 Self { outdir, path: test_path }
873 }
874}
875
876#[derive(Debug)]
886pub(crate) struct ScrapedDocTest {
887 filename: FileName,
888 line: usize,
889 langstr: LangString,
890 text: String,
891 name: String,
892}
893
894impl ScrapedDocTest {
895 fn new(
896 filename: FileName,
897 line: usize,
898 logical_path: Vec<String>,
899 langstr: LangString,
900 text: String,
901 ) -> Self {
902 let mut item_path = logical_path.join("::");
903 item_path.retain(|c| c != ' ');
904 if !item_path.is_empty() {
905 item_path.push(' ');
906 }
907 let name =
908 format!("{} - {item_path}(line {line})", filename.prefer_remapped_unconditionaly());
909
910 Self { filename, line, langstr, text, name }
911 }
912 fn edition(&self, opts: &RustdocOptions) -> Edition {
913 self.langstr.edition.unwrap_or(opts.edition)
914 }
915
916 fn no_run(&self, opts: &RustdocOptions) -> bool {
917 self.langstr.no_run || opts.no_run
918 }
919 fn path(&self) -> PathBuf {
920 match &self.filename {
921 FileName::Real(path) => {
922 if let Some(local_path) = path.local_path() {
923 local_path.to_path_buf()
924 } else {
925 unreachable!("doctest from a different crate");
927 }
928 }
929 _ => PathBuf::from(r"doctest.rs"),
930 }
931 }
932}
933
934pub(crate) trait DocTestVisitor {
935 fn visit_test(&mut self, test: String, config: LangString, rel_line: MdRelLine);
936 fn visit_header(&mut self, _name: &str, _level: u32) {}
937}
938
939struct CreateRunnableDocTests {
940 standalone_tests: Vec<test::TestDescAndFn>,
941 mergeable_tests: FxIndexMap<Edition, Vec<(DocTestBuilder, ScrapedDocTest)>>,
942
943 rustdoc_options: Arc<RustdocOptions>,
944 opts: GlobalTestOptions,
945 visited_tests: FxHashMap<(String, usize), usize>,
946 unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
947 compiling_test_count: AtomicUsize,
948 can_merge_doctests: bool,
949}
950
951impl CreateRunnableDocTests {
952 fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests {
953 let can_merge_doctests = rustdoc_options.edition >= Edition::Edition2024;
954 CreateRunnableDocTests {
955 standalone_tests: Vec::new(),
956 mergeable_tests: FxIndexMap::default(),
957 rustdoc_options: Arc::new(rustdoc_options),
958 opts,
959 visited_tests: FxHashMap::default(),
960 unused_extern_reports: Default::default(),
961 compiling_test_count: AtomicUsize::new(0),
962 can_merge_doctests,
963 }
964 }
965
966 fn add_test(&mut self, scraped_test: ScrapedDocTest) {
967 let file = scraped_test
969 .filename
970 .prefer_local()
971 .to_string_lossy()
972 .chars()
973 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
974 .collect::<String>();
975 let test_id = format!(
976 "{file}_{line}_{number}",
977 file = file,
978 line = scraped_test.line,
979 number = {
980 self.visited_tests
983 .entry((file.clone(), scraped_test.line))
984 .and_modify(|v| *v += 1)
985 .or_insert(0)
986 },
987 );
988
989 let edition = scraped_test.edition(&self.rustdoc_options);
990 let doctest = DocTestBuilder::new(
991 &scraped_test.text,
992 Some(&self.opts.crate_name),
993 edition,
994 self.can_merge_doctests,
995 Some(test_id),
996 Some(&scraped_test.langstr),
997 );
998 let is_standalone = !doctest.can_be_merged
999 || scraped_test.langstr.compile_fail
1000 || scraped_test.langstr.test_harness
1001 || scraped_test.langstr.standalone_crate
1002 || self.rustdoc_options.nocapture
1003 || self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output");
1004 if is_standalone {
1005 let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
1006 self.standalone_tests.push(test_desc);
1007 } else {
1008 self.mergeable_tests.entry(edition).or_default().push((doctest, scraped_test));
1009 }
1010 }
1011
1012 fn generate_test_desc_and_fn(
1013 &mut self,
1014 test: DocTestBuilder,
1015 scraped_test: ScrapedDocTest,
1016 ) -> test::TestDescAndFn {
1017 if !scraped_test.langstr.compile_fail {
1018 self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
1019 }
1020
1021 generate_test_desc_and_fn(
1022 test,
1023 scraped_test,
1024 self.opts.clone(),
1025 Arc::clone(&self.rustdoc_options),
1026 self.unused_extern_reports.clone(),
1027 )
1028 }
1029}
1030
1031fn generate_test_desc_and_fn(
1032 test: DocTestBuilder,
1033 scraped_test: ScrapedDocTest,
1034 opts: GlobalTestOptions,
1035 rustdoc_options: Arc<RustdocOptions>,
1036 unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
1037) -> test::TestDescAndFn {
1038 let target_str = rustdoc_options.target.to_string();
1039 let rustdoc_test_options =
1040 IndividualTestOptions::new(&rustdoc_options, &test.test_id, scraped_test.path());
1041
1042 debug!("creating test {}: {}", scraped_test.name, scraped_test.text);
1043 test::TestDescAndFn {
1044 desc: test::TestDesc {
1045 name: test::DynTestName(scraped_test.name.clone()),
1046 ignore: match scraped_test.langstr.ignore {
1047 Ignore::All => true,
1048 Ignore::None => false,
1049 Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
1050 },
1051 ignore_message: None,
1052 source_file: "",
1053 start_line: 0,
1054 start_col: 0,
1055 end_line: 0,
1056 end_col: 0,
1057 should_panic: test::ShouldPanic::No,
1059 compile_fail: scraped_test.langstr.compile_fail,
1060 no_run: scraped_test.no_run(&rustdoc_options),
1061 test_type: test::TestType::DocTest,
1062 },
1063 testfn: test::DynTestFn(Box::new(move || {
1064 doctest_run_fn(
1065 rustdoc_test_options,
1066 opts,
1067 test,
1068 scraped_test,
1069 rustdoc_options,
1070 unused_externs,
1071 )
1072 })),
1073 }
1074}
1075
1076fn doctest_run_fn(
1077 test_opts: IndividualTestOptions,
1078 global_opts: GlobalTestOptions,
1079 doctest: DocTestBuilder,
1080 scraped_test: ScrapedDocTest,
1081 rustdoc_options: Arc<RustdocOptions>,
1082 unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
1083) -> Result<(), String> {
1084 let report_unused_externs = |uext| {
1085 unused_externs.lock().unwrap().push(uext);
1086 };
1087 let (full_test_code, full_test_line_offset) = doctest.generate_unique_doctest(
1088 &scraped_test.text,
1089 scraped_test.langstr.test_harness,
1090 &global_opts,
1091 Some(&global_opts.crate_name),
1092 );
1093 let runnable_test = RunnableDocTest {
1094 full_test_code,
1095 full_test_line_offset,
1096 test_opts,
1097 global_opts,
1098 langstr: scraped_test.langstr.clone(),
1099 line: scraped_test.line,
1100 edition: scraped_test.edition(&rustdoc_options),
1101 no_run: scraped_test.no_run(&rustdoc_options),
1102 merged_test_code: None,
1103 };
1104 let res =
1105 run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
1106
1107 if let Err(err) = res {
1108 match err {
1109 TestFailure::CompileError => {
1110 eprint!("Couldn't compile the test.");
1111 }
1112 TestFailure::UnexpectedCompilePass => {
1113 eprint!("Test compiled successfully, but it's marked `compile_fail`.");
1114 }
1115 TestFailure::UnexpectedRunPass => {
1116 eprint!("Test executable succeeded, but it's marked `should_panic`.");
1117 }
1118 TestFailure::MissingErrorCodes(codes) => {
1119 eprint!("Some expected error codes were not found: {codes:?}");
1120 }
1121 TestFailure::ExecutionError(err) => {
1122 eprint!("Couldn't run the test: {err}");
1123 if err.kind() == io::ErrorKind::PermissionDenied {
1124 eprint!(" - maybe your tempdir is mounted with noexec?");
1125 }
1126 }
1127 TestFailure::ExecutionFailure(out) => {
1128 eprintln!("Test executable failed ({reason}).", reason = out.status);
1129
1130 let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
1140 let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
1141
1142 if !stdout.is_empty() || !stderr.is_empty() {
1143 eprintln!();
1144
1145 if !stdout.is_empty() {
1146 eprintln!("stdout:\n{stdout}");
1147 }
1148
1149 if !stderr.is_empty() {
1150 eprintln!("stderr:\n{stderr}");
1151 }
1152 }
1153 }
1154 }
1155
1156 panic::resume_unwind(Box::new(()));
1157 }
1158 Ok(())
1159}
1160
1161#[cfg(test)] impl DocTestVisitor for Vec<usize> {
1163 fn visit_test(&mut self, _test: String, _config: LangString, rel_line: MdRelLine) {
1164 self.push(1 + rel_line.offset());
1165 }
1166}
1167
1168#[cfg(test)]
1169mod tests;