2 use rustc_data_structures
::fx
::{FxHashMap, FxHashSet}
;
3 use rustc_data_structures
::sync
::Lrc
;
4 use rustc_errors
::{ColorConfig, ErrorReported}
;
6 use rustc_hir
::def_id
::LOCAL_CRATE
;
7 use rustc_hir
::intravisit
;
8 use rustc_hir
::{HirId, CRATE_HIR_ID}
;
9 use rustc_interface
::interface
;
10 use rustc_middle
::hir
::map
::Map
;
11 use rustc_middle
::ty
::TyCtxt
;
12 use rustc_session
::config
::{self, CrateType, ErrorOutputType}
;
13 use rustc_session
::{lint, DiagnosticOutput, Session}
;
14 use rustc_span
::edition
::Edition
;
15 use rustc_span
::source_map
::SourceMap
;
16 use rustc_span
::symbol
::sym
;
17 use rustc_span
::Symbol
;
18 use rustc_span
::{BytePos, FileName, Pos, Span, DUMMY_SP}
;
19 use rustc_target
::spec
::TargetTriple
;
20 use tempfile
::Builder
as TempFileBuilder
;
23 use std
::io
::{self, Write}
;
25 use std
::path
::PathBuf
;
26 use std
::process
::{self, Command, Stdio}
;
28 use std
::sync
::atomic
::{AtomicUsize, Ordering}
;
29 use std
::sync
::{Arc, Mutex}
;
31 use crate::clean
::{types::AttributesExt, Attributes}
;
32 use crate::config
::Options
;
33 use crate::html
::markdown
::{self, ErrorCodes, Ignore, LangString}
;
34 use crate::lint
::init_lints
;
35 use crate::passes
::span_of_attrs
;
37 #[derive(Clone, Default)]
38 crate struct TestOptions
{
39 /// Whether to disable the default `extern crate my_crate;` when creating doctests.
40 crate no_crate_inject
: bool
,
41 /// Whether to emit compilation warnings when compiling doctests. Setting this will suppress
42 /// the default `#![allow(unused)]`.
43 crate display_warnings
: bool
,
44 /// Additional crate-level attributes to add to doctests.
45 crate attrs
: Vec
<String
>,
48 crate fn run(options
: Options
) -> Result
<(), ErrorReported
> {
49 let input
= config
::Input
::File(options
.input
.clone());
51 let invalid_codeblock_attributes_name
= crate::lint
::INVALID_CODEBLOCK_ATTRIBUTES
.name
;
53 // See core::create_config for what's going on here.
54 let allowed_lints
= vec
![
55 invalid_codeblock_attributes_name
.to_owned(),
56 lint
::builtin
::UNKNOWN_LINTS
.name
.to_owned(),
57 lint
::builtin
::RENAMED_AND_REMOVED_LINTS
.name
.to_owned(),
60 let (lint_opts
, lint_caps
) = init_lints(allowed_lints
, options
.lint_opts
.clone(), |lint
| {
61 if lint
.name
== invalid_codeblock_attributes_name
{
64 Some((lint
.name_lower(), lint
::Allow
))
69 if options
.proc_macro_crate { vec![CrateType::ProcMacro] }
else { vec![CrateType::Rlib] }
;
71 let sessopts
= config
::Options
{
72 maybe_sysroot
: options
.maybe_sysroot
.clone(),
73 search_paths
: options
.libs
.clone(),
75 lint_opts
: if !options
.display_warnings { lint_opts }
else { vec![] }
,
76 lint_cap
: Some(options
.lint_cap
.clone().unwrap_or_else(|| lint
::Forbid
)),
77 cg
: options
.codegen_options
.clone(),
78 externs
: options
.externs
.clone(),
79 unstable_features
: options
.render_options
.unstable_features
,
80 actually_rustdoc
: true,
81 edition
: options
.edition
,
82 target_triple
: options
.target
.clone(),
83 crate_name
: options
.crate_name
.clone(),
84 ..config
::Options
::default()
87 let mut cfgs
= options
.cfgs
.clone();
88 cfgs
.push("doc".to_owned());
89 cfgs
.push("doctest".to_owned());
90 let config
= interface
::Config
{
92 crate_cfg
: interface
::parse_cfgspecs(cfgs
),
98 diagnostic_output
: DiagnosticOutput
::Default
,
101 parse_sess_created
: None
,
102 register_lints
: Some(Box
::new(crate::lint
::register_lints
)),
103 override_queries
: None
,
104 make_codegen_backend
: None
,
105 registry
: rustc_driver
::diagnostics_registry(),
108 let test_args
= options
.test_args
.clone();
109 let display_warnings
= options
.display_warnings
;
110 let nocapture
= options
.nocapture
;
111 let externs
= options
.externs
.clone();
112 let json_unused_externs
= options
.json_unused_externs
;
114 let res
= interface
::run_compiler(config
, |compiler
| {
115 compiler
.enter(|queries
| {
116 let mut global_ctxt
= queries
.global_ctxt()?
.take();
118 let collector
= global_ctxt
.enter(|tcx
| {
119 let krate
= tcx
.hir().krate();
120 let crate_attrs
= tcx
.hir().attrs(CRATE_HIR_ID
);
122 let mut opts
= scrape_test_config(crate_attrs
);
123 opts
.display_warnings
|= options
.display_warnings
;
124 let enable_per_target_ignores
= options
.enable_per_target_ignores
;
125 let mut collector
= Collector
::new(
126 tcx
.crate_name(LOCAL_CRATE
),
130 Some(compiler
.session().parse_sess
.clone_source_map()),
132 enable_per_target_ignores
,
135 let mut hir_collector
= HirCollector
{
136 sess
: compiler
.session(),
137 collector
: &mut collector
,
139 codes
: ErrorCodes
::from(
140 compiler
.session().opts
.unstable_features
.is_nightly_build(),
144 hir_collector
.visit_testable(
147 krate
.module().inner
,
149 intravisit
::walk_crate(this
, krate
);
155 compiler
.session().abort_if_errors();
157 let unused_extern_reports
= collector
.unused_extern_reports
.clone();
158 let compiling_test_count
= collector
.compiling_test_count
.load(Ordering
::SeqCst
);
159 let ret
: Result
<_
, ErrorReported
> =
160 Ok((collector
.tests
, unused_extern_reports
, compiling_test_count
));
164 let (tests
, unused_extern_reports
, compiling_test_count
) = match res
{
166 Err(ErrorReported
) => return Err(ErrorReported
),
169 run_tests(test_args
, nocapture
, display_warnings
, tests
);
171 // Collect and warn about unused externs, but only if we've gotten
172 // reports for each doctest
173 if json_unused_externs
{
174 let unused_extern_reports
: Vec
<_
> =
175 std
::mem
::take(&mut unused_extern_reports
.lock().unwrap());
176 if unused_extern_reports
.len() == compiling_test_count
{
177 let extern_names
= externs
.iter().map(|(name
, _
)| name
).collect
::<FxHashSet
<&String
>>();
178 let mut unused_extern_names
= unused_extern_reports
180 .map(|uexts
| uexts
.unused_extern_names
.iter().collect
::<FxHashSet
<&String
>>())
181 .fold(extern_names
, |uextsa
, uextsb
| {
182 uextsa
.intersection(&uextsb
).map(|v
| *v
).collect
::<FxHashSet
<&String
>>()
185 .map(|v
| (*v
).clone())
186 .collect
::<Vec
<String
>>();
187 unused_extern_names
.sort();
188 // Take the most severe lint level
189 let lint_level
= unused_extern_reports
191 .map(|uexts
| uexts
.lint_level
.as_str())
192 .max_by_key(|v
| match *v
{
196 // The allow lint level is not expected,
197 // as if allow is specified, no message
199 v
=> unreachable
!("Invalid lint level '{}'", v
),
203 let uext
= UnusedExterns { lint_level, unused_extern_names }
;
204 let unused_extern_json
= serde_json
::to_string(&uext
).unwrap();
205 eprintln
!("{}", unused_extern_json
);
213 mut test_args
: Vec
<String
>,
215 display_warnings
: bool
,
216 tests
: Vec
<test
::TestDescAndFn
>,
218 test_args
.insert(0, "rustdoctest".to_string());
220 test_args
.push("--nocapture".to_string());
222 test
::test_main(&test_args
, tests
, Some(test
::Options
::new().display_output(display_warnings
)));
225 // Look for `#![doc(test(no_crate_inject))]`, used by crates in the std facade.
226 fn scrape_test_config(attrs
: &[ast
::Attribute
]) -> TestOptions
{
227 use rustc_ast_pretty
::pprust
;
230 TestOptions { no_crate_inject: false, display_warnings: false, attrs: Vec::new() }
;
232 let test_attrs
: Vec
<_
> = attrs
234 .filter(|a
| a
.has_name(sym
::doc
))
235 .flat_map(|a
| a
.meta_item_list().unwrap_or_else(Vec
::new
))
236 .filter(|a
| a
.has_name(sym
::test
))
238 let attrs
= test_attrs
.iter().flat_map(|a
| a
.meta_item_list().unwrap_or(&[]));
241 if attr
.has_name(sym
::no_crate_inject
) {
242 opts
.no_crate_inject
= true;
244 if attr
.has_name(sym
::attr
) {
245 if let Some(l
) = attr
.meta_item_list() {
247 opts
.attrs
.push(pprust
::meta_list_item_to_string(item
));
256 /// Documentation test failure modes.
258 /// The test failed to compile.
260 /// The test is marked `compile_fail` but compiled successfully.
261 UnexpectedCompilePass
,
262 /// The test failed to compile (as expected) but the compiler output did not contain all
263 /// expected error codes.
264 MissingErrorCodes(Vec
<String
>),
265 /// The test binary was unable to be executed.
266 ExecutionError(io
::Error
),
267 /// The test binary exited with a non-zero exit code.
269 /// This typically means an assertion in the test failed or another form of panic occurred.
270 ExecutionFailure(process
::Output
),
271 /// The test is marked `should_panic` but the test binary executed successfully.
276 Temp(tempfile
::TempDir
),
281 fn path(&self) -> &std
::path
::Path
{
283 DirState
::Temp(t
) => t
.path(),
284 DirState
::Perm(p
) => p
.as_path(),
289 // NOTE: Keep this in sync with the equivalent structs in rustc
291 // We could unify this struct the one in rustc but they have different
292 // ownership semantics, so doing so would create wasteful allocations.
293 #[derive(serde::Serialize, serde::Deserialize)]
294 struct UnusedExterns
{
295 /// Lint level of the unused_crate_dependencies lint
297 /// List of unused externs by their names.
298 unused_extern_names
: Vec
<String
>,
308 as_test_harness
: bool
,
309 runtool
: Option
<String
>,
310 runtool_args
: Vec
<String
>,
311 target
: TargetTriple
,
313 mut error_codes
: Vec
<String
>,
319 report_unused_externs
: impl Fn(UnusedExterns
),
320 ) -> Result
<(), TestFailure
> {
321 let (test
, line_offset
, supports_color
) =
322 make_test(test
, Some(crate_name
), as_test_harness
, opts
, edition
, Some(test_id
));
324 let output_file
= outdir
.path().join("rust_out");
326 let rustc_binary
= options
329 .unwrap_or_else(|| rustc_interface
::util
::rustc_path().expect("found rustc"));
330 let mut compiler
= Command
::new(&rustc_binary
);
331 compiler
.arg("--crate-type").arg("bin");
332 for cfg
in &options
.cfgs
{
333 compiler
.arg("--cfg").arg(&cfg
);
335 if let Some(sysroot
) = options
.maybe_sysroot
{
336 compiler
.arg("--sysroot").arg(sysroot
);
338 compiler
.arg("--edition").arg(&edition
.to_string());
339 compiler
.env("UNSTABLE_RUSTDOC_TEST_PATH", path
);
340 compiler
.env("UNSTABLE_RUSTDOC_TEST_LINE", format
!("{}", line
as isize - line_offset
as isize));
341 compiler
.arg("-o").arg(&output_file
);
343 compiler
.arg("--test");
345 if options
.json_unused_externs
&& !compile_fail
{
346 compiler
.arg("--error-format=json");
347 compiler
.arg("--json").arg("unused-externs");
348 compiler
.arg("-Z").arg("unstable-options");
349 compiler
.arg("-W").arg("unused_crate_dependencies");
351 for lib_str
in &options
.lib_strs
{
352 compiler
.arg("-L").arg(&lib_str
);
354 for extern_str
in &options
.extern_strs
{
355 compiler
.arg("--extern").arg(&extern_str
);
357 compiler
.arg("-Ccodegen-units=1");
358 for codegen_options_str
in &options
.codegen_options_strs
{
359 compiler
.arg("-C").arg(&codegen_options_str
);
361 for debugging_option_str
in &options
.debugging_opts_strs
{
362 compiler
.arg("-Z").arg(&debugging_option_str
);
364 if no_run
&& !compile_fail
&& options
.persist_doctests
.is_none() {
365 compiler
.arg("--emit=metadata");
367 compiler
.arg("--target").arg(match target
{
368 TargetTriple
::TargetTriple(s
) => s
,
369 TargetTriple
::TargetPath(path
) => {
370 path
.to_str().expect("target path must be valid unicode").to_string()
373 if let ErrorOutputType
::HumanReadable(kind
) = options
.error_format
{
374 let (short
, color_config
) = kind
.unzip();
377 compiler
.arg("--error-format").arg("short");
381 ColorConfig
::Never
=> {
382 compiler
.arg("--color").arg("never");
384 ColorConfig
::Always
=> {
385 compiler
.arg("--color").arg("always");
387 ColorConfig
::Auto
=> {
388 compiler
.arg("--color").arg(if supports_color { "always" }
else { "never" }
);
394 compiler
.stdin(Stdio
::piped());
395 compiler
.stderr(Stdio
::piped());
397 let mut child
= compiler
.spawn().expect("Failed to spawn rustc process");
399 let stdin
= child
.stdin
.as_mut().expect("Failed to open stdin");
400 stdin
.write_all(test
.as_bytes()).expect("could write out test sources");
402 let output
= child
.wait_with_output().expect("Failed to read stdout");
404 struct Bomb
<'a
>(&'a
str);
405 impl Drop
for Bomb
<'_
> {
407 eprint
!("{}", self.0);
410 let mut out_lines
= str::from_utf8(&output
.stderr
)
414 if let Ok(uext
) = serde_json
::from_str
::<UnusedExterns
>(l
) {
415 report_unused_externs(uext
);
421 .collect
::<Vec
<_
>>();
423 // Add a \n to the end to properly terminate the last line,
424 // but only if there was output to be printed
425 if out_lines
.len() > 0 {
429 let out
= out_lines
.join("\n");
430 let _bomb
= Bomb(&out
);
431 match (output
.status
.success(), compile_fail
) {
433 return Err(TestFailure
::UnexpectedCompilePass
);
437 if !error_codes
.is_empty() {
438 // We used to check if the output contained "error[{}]: " but since we added the
439 // colored output, we can't anymore because of the color escape characters before
441 error_codes
.retain(|err
| !out
.contains(&format
!("error[{}]", err
)));
443 if !error_codes
.is_empty() {
444 return Err(TestFailure
::MissingErrorCodes(error_codes
));
449 return Err(TestFailure
::CompileError
);
460 if let Some(tool
) = runtool
{
461 cmd
= Command
::new(tool
);
462 cmd
.args(runtool_args
);
463 cmd
.arg(output_file
);
465 cmd
= Command
::new(output_file
);
467 if let Some(run_directory
) = options
.test_run_directory
{
468 cmd
.current_dir(run_directory
);
471 let result
= if options
.nocapture
{
472 cmd
.status().map(|status
| process
::Output
{
481 Err(e
) => return Err(TestFailure
::ExecutionError(e
)),
483 if should_panic
&& out
.status
.success() {
484 return Err(TestFailure
::UnexpectedRunPass
);
485 } else if !should_panic
&& !out
.status
.success() {
486 return Err(TestFailure
::ExecutionFailure(out
));
494 /// Transforms a test into code that can be compiled into a Rust binary, and returns the number of
495 /// lines before the test code begins as well as if the output stream supports colors or not.
498 crate_name
: Option
<&str>,
499 dont_insert_main
: bool
,
502 test_id
: Option
<&str>,
503 ) -> (String
, usize, bool
) {
504 let (crate_attrs
, everything_else
, crates
) = partition_source(s
);
505 let everything_else
= everything_else
.trim();
506 let mut line_offset
= 0;
507 let mut prog
= String
::new();
508 let mut supports_color
= false;
510 if opts
.attrs
.is_empty() && !opts
.display_warnings
{
511 // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
512 // lints that are commonly triggered in doctests. The crate-level test attributes are
513 // commonly used to make tests fail in case they trigger warnings, so having this there in
514 // that case may cause some tests to pass when they shouldn't have.
515 prog
.push_str("#![allow(unused)]\n");
519 // Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
520 for attr
in &opts
.attrs
{
521 prog
.push_str(&format
!("#![{}]\n", attr
));
525 // Now push any outer attributes from the example, assuming they
526 // are intended to be crate attributes.
527 prog
.push_str(&crate_attrs
);
528 prog
.push_str(&crates
);
530 // Uses librustc_ast to parse the doctest and find if there's a main fn and the extern
531 // crate already is included.
532 let result
= rustc_driver
::catch_fatal_errors(|| {
533 rustc_span
::create_session_if_not_set_then(edition
, |_
| {
534 use rustc_errors
::emitter
::{Emitter, EmitterWriter}
;
535 use rustc_errors
::Handler
;
536 use rustc_parse
::maybe_new_parser_from_source_str
;
537 use rustc_parse
::parser
::ForceCollect
;
538 use rustc_session
::parse
::ParseSess
;
539 use rustc_span
::source_map
::FilePathMapping
;
541 let filename
= FileName
::anon_source_code(s
);
542 let source
= crates
+ everything_else
;
544 // Any errors in parsing should also appear when the doctest is compiled for real, so just
545 // send all the errors that librustc_ast emits directly into a `Sink` instead of stderr.
546 let sm
= Lrc
::new(SourceMap
::new(FilePathMapping
::empty()));
548 EmitterWriter
::stderr(ColorConfig
::Auto
, None
, false, false, Some(80), false)
552 EmitterWriter
::new(Box
::new(io
::sink()), None
, false, false, false, None
, false);
554 // FIXME(misdreavus): pass `-Z treat-err-as-bug` to the doctest parser
555 let handler
= Handler
::with_emitter(false, None
, Box
::new(emitter
));
556 let sess
= ParseSess
::with_span_handler(handler
, sm
);
558 let mut found_main
= false;
559 let mut found_extern_crate
= crate_name
.is_none();
560 let mut found_macro
= false;
562 let mut parser
= match maybe_new_parser_from_source_str(&sess
, filename
, source
) {
565 for mut err
in errs
{
569 return (found_main
, found_extern_crate
, found_macro
);
574 match parser
.parse_item(ForceCollect
::No
) {
577 if let ast
::ItemKind
::Fn(..) = item
.kind
{
578 if item
.ident
.name
== sym
::main
{
584 if !found_extern_crate
{
585 if let ast
::ItemKind
::ExternCrate(original
) = item
.kind
{
586 // This code will never be reached if `crate_name` is none because
587 // `found_extern_crate` is initialized to `true` if it is none.
588 let crate_name
= crate_name
.unwrap();
591 Some(name
) => found_extern_crate
= name
.as_str() == crate_name
,
592 None
=> found_extern_crate
= item
.ident
.as_str() == crate_name
,
598 if let ast
::ItemKind
::MacCall(..) = item
.kind
{
603 if found_main
&& found_extern_crate
{
615 // Reset errors so that they won't be reported as compiler bugs when dropping the
616 // handler. Any errors in the tests will be reported when the test file is compiled,
617 // Note that we still need to cancel the errors above otherwise `DiagnosticBuilder`
618 // will panic on drop.
619 sess
.span_diagnostic
.reset_err_count();
621 (found_main
, found_extern_crate
, found_macro
)
624 let (already_has_main
, already_has_extern_crate
, found_macro
) = match result
{
625 Ok(result
) => result
,
626 Err(ErrorReported
) => {
627 // If the parser panicked due to a fatal error, pass the test code through unchanged.
628 // The error will be reported during compilation.
629 return (s
.to_owned(), 0, false);
633 // If a doctest's `fn main` is being masked by a wrapper macro, the parsing loop above won't
634 // see it. In that case, run the old text-based scan to see if they at least have a main
635 // function written inside a macro invocation. See
636 // https://github.com/rust-lang/rust/issues/56898
637 let already_has_main
= if found_macro
&& !already_has_main
{
640 let comment
= line
.find("//");
641 if let Some(comment_begins
) = comment { &line[0..comment_begins] }
else { line }
643 .any(|code
| code
.contains("fn main"))
648 // Don't inject `extern crate std` because it's already injected by the
650 if !already_has_extern_crate
&& !opts
.no_crate_inject
&& crate_name
!= Some("std") {
651 if let Some(crate_name
) = crate_name
{
652 // Don't inject `extern crate` if the crate is never used.
653 // NOTE: this is terribly inaccurate because it doesn't actually
654 // parse the source, but only has false positives, not false
656 if s
.contains(crate_name
) {
657 prog
.push_str(&format
!("extern crate r#{};\n", crate_name
));
663 // FIXME: This code cannot yet handle no_std test cases yet
664 if dont_insert_main
|| already_has_main
|| prog
.contains("![no_std]") {
665 prog
.push_str(everything_else
);
667 let returns_result
= everything_else
.trim_end().ends_with("(())");
668 // Give each doctest main function a unique name.
669 // This is for example needed for the tooling around `-Z instrument-coverage`.
670 let inner_fn_name
= if let Some(test_id
) = test_id
{
671 format
!("_doctest_main_{}", test_id
)
675 let inner_attr
= if test_id
.is_some() { "#[allow(non_snake_case)] " }
else { "" }
;
676 let (main_pre
, main_post
) = if returns_result
{
679 "fn main() {{ {}fn {}() -> Result<(), impl core::fmt::Debug> {{\n",
680 inner_attr
, inner_fn_name
682 format
!("\n}} {}().unwrap() }}", inner_fn_name
),
684 } else if test_id
.is_some() {
686 format
!("fn main() {{ {}fn {}() {{\n", inner_attr
, inner_fn_name
),
687 format
!("\n}} {}() }}", inner_fn_name
),
690 ("fn main() {\n".into(), "\n}".into())
692 // Note on newlines: We insert a line/newline *before*, and *after*
693 // the doctest and adjust the `line_offset` accordingly.
694 // In the case of `-Z instrument-coverage`, this means that the generated
695 // inner `main` function spans from the doctest opening codeblock to the
696 // closing one. For example
697 // /// ``` <- start of the inner main
698 // /// <- code under doctest
699 // /// ``` <- end of the inner main
702 prog
.extend([&main_pre
, everything_else
, &main_post
].iter().cloned());
705 debug
!("final doctest:\n{}", prog
);
707 (prog
, line_offset
, supports_color
)
710 // FIXME(aburka): use a real parser to deal with multiline attributes
711 fn partition_source(s
: &str) -> (String
, String
, String
) {
712 #[derive(Copy, Clone, PartialEq)]
713 enum PartitionState
{
718 let mut state
= PartitionState
::Attrs
;
719 let mut before
= String
::new();
720 let mut crates
= String
::new();
721 let mut after
= String
::new();
723 for line
in s
.lines() {
724 let trimline
= line
.trim();
726 // FIXME(misdreavus): if a doc comment is placed on an extern crate statement, it will be
727 // shunted into "everything else"
729 PartitionState
::Attrs
=> {
730 state
= if trimline
.starts_with("#![")
731 || trimline
.chars().all(|c
| c
.is_whitespace())
732 || (trimline
.starts_with("//") && !trimline
.starts_with("///"))
734 PartitionState
::Attrs
735 } else if trimline
.starts_with("extern crate")
736 || trimline
.starts_with("#[macro_use] extern crate")
738 PartitionState
::Crates
740 PartitionState
::Other
743 PartitionState
::Crates
=> {
744 state
= if trimline
.starts_with("extern crate")
745 || trimline
.starts_with("#[macro_use] extern crate")
746 || trimline
.chars().all(|c
| c
.is_whitespace())
747 || (trimline
.starts_with("//") && !trimline
.starts_with("///"))
749 PartitionState
::Crates
751 PartitionState
::Other
754 PartitionState
::Other
=> {}
758 PartitionState
::Attrs
=> {
759 before
.push_str(line
);
762 PartitionState
::Crates
=> {
763 crates
.push_str(line
);
766 PartitionState
::Other
=> {
767 after
.push_str(line
);
773 debug
!("before:\n{}", before
);
774 debug
!("crates:\n{}", crates
);
775 debug
!("after:\n{}", after
);
777 (before
, after
, crates
)
781 fn add_test(&mut self, test
: String
, config
: LangString
, line
: usize);
782 fn get_line(&self) -> usize {
785 fn register_header(&mut self, _name
: &str, _level
: u32) {}
788 crate struct Collector
{
789 crate tests
: Vec
<test
::TestDescAndFn
>,
791 // The name of the test displayed to the user, separated by `::`.
793 // In tests from Rust source, this is the path to the item
794 // e.g., `["std", "vec", "Vec", "push"]`.
796 // In tests from a markdown file, this is the titles of all headers (h1~h6)
797 // of the sections that contain the code block, e.g., if the markdown file is
810 // the `names` vector of that test will be `["Title", "Subtitle"]`.
815 enable_per_target_ignores
: bool
,
819 source_map
: Option
<Lrc
<SourceMap
>>,
820 filename
: Option
<PathBuf
>,
821 visited_tests
: FxHashMap
<(String
, usize), usize>,
822 unused_extern_reports
: Arc
<Mutex
<Vec
<UnusedExterns
>>>,
823 compiling_test_count
: AtomicUsize
,
832 source_map
: Option
<Lrc
<SourceMap
>>,
833 filename
: Option
<PathBuf
>,
834 enable_per_target_ignores
: bool
,
841 enable_per_target_ignores
,
847 visited_tests
: FxHashMap
::default(),
848 unused_extern_reports
: Default
::default(),
849 compiling_test_count
: AtomicUsize
::new(0),
853 fn generate_name(&self, line
: usize, filename
: &FileName
) -> String
{
854 let mut item_path
= self.names
.join("::");
855 if !item_path
.is_empty() {
858 format
!("{} - {}(line {})", filename
.prefer_local(), item_path
, line
)
861 crate fn set_position(&mut self, position
: Span
) {
862 self.position
= position
;
865 fn get_filename(&self) -> FileName
{
866 if let Some(ref source_map
) = self.source_map
{
867 let filename
= source_map
.span_to_filename(self.position
);
868 if let FileName
::Real(ref filename
) = filename
{
869 if let Ok(cur_dir
) = env
::current_dir() {
870 if let Some(local_path
) = filename
.local_path() {
871 if let Ok(path
) = local_path
.strip_prefix(&cur_dir
) {
872 return path
.to_owned().into();
878 } else if let Some(ref filename
) = self.filename
{
879 filename
.clone().into()
881 FileName
::Custom("input".to_owned())
886 impl Tester
for Collector
{
887 fn add_test(&mut self, test
: String
, config
: LangString
, line
: usize) {
888 let filename
= self.get_filename();
889 let name
= self.generate_name(line
, &filename
);
890 let crate_name
= self.crate_name
.to_string();
891 let opts
= self.opts
.clone();
892 let edition
= config
.edition
.unwrap_or(self.options
.edition
);
893 let options
= self.options
.clone();
894 let runtool
= self.options
.runtool
.clone();
895 let runtool_args
= self.options
.runtool_args
.clone();
896 let target
= self.options
.target
.clone();
897 let target_str
= target
.to_string();
898 let unused_externs
= self.unused_extern_reports
.clone();
899 let no_run
= config
.no_run
|| options
.no_run
;
900 if !config
.compile_fail
{
901 self.compiling_test_count
.fetch_add(1, Ordering
::SeqCst
);
904 let path
= match &filename
{
905 FileName
::Real(path
) => {
906 if let Some(local_path
) = path
.local_path() {
907 local_path
.to_path_buf()
909 // Somehow we got the filename from the metadata of another crate, should never happen
910 unreachable
!("doctest from a different crate");
913 _
=> PathBuf
::from(r
"doctest.rs"),
916 // For example `module/file.rs` would become `module_file_rs`
921 .map(|c
| if c
.is_ascii_alphanumeric() { c }
else { '_' }
)
922 .collect
::<String
>();
923 let test_id
= format
!(
924 "{file}_{line}_{number}",
928 // Increases the current test number, if this file already
929 // exists or it creates a new entry with a test number of 0.
930 self.visited_tests
.entry((file
.clone(), line
)).and_modify(|v
| *v
+= 1).or_insert(0)
933 let outdir
= if let Some(mut path
) = options
.persist_doctests
.clone() {
936 std
::fs
::create_dir_all(&path
)
937 .expect("Couldn't create directory for doctest executables");
942 TempFileBuilder
::new()
943 .prefix("rustdoctest")
945 .expect("rustdoc needs a tempdir"),
949 debug
!("creating test {}: {}", name
, test
);
950 self.tests
.push(test
::TestDescAndFn
{
951 desc
: test
::TestDesc
{
952 name
: test
::DynTestName(name
),
953 ignore
: match config
.ignore
{
955 Ignore
::None
=> false,
956 Ignore
::Some(ref ignores
) => ignores
.iter().any(|s
| target_str
.contains(s
)),
958 // compiler failures are test failures
959 should_panic
: test
::ShouldPanic
::No
,
960 allow_fail
: config
.allow_fail
,
961 compile_fail
: config
.compile_fail
,
963 test_type
: test
::TestType
::DocTest
,
965 testfn
: test
::DynTestFn(Box
::new(move || {
966 let report_unused_externs
= |uext
| {
967 unused_externs
.lock().unwrap().push(uext
);
987 report_unused_externs
,
990 if let Err(err
) = res
{
992 TestFailure
::CompileError
=> {
993 eprint
!("Couldn't compile the test.");
995 TestFailure
::UnexpectedCompilePass
=> {
996 eprint
!("Test compiled successfully, but it's marked `compile_fail`.");
998 TestFailure
::UnexpectedRunPass
=> {
999 eprint
!("Test executable succeeded, but it's marked `should_panic`.");
1001 TestFailure
::MissingErrorCodes(codes
) => {
1002 eprint
!("Some expected error codes were not found: {:?}", codes
);
1004 TestFailure
::ExecutionError(err
) => {
1005 eprint
!("Couldn't run the test: {}", err
);
1006 if err
.kind() == io
::ErrorKind
::PermissionDenied
{
1007 eprint
!(" - maybe your tempdir is mounted with noexec?");
1010 TestFailure
::ExecutionFailure(out
) => {
1011 let reason
= if let Some(code
) = out
.status
.code() {
1012 format
!("exit code {}", code
)
1014 String
::from("terminated by signal")
1017 eprintln
!("Test executable failed ({}).", reason
);
1019 // FIXME(#12309): An unfortunate side-effect of capturing the test
1020 // executable's output is that the relative ordering between the test's
1021 // stdout and stderr is lost. However, this is better than the
1022 // alternative: if the test executable inherited the parent's I/O
1023 // handles the output wouldn't be captured at all, even on success.
1025 // The ordering could be preserved if the test process' stderr was
1026 // redirected to stdout, but that functionality does not exist in the
1027 // standard library, so it may not be portable enough.
1028 let stdout
= str::from_utf8(&out
.stdout
).unwrap_or_default();
1029 let stderr
= str::from_utf8(&out
.stderr
).unwrap_or_default();
1031 if !stdout
.is_empty() || !stderr
.is_empty() {
1034 if !stdout
.is_empty() {
1035 eprintln
!("stdout:\n{}", stdout
);
1038 if !stderr
.is_empty() {
1039 eprintln
!("stderr:\n{}", stderr
);
1045 panic
::resume_unwind(Box
::new(()));
1051 fn get_line(&self) -> usize {
1052 if let Some(ref source_map
) = self.source_map
{
1053 let line
= self.position
.lo().to_usize();
1054 let line
= source_map
.lookup_char_pos(BytePos(line
as u32)).line
;
1055 if line
> 0 { line - 1 }
else { line }
1061 fn register_header(&mut self, name
: &str, level
: u32) {
1062 if self.use_headers
{
1063 // We use these headings as test names, so it's good if
1064 // they're valid identifiers.
1069 if (i
== 0 && rustc_lexer
::is_id_start(c
))
1070 || (i
!= 0 && rustc_lexer
::is_id_continue(c
))
1077 .collect
::<String
>();
1079 // Here we try to efficiently assemble the header titles into the
1080 // test name in the form of `h1::h2::h3::h4::h5::h6`.
1082 // Suppose that originally `self.names` contains `[h1, h2, h3]`...
1083 let level
= level
as usize;
1084 if level
<= self.names
.len() {
1085 // ... Consider `level == 2`. All headers in the lower levels
1086 // are irrelevant in this new level. So we should reset
1087 // `self.names` to contain headers until <h2>, and replace that
1088 // slot with the new name: `[h1, name]`.
1089 self.names
.truncate(level
);
1090 self.names
[level
- 1] = name
;
1092 // ... On the other hand, consider `level == 5`. This means we
1093 // need to extend `self.names` to contain five headers. We fill
1094 // in the missing level (<h4>) with `_`. Thus `self.names` will
1095 // become `[h1, h2, h3, "_", name]`.
1096 if level
- 1 > self.names
.len() {
1097 self.names
.resize(level
- 1, "_".to_owned());
1099 self.names
.push(name
);
1105 struct HirCollector
<'a
, 'hir
, 'tcx
> {
1107 collector
: &'a
mut Collector
,
1113 impl<'a
, 'hir
, 'tcx
> HirCollector
<'a
, 'hir
, 'tcx
> {
1114 fn visit_testable
<F
: FnOnce(&mut Self)>(
1121 let ast_attrs
= self.tcx
.hir().attrs(hir_id
);
1122 let mut attrs
= Attributes
::from_ast(ast_attrs
, None
);
1124 if let Some(ref cfg
) = ast_attrs
.cfg(self.sess
) {
1125 if !cfg
.matches(&self.sess
.parse_sess
, Some(&self.sess
.features_untracked())) {
1130 let has_name
= !name
.is_empty();
1132 self.collector
.names
.push(name
);
1135 attrs
.unindent_doc_comments();
1136 // The collapse-docs pass won't combine sugared/raw doc attributes, or included files with
1137 // anything else, this will combine them for us.
1138 if let Some(doc
) = attrs
.collapsed_doc_value() {
1139 // Use the outermost invocation, so that doctest names come from where the docs were written.
1140 let span
= ast_attrs
1142 .map(|span
| span
.ctxt().outer_expn().expansion_cause().unwrap_or(span
))
1143 .unwrap_or(DUMMY_SP
);
1144 self.collector
.set_position(span
);
1145 markdown
::find_testable_code(
1149 self.collector
.enable_per_target_ignores
,
1150 Some(&crate::html
::markdown
::ExtraInfo
::new(
1153 span_of_attrs(&attrs
).unwrap_or(sp
),
1161 self.collector
.names
.pop();
1166 impl<'a
, 'hir
, 'tcx
> intravisit
::Visitor
<'hir
> for HirCollector
<'a
, 'hir
, 'tcx
> {
1167 type Map
= Map
<'hir
>;
1169 fn nested_visit_map(&mut self) -> intravisit
::NestedVisitorMap
<Self::Map
> {
1170 intravisit
::NestedVisitorMap
::All(self.map
)
1173 fn visit_item(&mut self, item
: &'hir hir
::Item
<'_
>) {
1174 let name
= match &item
.kind
{
1175 hir
::ItemKind
::Macro(ref macro_def
) => {
1176 // FIXME(#88038): Non exported macros have historically not been tested,
1177 // but we really ought to start testing them.
1178 let def_id
= item
.def_id
.to_def_id();
1179 if macro_def
.macro_rules
&& !self.tcx
.has_attr(def_id
, sym
::macro_export
) {
1180 intravisit
::walk_item(self, item
);
1183 item
.ident
.to_string()
1185 hir
::ItemKind
::Impl(impl_
) => {
1186 rustc_hir_pretty
::id_to_string(&self.map
, impl_
.self_ty
.hir_id
)
1188 _
=> item
.ident
.to_string(),
1191 self.visit_testable(name
, item
.hir_id(), item
.span
, |this
| {
1192 intravisit
::walk_item(this
, item
);
1196 fn visit_trait_item(&mut self, item
: &'hir hir
::TraitItem
<'_
>) {
1197 self.visit_testable(item
.ident
.to_string(), item
.hir_id(), item
.span
, |this
| {
1198 intravisit
::walk_trait_item(this
, item
);
1202 fn visit_impl_item(&mut self, item
: &'hir hir
::ImplItem
<'_
>) {
1203 self.visit_testable(item
.ident
.to_string(), item
.hir_id(), item
.span
, |this
| {
1204 intravisit
::walk_impl_item(this
, item
);
1208 fn visit_foreign_item(&mut self, item
: &'hir hir
::ForeignItem
<'_
>) {
1209 self.visit_testable(item
.ident
.to_string(), item
.hir_id(), item
.span
, |this
| {
1210 intravisit
::walk_foreign_item(this
, item
);
1216 v
: &'hir hir
::Variant
<'_
>,
1217 g
: &'hir hir
::Generics
<'_
>,
1218 item_id
: hir
::HirId
,
1220 self.visit_testable(v
.ident
.to_string(), v
.id
, v
.span
, |this
| {
1221 intravisit
::walk_variant(this
, v
, g
, item_id
);
1225 fn visit_field_def(&mut self, f
: &'hir hir
::FieldDef
<'_
>) {
1226 self.visit_testable(f
.ident
.to_string(), f
.hir_id
, f
.span
, |this
| {
1227 intravisit
::walk_field_def(this
, f
);