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