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