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::str::FromStr;
13use std::sync::atomic::{AtomicUsize, Ordering};
14use std::sync::{Arc, Mutex};
15use std::time::{Duration, Instant};
16use std::{panic, str};
17
18pub(crate) use make::{BuildDocTestBuilder, DocTestBuilder};
19pub(crate) use markdown::test as test_markdown;
20use proc_macro2::{TokenStream, TokenTree};
21use rustc_data_structures::fx::{FxHashMap, FxHashSet, FxHasher, FxIndexMap, FxIndexSet};
22use rustc_errors::emitter::HumanReadableErrorType;
23use rustc_errors::{ColorConfig, DiagCtxtHandle};
24use rustc_hir::attrs::AttributeKind;
25use rustc_hir::def_id::LOCAL_CRATE;
26use rustc_hir::{Attribute, CRATE_HIR_ID};
27use rustc_interface::interface;
28use rustc_middle::ty::TyCtxt;
29use rustc_session::config::{self, CrateType, ErrorOutputType, Input};
30use rustc_session::lint;
31use rustc_span::edition::Edition;
32use rustc_span::{FileName, RemapPathScopeComponents, Span};
33use rustc_target::spec::{Target, TargetTuple};
34use tempfile::{Builder as TempFileBuilder, TempDir};
35use tracing::{debug, info};
36
37use self::rust::HirCollector;
38use crate::config::{MergeDoctests, Options as RustdocOptions, OutputFormat};
39use crate::html::markdown::{CodeLineMapping, ErrorCodes, Ignore, LangString, MdRelLine};
40use crate::lint::init_lints;
41
42struct MergedDoctestTimes {
44 total_time: Instant,
45 compilation_time: Duration,
47 added_compilation_times: usize,
49}
50
51impl MergedDoctestTimes {
52 fn new() -> Self {
53 Self {
54 total_time: Instant::now(),
55 compilation_time: Duration::default(),
56 added_compilation_times: 0,
57 }
58 }
59
60 fn add_compilation_time(&mut self, duration: Duration) {
61 self.compilation_time += duration;
62 self.added_compilation_times += 1;
63 }
64
65 fn times_in_secs(&self) -> Option<(f64, f64)> {
67 if self.added_compilation_times == 0 {
71 return None;
72 }
73 Some((self.total_time.elapsed().as_secs_f64(), self.compilation_time.as_secs_f64()))
74 }
75}
76
77#[derive(Clone)]
79pub(crate) struct GlobalTestOptions {
80 pub(crate) crate_name: String,
82 pub(crate) no_crate_inject: bool,
84 pub(crate) insert_indent_space: bool,
87 pub(crate) args_file: PathBuf,
89}
90
91pub(crate) fn generate_args_file(file_path: &Path, options: &RustdocOptions) -> Result<(), String> {
92 let mut file = File::create(file_path)
93 .map_err(|error| format!("failed to create args file: {error:?}"))?;
94
95 let mut content = vec![];
97
98 for cfg in &options.cfgs {
99 content.push(format!("--cfg={cfg}"));
100 }
101 for check_cfg in &options.check_cfgs {
102 content.push(format!("--check-cfg={check_cfg}"));
103 }
104
105 for lib_str in &options.lib_strs {
106 content.push(format!("-L{lib_str}"));
107 }
108 for extern_str in &options.extern_strs {
109 content.push(format!("--extern={extern_str}"));
110 }
111 content.push("-Ccodegen-units=1".to_string());
112 for codegen_options_str in &options.codegen_options_strs {
113 content.push(format!("-C{codegen_options_str}"));
114 }
115 for unstable_option_str in &options.unstable_opts_strs {
116 content.push(format!("-Z{unstable_option_str}"));
117 }
118
119 content.extend(options.doctest_build_args.clone());
120
121 let content = content.join("\n");
122
123 file.write_all(content.as_bytes())
124 .map_err(|error| format!("failed to write arguments to temporary file: {error:?}"))?;
125 Ok(())
126}
127
128fn get_doctest_dir(opts: &RustdocOptions) -> io::Result<TempDir> {
129 let mut builder = TempFileBuilder::new();
130 builder.prefix("rustdoctest");
131 if opts.codegen_options.save_temps {
132 builder.disable_cleanup(true);
133 }
134 builder.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 remap_path_scope: options.remap_path_scope.clone(),
175 unstable_opts: options.unstable_opts.clone(),
176 error_format: options.error_format.clone(),
177 target_modifiers: options.target_modifiers.clone(),
178 ..config::Options::default()
179 };
180
181 let mut cfgs = options.cfgs.clone();
182 cfgs.push("doc".to_owned());
183 cfgs.push("doctest".to_owned());
184 let config = interface::Config {
185 opts: sessopts,
186 crate_cfg: cfgs,
187 crate_check_cfg: options.check_cfgs.clone(),
188 input: input.clone(),
189 output_file: None,
190 output_dir: None,
191 file_loader: None,
192 lint_caps,
193 psess_created: None,
194 track_state: None,
195 register_lints: Some(Box::new(crate::lint::register_lints)),
196 override_queries: None,
197 extra_symbols: Vec::new(),
198 make_codegen_backend: None,
199 ice_file: None,
200 using_internal_features: &rustc_driver::USING_INTERNAL_FEATURES,
201 };
202
203 let externs = options.externs.clone();
204 let json_unused_externs = options.json_unused_externs;
205
206 let temp_dir = match get_doctest_dir(&options)
207 .map_err(|error| format!("failed to create temporary directory: {error:?}"))
208 {
209 Ok(temp_dir) => temp_dir,
210 Err(error) => return crate::wrap_return(dcx, Err(error)),
211 };
212 let args_path = temp_dir.path().join("rustdoc-cfgs");
213 crate::wrap_return(dcx, generate_args_file(&args_path, &options));
214
215 let extract_doctests = options.output_format == OutputFormat::Doctest;
216 let save_temps = options.codegen_options.save_temps;
217 let result = interface::run_compiler(config, |compiler| {
218 let krate = rustc_interface::passes::parse(&compiler.sess);
219
220 let (collector, _incr_comp_session) =
221 rustc_interface::create_and_enter_global_ctxt(compiler, krate, |tcx| {
222 let crate_name = tcx.crate_name(LOCAL_CRATE).to_string();
223 let opts = scrape_test_config(tcx, crate_name, 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
245 .into_iter()
246 .for_each(|t| collector.add_test(t, Some(compiler.sess.dcx())));
247
248 Ok(Some(collector))
249 }
250 });
251 compiler.sess.dcx().abort_if_errors();
252
253 collector
254 });
255
256 let CreateRunnableDocTests {
257 standalone_tests,
258 mergeable_tests,
259 rustdoc_options,
260 opts,
261 unused_extern_reports,
262 compiling_test_count,
263 ..
264 } = match result {
265 Ok(Some(collector)) => collector,
266 Ok(None) => return,
267 Err(error) => {
268 eprintln!("{error}");
269 if !save_temps {
272 let _ = std::fs::remove_dir_all(temp_dir.path());
273 }
274 std::process::exit(1);
275 }
276 };
277
278 run_tests(
279 dcx,
280 opts,
281 &rustdoc_options,
282 &unused_extern_reports,
283 standalone_tests,
284 mergeable_tests,
285 Some(temp_dir),
286 );
287
288 let compiling_test_count = compiling_test_count.load(Ordering::SeqCst);
289
290 if json_unused_externs.is_enabled() {
293 let unused_extern_reports: Vec<_> =
294 std::mem::take(&mut unused_extern_reports.lock().unwrap());
295 if unused_extern_reports.len() == compiling_test_count {
296 let extern_names =
297 externs.iter().map(|(name, _)| name).collect::<FxIndexSet<&String>>();
298 let mut unused_extern_names = unused_extern_reports
299 .iter()
300 .map(|uexts| uexts.unused_extern_names.iter().collect::<FxIndexSet<&String>>())
301 .fold(extern_names, |uextsa, uextsb| {
302 uextsa.intersection(&uextsb).copied().collect::<FxIndexSet<&String>>()
303 })
304 .iter()
305 .map(|v| (*v).clone())
306 .collect::<Vec<String>>();
307 unused_extern_names.sort();
308 let lint_level = unused_extern_reports
310 .iter()
311 .map(|uexts| uexts.lint_level.as_str())
312 .max_by_key(|v| match *v {
313 "warn" => 1,
314 "deny" => 2,
315 "forbid" => 3,
316 v => unreachable!("Invalid lint level '{v}'"),
320 })
321 .unwrap_or("warn")
322 .to_string();
323 let uext = UnusedExterns { lint_level, unused_extern_names };
324 let unused_extern_json = serde_json::to_string(&uext).unwrap();
325 eprintln!("{unused_extern_json}");
326 }
327 }
328}
329
330pub(crate) fn run_tests(
331 dcx: DiagCtxtHandle<'_>,
332 opts: GlobalTestOptions,
333 rustdoc_options: &Arc<RustdocOptions>,
334 unused_extern_reports: &Arc<Mutex<Vec<UnusedExterns>>>,
335 mut standalone_tests: Vec<test::TestDescAndFn>,
336 mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,
337 mut temp_dir: Option<TempDir>,
339) {
340 let mut test_args = Vec::with_capacity(rustdoc_options.test_args.len() + 1);
341 test_args.insert(0, "rustdoctest".to_string());
342 test_args.extend_from_slice(&rustdoc_options.test_args);
343 if rustdoc_options.no_capture {
344 test_args.push("--no-capture".to_string());
345 }
346
347 let mut nb_errors = 0;
348 let mut ran_edition_tests = 0;
349 let mut times = MergedDoctestTimes::new();
350 let target_str = rustdoc_options.target.to_string();
351
352 for (MergeableTestKey { edition, global_crate_attrs_hash }, mut doctests) in mergeable_tests {
353 if doctests.is_empty() {
354 continue;
355 }
356 doctests.sort_by(|(_, a), (_, b)| a.name.cmp(&b.name));
357
358 let mut tests_runner = runner::DocTestRunner::new();
359
360 let rustdoc_test_options = IndividualTestOptions::new(
361 rustdoc_options,
362 &Some(format!("merged_doctest_{edition}_{global_crate_attrs_hash}")),
363 PathBuf::from(format!("doctest_{edition}_{global_crate_attrs_hash}.rs")),
364 );
365
366 for (doctest, scraped_test) in &doctests {
367 tests_runner.add_test(doctest, scraped_test, &target_str);
368 }
369 let (duration, ret) = tests_runner.run_merged_tests(
370 rustdoc_test_options,
371 edition,
372 &opts,
373 &test_args,
374 rustdoc_options,
375 );
376 times.add_compilation_time(duration);
377 if let Ok(success) = ret {
378 ran_edition_tests += 1;
379 if !success {
380 nb_errors += 1;
381 }
382 continue;
383 }
384
385 if rustdoc_options.merge_doctests == MergeDoctests::Always {
386 let mut diag = dcx.struct_fatal("failed to merge doctests");
387 diag.note("requested explicitly on the command line with `--merge-doctests=yes`");
388 diag.emit();
389 }
390
391 debug!("Failed to compile compatible doctests for edition {} all at once", edition);
394 for (doctest, scraped_test) in doctests {
395 doctest.generate_unique_doctest(
396 &scraped_test.text,
397 scraped_test.langstr.test_harness,
398 &opts,
399 Some(&opts.crate_name),
400 );
401 standalone_tests.push(generate_test_desc_and_fn(
402 doctest,
403 scraped_test,
404 opts.clone(),
405 Arc::clone(rustdoc_options),
406 unused_extern_reports.clone(),
407 ));
408 }
409 }
410
411 if ran_edition_tests == 0 || !standalone_tests.is_empty() {
414 standalone_tests.sort_by(|a, b| a.desc.name.as_slice().cmp(b.desc.name.as_slice()));
415 test::test_main_with_exit_callback(&test_args, standalone_tests, None, || {
416 let times = times.times_in_secs();
417 std::mem::drop(temp_dir.take());
419 if let Some((total_time, compilation_time)) = times {
420 test::print_merged_doctests_times(&test_args, total_time, compilation_time);
421 }
422 });
423 } else {
424 if let Some((total_time, compilation_time)) = times.times_in_secs() {
428 test::print_merged_doctests_times(&test_args, total_time, compilation_time);
429 }
430 }
431 std::mem::drop(temp_dir);
433 if nb_errors != 0 {
434 std::process::exit(test::ERROR_EXIT_CODE);
435 }
436}
437
438fn scrape_test_config(
440 tcx: TyCtxt<'_>,
441 crate_name: String,
442 args_file: PathBuf,
443) -> GlobalTestOptions {
444 let mut opts = GlobalTestOptions {
445 crate_name,
446 no_crate_inject: false,
447 insert_indent_space: false,
448 args_file,
449 };
450
451 let source_map = tcx.sess.source_map();
452 'main: for attr in tcx.hir_attrs(CRATE_HIR_ID) {
453 let Attribute::Parsed(AttributeKind::Doc(d)) = attr else { continue };
454 for attr_span in &d.test_attrs {
455 if let Ok(snippet) = source_map.span_to_snippet(*attr_span)
457 && let Ok(stream) = TokenStream::from_str(&snippet)
458 {
459 if stream.into_iter().any(|token| {
461 matches!(
462 token,
463 TokenTree::Ident(i) if i.to_string() == "no_crate_inject",
464 )
465 }) {
466 opts.no_crate_inject = true;
467 break 'main;
468 }
469 }
470 }
471 }
472
473 opts
474}
475
476enum TestFailure {
478 CompileError,
480 UnexpectedCompilePass,
482 MissingErrorCodes(Vec<String>),
485 ExecutionError(io::Error),
487 ExecutionFailure(process::Output),
491 UnexpectedRunPass,
493}
494
495enum DirState {
496 Temp(TempDir),
497 Perm(PathBuf),
498}
499
500impl DirState {
501 fn path(&self) -> &std::path::Path {
502 match self {
503 DirState::Temp(t) => t.path(),
504 DirState::Perm(p) => p.as_path(),
505 }
506 }
507}
508
509#[derive(serde::Serialize, serde::Deserialize)]
514pub(crate) struct UnusedExterns {
515 lint_level: String,
517 unused_extern_names: Vec<String>,
519}
520
521fn add_exe_suffix(input: String, target: &TargetTuple) -> String {
522 let exe_suffix = match target {
523 TargetTuple::TargetTuple(_) => Target::expect_builtin(target).options.exe_suffix,
524 TargetTuple::TargetJson { contents, .. } => {
525 Target::from_json(contents).unwrap().0.options.exe_suffix
526 }
527 };
528 input + &exe_suffix
529}
530
531fn wrapped_rustc_command(rustc_wrappers: &[PathBuf], rustc_binary: &Path) -> Command {
532 let mut args = rustc_wrappers.iter().map(PathBuf::as_path).chain([rustc_binary]);
533
534 let exe = args.next().expect("unable to create rustc command");
535 let mut command = Command::new(exe);
536 for arg in args {
537 command.arg(arg);
538 }
539
540 command
541}
542
543pub(crate) struct RunnableDocTest {
550 full_test_code: String,
551 full_test_line_offset: usize,
552 test_opts: IndividualTestOptions,
553 global_opts: GlobalTestOptions,
554 langstr: LangString,
555 line: usize,
556 edition: Edition,
557 no_run: bool,
558 merged_test_code: Option<String>,
559}
560
561impl RunnableDocTest {
562 fn path_for_merged_doctest_bundle(&self) -> PathBuf {
563 self.test_opts.outdir.path().join(format!("doctest_bundle_{}.rs", self.edition))
564 }
565 fn path_for_merged_doctest_runner(&self) -> PathBuf {
566 self.test_opts.outdir.path().join(format!("doctest_runner_{}.rs", self.edition))
567 }
568 fn is_multiple_tests(&self) -> bool {
569 self.merged_test_code.is_some()
570 }
571}
572
573fn run_test(
580 doctest: RunnableDocTest,
581 rustdoc_options: &RustdocOptions,
582 supports_color: bool,
583 report_unused_externs: impl Fn(UnusedExterns),
584) -> (Duration, Result<(), TestFailure>) {
585 let langstr = &doctest.langstr;
586 let rust_out = add_exe_suffix("rust_out".to_owned(), &rustdoc_options.target);
588 let output_file = doctest.test_opts.outdir.path().join(rust_out);
589 let instant = Instant::now();
590
591 let mut compiler_args = vec![];
595
596 compiler_args.push(format!("@{}", doctest.global_opts.args_file.display()));
597
598 let sysroot = &rustdoc_options.sysroot;
599 if let Some(explicit_sysroot) = &sysroot.explicit {
600 compiler_args.push(format!("--sysroot={}", explicit_sysroot.display()));
601 }
602
603 compiler_args.extend_from_slice(&["--edition".to_owned(), doctest.edition.to_string()]);
604 if langstr.test_harness {
605 compiler_args.push("--test".to_owned());
606 }
607 if rustdoc_options.json_unused_externs.is_enabled() && !langstr.compile_fail {
608 compiler_args.push("--error-format=json".to_owned());
609 compiler_args.extend_from_slice(&["--json".to_owned(), "unused-externs".to_owned()]);
610 compiler_args.extend_from_slice(&["-W".to_owned(), "unused_crate_dependencies".to_owned()]);
611 compiler_args.extend_from_slice(&["-Z".to_owned(), "unstable-options".to_owned()]);
612 }
613
614 if doctest.no_run && !langstr.compile_fail && rustdoc_options.persist_doctests.is_none() {
615 compiler_args.push("--emit=metadata".to_owned());
618 }
619 compiler_args.extend_from_slice(&[
620 "--target".to_owned(),
621 match &rustdoc_options.target {
622 TargetTuple::TargetTuple(s) => s.clone(),
623 TargetTuple::TargetJson { path_for_rustdoc, .. } => {
624 path_for_rustdoc.to_str().expect("target path must be valid unicode").to_owned()
625 }
626 },
627 ]);
628 if let ErrorOutputType::HumanReadable { kind, color_config } = rustdoc_options.error_format {
629 let short = kind.short();
630 let unicode = kind == HumanReadableErrorType { unicode: true, short };
631
632 if short {
633 compiler_args.extend_from_slice(&["--error-format".to_owned(), "short".to_owned()]);
634 }
635 if unicode {
636 compiler_args
637 .extend_from_slice(&["--error-format".to_owned(), "human-unicode".to_owned()]);
638 }
639
640 match color_config {
641 ColorConfig::Never => {
642 compiler_args.extend_from_slice(&["--color".to_owned(), "never".to_owned()]);
643 }
644 ColorConfig::Always => {
645 compiler_args.extend_from_slice(&["--color".to_owned(), "always".to_owned()]);
646 }
647 ColorConfig::Auto => {
648 compiler_args.extend_from_slice(&[
649 "--color".to_owned(),
650 if supports_color { "always" } else { "never" }.to_owned(),
651 ]);
652 }
653 }
654 }
655
656 let rustc_binary = rustdoc_options
657 .test_builder
658 .as_deref()
659 .unwrap_or_else(|| rustc_interface::util::rustc_path(sysroot).expect("found rustc"));
660 let mut compiler = wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
661
662 compiler.args(&compiler_args);
663
664 if doctest.is_multiple_tests() {
667 compiler.arg("--error-format=short");
669 let input_file = doctest.path_for_merged_doctest_bundle();
670 if std::fs::write(&input_file, &doctest.full_test_code).is_err() {
671 return (Duration::default(), Err(TestFailure::CompileError));
674 }
675 if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto {
676 compiler.stderr(Stdio::null());
679 }
680 compiler
682 .arg("--crate-type=lib")
683 .arg("--out-dir")
684 .arg(doctest.test_opts.outdir.path())
685 .arg(input_file);
686 } else {
687 compiler.arg("--crate-type=bin").arg("-o").arg(&output_file);
688 compiler.env("UNSTABLE_RUSTDOC_TEST_PATH", &doctest.test_opts.path);
690 compiler.env(
691 "UNSTABLE_RUSTDOC_TEST_LINE",
692 format!("{}", doctest.line as isize - doctest.full_test_line_offset as isize),
693 );
694 compiler.arg("-");
695 compiler.stdin(Stdio::piped());
696 compiler.stderr(Stdio::piped());
697 }
698
699 info!("compiler invocation for doctest: {compiler:?}");
700
701 let mut child = match compiler.spawn() {
702 Ok(child) => child,
703 Err(error) => {
704 eprintln!("Failed to spawn {:?}: {error:?}", compiler.get_program());
705 return (Duration::default(), Err(TestFailure::CompileError));
706 }
707 };
708 let output = if let Some(merged_test_code) = &doctest.merged_test_code {
709 let status = child.wait().expect("Failed to wait");
711
712 let runner_input_file = doctest.path_for_merged_doctest_runner();
715
716 let mut runner_compiler =
717 wrapped_rustc_command(&rustdoc_options.test_builder_wrappers, rustc_binary);
718 runner_compiler.env("RUSTC_BOOTSTRAP", "1");
721 runner_compiler.args(compiler_args);
722 runner_compiler.args(["--crate-type=bin", "-o"]).arg(&output_file);
723 let mut extern_path = std::ffi::OsString::from(format!(
724 "--extern=doctest_bundle_{edition}=",
725 edition = doctest.edition
726 ));
727
728 let mut seen_search_dirs = FxHashSet::default();
731 for extern_str in &rustdoc_options.extern_strs {
732 if let Some((_cratename, path)) = extern_str.split_once('=') {
733 let dir = Path::new(path)
737 .parent()
738 .filter(|x| x.components().count() > 0)
739 .unwrap_or(Path::new("."));
740 if seen_search_dirs.insert(dir) {
741 runner_compiler.arg("-L").arg(dir);
742 }
743 }
744 }
745 let output_bundle_file = doctest
746 .test_opts
747 .outdir
748 .path()
749 .join(format!("libdoctest_bundle_{edition}.rlib", edition = doctest.edition));
750 extern_path.push(&output_bundle_file);
751 runner_compiler.arg(extern_path);
752 runner_compiler.arg(&runner_input_file);
753 if std::fs::write(&runner_input_file, merged_test_code).is_err() {
754 return (instant.elapsed(), Err(TestFailure::CompileError));
757 }
758 if !rustdoc_options.no_capture && rustdoc_options.merge_doctests == MergeDoctests::Auto {
759 runner_compiler.stderr(Stdio::null());
762 } else {
763 runner_compiler.stderr(Stdio::inherit());
764 }
765 runner_compiler.arg("--error-format=short");
766 info!("compiler invocation for doctest runner: {runner_compiler:?}");
767
768 let status = if !status.success() {
769 status
770 } else {
771 let mut child_runner = match runner_compiler.spawn() {
772 Ok(child) => child,
773 Err(error) => {
774 eprintln!("Failed to spawn {:?}: {error:?}", runner_compiler.get_program());
775 return (Duration::default(), Err(TestFailure::CompileError));
776 }
777 };
778 child_runner.wait().expect("Failed to wait")
779 };
780
781 process::Output { status, stdout: Vec::new(), stderr: Vec::new() }
782 } else {
783 let stdin = child.stdin.as_mut().expect("Failed to open stdin");
784 stdin.write_all(doctest.full_test_code.as_bytes()).expect("could write out test sources");
785 child.wait_with_output().expect("Failed to read stdout")
786 };
787
788 struct Bomb<'a>(&'a str);
789 impl Drop for Bomb<'_> {
790 fn drop(&mut self) {
791 eprint!("{}", self.0);
792 }
793 }
794 let mut out = str::from_utf8(&output.stderr)
795 .unwrap()
796 .lines()
797 .filter(|l| {
798 if let Ok(uext) = serde_json::from_str::<UnusedExterns>(l) {
799 report_unused_externs(uext);
800 false
801 } else {
802 true
803 }
804 })
805 .intersperse_with(|| "\n")
806 .collect::<String>();
807
808 if !out.is_empty() {
811 out.push('\n');
812 }
813
814 let _bomb = Bomb(&out);
815 match (output.status.success(), langstr.compile_fail) {
816 (true, true) => {
817 return (instant.elapsed(), Err(TestFailure::UnexpectedCompilePass));
818 }
819 (true, false) => {}
820 (false, true) => {
821 if !langstr.error_codes.is_empty() {
822 let missing_codes: Vec<String> = langstr
826 .error_codes
827 .iter()
828 .filter(|err| !out.contains(&format!("error[{err}]")))
829 .cloned()
830 .collect();
831
832 if !missing_codes.is_empty() {
833 return (instant.elapsed(), Err(TestFailure::MissingErrorCodes(missing_codes)));
834 }
835 }
836 }
837 (false, false) => {
838 return (instant.elapsed(), Err(TestFailure::CompileError));
839 }
840 }
841
842 let duration = instant.elapsed();
843 if doctest.no_run {
844 return (duration, Ok(()));
845 }
846
847 let mut cmd;
849
850 let output_file = make_maybe_absolute_path(output_file);
851 if let Some(tool) = &rustdoc_options.test_runtool {
852 let tool = make_maybe_absolute_path(tool.into());
853 cmd = Command::new(tool);
854 cmd.args(&rustdoc_options.test_runtool_args);
855 cmd.arg(&output_file);
856 } else {
857 cmd = Command::new(&output_file);
858 if doctest.is_multiple_tests() {
859 cmd.env("RUSTDOC_DOCTEST_BIN_PATH", &output_file);
860 }
861 }
862 if let Some(run_directory) = &rustdoc_options.test_run_directory {
863 cmd.current_dir(run_directory);
864 }
865
866 info!("running doctest executable: {cmd:?}");
867
868 let result = if doctest.is_multiple_tests() || rustdoc_options.no_capture {
869 cmd.status().map(|status| process::Output {
870 status,
871 stdout: Vec::new(),
872 stderr: Vec::new(),
873 })
874 } else {
875 cmd.output()
876 };
877 match result {
878 Err(e) => return (duration, Err(TestFailure::ExecutionError(e))),
879 Ok(out) => {
880 if langstr.should_panic && out.status.success() {
881 return (duration, Err(TestFailure::UnexpectedRunPass));
882 } else if !langstr.should_panic && !out.status.success() {
883 return (duration, Err(TestFailure::ExecutionFailure(out)));
884 }
885 }
886 }
887
888 (duration, Ok(()))
889}
890
891fn make_maybe_absolute_path(path: PathBuf) -> PathBuf {
897 if path.components().count() == 1 {
898 path
900 } else {
901 std::env::current_dir().map(|c| c.join(&path)).unwrap_or_else(|_| path)
902 }
903}
904struct IndividualTestOptions {
905 outdir: DirState,
906 path: PathBuf,
907}
908
909impl IndividualTestOptions {
910 fn new(options: &RustdocOptions, test_id: &Option<String>, test_path: PathBuf) -> Self {
911 let outdir = if let Some(ref path) = options.persist_doctests {
912 let mut path = path.clone();
913 path.push(test_id.as_deref().unwrap_or("<doctest>"));
914
915 if let Err(err) = std::fs::create_dir_all(&path) {
916 eprintln!("Couldn't create directory for doctest executables: {err}");
917 panic::resume_unwind(Box::new(()));
918 }
919
920 DirState::Perm(path)
921 } else {
922 DirState::Temp(get_doctest_dir(options).expect("rustdoc needs a tempdir"))
923 };
924
925 Self { outdir, path: test_path }
926 }
927}
928
929#[derive(Debug)]
939pub(crate) struct ScrapedDocTest {
940 filename: FileName,
941 line: usize,
942 langstr: LangString,
943 text: String,
944 name: String,
945 span: Span,
946 code_mappings: Vec<CodeLineMapping>,
947 global_crate_attrs: Vec<String>,
948}
949
950impl ScrapedDocTest {
951 fn new(
952 filename: FileName,
953 line: usize,
954 logical_path: Vec<String>,
955 langstr: LangString,
956 text: String,
957 span: Span,
958 code_mappings: Vec<CodeLineMapping>,
959 global_crate_attrs: Vec<String>,
960 ) -> Self {
961 let mut item_path = logical_path.join("::");
962 item_path.retain(|c| c != ' ');
963 if !item_path.is_empty() {
964 item_path.push(' ');
965 }
966 let name = format!(
967 "{} - {item_path}(line {line})",
968 filename.display(RemapPathScopeComponents::DOCUMENTATION)
969 );
970
971 Self { filename, line, langstr, text, name, span, code_mappings, global_crate_attrs }
972 }
973 fn edition(&self, opts: &RustdocOptions) -> Edition {
974 self.langstr.edition.unwrap_or(opts.edition)
975 }
976
977 fn no_run(&self, opts: &RustdocOptions) -> bool {
978 self.langstr.no_run || opts.no_run
979 }
980
981 fn path(&self) -> PathBuf {
982 match &self.filename {
983 FileName::Real(name) => {
984 name.path(RemapPathScopeComponents::DOCUMENTATION).to_path_buf()
985 }
986 _ => PathBuf::from(r"doctest.rs"),
987 }
988 }
989}
990
991pub(crate) trait DocTestVisitor {
992 fn visit_test(
993 &mut self,
994 test: String,
995 config: LangString,
996 rel_line: MdRelLine,
997 code_mappings: Vec<CodeLineMapping>,
998 );
999 fn visit_header(&mut self, _name: &str, _level: u32) {}
1000}
1001
1002#[derive(Clone, Debug, Hash, Eq, PartialEq)]
1003pub(crate) struct MergeableTestKey {
1004 edition: Edition,
1005 global_crate_attrs_hash: u64,
1006}
1007
1008struct CreateRunnableDocTests {
1009 standalone_tests: Vec<test::TestDescAndFn>,
1010 mergeable_tests: FxIndexMap<MergeableTestKey, Vec<(DocTestBuilder, ScrapedDocTest)>>,
1011
1012 rustdoc_options: Arc<RustdocOptions>,
1013 opts: GlobalTestOptions,
1014 visited_tests: FxHashMap<(String, usize), usize>,
1015 unused_extern_reports: Arc<Mutex<Vec<UnusedExterns>>>,
1016 compiling_test_count: AtomicUsize,
1017 can_merge_doctests: MergeDoctests,
1018}
1019
1020impl CreateRunnableDocTests {
1021 fn new(rustdoc_options: RustdocOptions, opts: GlobalTestOptions) -> CreateRunnableDocTests {
1022 CreateRunnableDocTests {
1023 standalone_tests: Vec::new(),
1024 mergeable_tests: FxIndexMap::default(),
1025 opts,
1026 visited_tests: FxHashMap::default(),
1027 unused_extern_reports: Default::default(),
1028 compiling_test_count: AtomicUsize::new(0),
1029 can_merge_doctests: rustdoc_options.merge_doctests,
1030 rustdoc_options: Arc::new(rustdoc_options),
1031 }
1032 }
1033
1034 fn add_test(&mut self, scraped_test: ScrapedDocTest, dcx: Option<DiagCtxtHandle<'_>>) {
1035 let file = scraped_test
1040 .filename
1041 .display(RemapPathScopeComponents::MACRO)
1042 .to_string_lossy()
1043 .chars()
1044 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
1045 .collect::<String>();
1046 let test_id = format!(
1047 "{file}_{line}_{number}",
1048 file = file,
1049 line = scraped_test.line,
1050 number = {
1051 self.visited_tests
1054 .entry((file.clone(), scraped_test.line))
1055 .and_modify(|v| *v += 1)
1056 .or_insert(0)
1057 },
1058 );
1059
1060 let edition = scraped_test.edition(&self.rustdoc_options);
1061 let doctest = BuildDocTestBuilder::new(&scraped_test.text)
1062 .crate_name(&self.opts.crate_name)
1063 .global_crate_attrs(scraped_test.global_crate_attrs.clone())
1064 .edition(edition)
1065 .can_merge_doctests(self.can_merge_doctests)
1066 .test_id(test_id)
1067 .lang_str(&scraped_test.langstr)
1068 .span(scraped_test.span)
1069 .code_mappings(&scraped_test.code_mappings)
1070 .build(dcx);
1071 let is_standalone = !doctest.can_be_merged
1072 || self.rustdoc_options.no_capture
1073 || self.rustdoc_options.test_args.iter().any(|arg| arg == "--show-output");
1074 if is_standalone {
1075 let test_desc = self.generate_test_desc_and_fn(doctest, scraped_test);
1076 self.standalone_tests.push(test_desc);
1077 } else {
1078 self.mergeable_tests
1079 .entry(MergeableTestKey {
1080 edition,
1081 global_crate_attrs_hash: {
1082 let mut hasher = FxHasher::default();
1083 scraped_test.global_crate_attrs.hash(&mut hasher);
1084 hasher.finish()
1085 },
1086 })
1087 .or_default()
1088 .push((doctest, scraped_test));
1089 }
1090 }
1091
1092 fn generate_test_desc_and_fn(
1093 &mut self,
1094 test: DocTestBuilder,
1095 scraped_test: ScrapedDocTest,
1096 ) -> test::TestDescAndFn {
1097 if !scraped_test.langstr.compile_fail {
1098 self.compiling_test_count.fetch_add(1, Ordering::SeqCst);
1099 }
1100
1101 generate_test_desc_and_fn(
1102 test,
1103 scraped_test,
1104 self.opts.clone(),
1105 Arc::clone(&self.rustdoc_options),
1106 self.unused_extern_reports.clone(),
1107 )
1108 }
1109}
1110
1111fn generate_test_desc_and_fn(
1112 test: DocTestBuilder,
1113 scraped_test: ScrapedDocTest,
1114 opts: GlobalTestOptions,
1115 rustdoc_options: Arc<RustdocOptions>,
1116 unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
1117) -> test::TestDescAndFn {
1118 let target_str = rustdoc_options.target.to_string();
1119 let rustdoc_test_options =
1120 IndividualTestOptions::new(&rustdoc_options, &test.test_id, scraped_test.path());
1121
1122 debug!("creating test {}: {}", scraped_test.name, scraped_test.text);
1123 test::TestDescAndFn {
1124 desc: test::TestDesc {
1125 name: test::DynTestName(scraped_test.name.clone()),
1126 ignore: match scraped_test.langstr.ignore {
1127 Ignore::All => true,
1128 Ignore::None => false,
1129 Ignore::Some(ref ignores) => ignores.iter().any(|s| target_str.contains(s)),
1130 },
1131 ignore_message: None,
1132 source_file: "",
1133 start_line: 0,
1134 start_col: 0,
1135 end_line: 0,
1136 end_col: 0,
1137 should_panic: test::ShouldPanic::No,
1139 compile_fail: scraped_test.langstr.compile_fail,
1140 no_run: scraped_test.no_run(&rustdoc_options),
1141 test_type: test::TestType::DocTest,
1142 },
1143 testfn: test::DynTestFn(Box::new(move || {
1144 doctest_run_fn(
1145 rustdoc_test_options,
1146 opts,
1147 test,
1148 scraped_test,
1149 rustdoc_options,
1150 unused_externs,
1151 )
1152 })),
1153 }
1154}
1155
1156fn doctest_run_fn(
1157 test_opts: IndividualTestOptions,
1158 global_opts: GlobalTestOptions,
1159 doctest: DocTestBuilder,
1160 scraped_test: ScrapedDocTest,
1161 rustdoc_options: Arc<RustdocOptions>,
1162 unused_externs: Arc<Mutex<Vec<UnusedExterns>>>,
1163) -> Result<(), String> {
1164 let report_unused_externs = |uext| {
1165 unused_externs.lock().unwrap().push(uext);
1166 };
1167 let (wrapped, full_test_line_offset) = doctest.generate_unique_doctest(
1168 &scraped_test.text,
1169 scraped_test.langstr.test_harness,
1170 &global_opts,
1171 Some(&global_opts.crate_name),
1172 );
1173 let runnable_test = RunnableDocTest {
1174 full_test_code: wrapped.to_string(),
1175 full_test_line_offset,
1176 test_opts,
1177 global_opts,
1178 langstr: scraped_test.langstr.clone(),
1179 line: scraped_test.line,
1180 edition: scraped_test.edition(&rustdoc_options),
1181 no_run: scraped_test.no_run(&rustdoc_options),
1182 merged_test_code: None,
1183 };
1184 let (_, res) =
1185 run_test(runnable_test, &rustdoc_options, doctest.supports_color, report_unused_externs);
1186
1187 if let Err(err) = res {
1188 match err {
1189 TestFailure::CompileError => {
1190 eprint!("Couldn't compile the test.");
1191 }
1192 TestFailure::UnexpectedCompilePass => {
1193 eprint!("Test compiled successfully, but it's marked `compile_fail`.");
1194 }
1195 TestFailure::UnexpectedRunPass => {
1196 eprint!("Test executable succeeded, but it's marked `should_panic`.");
1197 }
1198 TestFailure::MissingErrorCodes(codes) => {
1199 eprint!("Some expected error codes were not found: {codes:?}");
1200 }
1201 TestFailure::ExecutionError(err) => {
1202 eprint!("Couldn't run the test: {err}");
1203 if err.kind() == io::ErrorKind::PermissionDenied {
1204 eprint!(" - maybe your tempdir is mounted with noexec?");
1205 }
1206 }
1207 TestFailure::ExecutionFailure(out) => {
1208 eprintln!("Test executable failed ({reason}).", reason = out.status);
1209
1210 let stdout = str::from_utf8(&out.stdout).unwrap_or_default();
1220 let stderr = str::from_utf8(&out.stderr).unwrap_or_default();
1221
1222 if !stdout.is_empty() || !stderr.is_empty() {
1223 eprintln!();
1224
1225 if !stdout.is_empty() {
1226 eprintln!("stdout:\n{stdout}");
1227 }
1228
1229 if !stderr.is_empty() {
1230 eprintln!("stderr:\n{stderr}");
1231 }
1232 }
1233 }
1234 }
1235
1236 panic::resume_unwind(Box::new(()));
1237 }
1238 Ok(())
1239}
1240
1241#[cfg(test)] impl DocTestVisitor for Vec<usize> {
1243 fn visit_test(
1244 &mut self,
1245 _test: String,
1246 _config: LangString,
1247 rel_line: MdRelLine,
1248 _code_mappings: Vec<CodeLineMapping>,
1249 ) {
1250 self.push(1 + rel_line.offset());
1251 }
1252}
1253
1254#[cfg(test)]
1255mod tests;