1 // Copyright 2013-2014 The Rust Project Developers. See the COPYRIGHT
2 // file at the top-level directory of this distribution and at
3 // http://rust-lang.org/COPYRIGHT.
5 // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
6 // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
7 // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
8 // option. This file may not be copied, modified, or distributed
9 // except according to those terms.
11 use std
::collections
::HashMap
;
13 use std
::ffi
::OsString
;
14 use std
::io
::prelude
::*;
16 use std
::path
::{Path, PathBuf}
;
17 use std
::panic
::{self, AssertUnwindSafe}
;
18 use std
::process
::Command
;
21 use std
::sync
::{Arc, Mutex}
;
26 use rustc
::hir
::intravisit
;
27 use rustc
::session
::{self, CompileIncomplete, config}
;
28 use rustc
::session
::config
::{OutputType, OutputTypes, Externs}
;
29 use rustc
::session
::search_paths
::{SearchPaths, PathKind}
;
30 use rustc_metadata
::dynamic_lib
::DynamicLibrary
;
32 use rustc_driver
::{self, driver, Compilation}
;
33 use rustc_driver
::driver
::phase_2_configure_and_expand
;
34 use rustc_metadata
::cstore
::CStore
;
35 use rustc_resolve
::MakeGlobMap
;
37 use syntax
::codemap
::CodeMap
;
38 use syntax
::feature_gate
::UnstableFeatures
;
39 use syntax_pos
::{BytePos, DUMMY_SP, Pos, Span, FileName}
;
41 use errors
::emitter
::ColorConfig
;
43 use clean
::Attributes
;
44 use html
::markdown
::{self, RenderType}
;
46 #[derive(Clone, Default)]
47 pub struct TestOptions
{
48 pub no_crate_inject
: bool
,
49 pub attrs
: Vec
<String
>,
52 pub fn run(input_path
: &Path
,
56 mut test_args
: Vec
<String
>,
57 crate_name
: Option
<String
>,
58 maybe_sysroot
: Option
<PathBuf
>,
59 render_type
: RenderType
,
60 display_warnings
: bool
,
61 linker
: Option
<PathBuf
>)
63 let input
= config
::Input
::File(input_path
.to_owned());
65 let sessopts
= config
::Options
{
66 maybe_sysroot
: maybe_sysroot
.clone().or_else(
67 || Some(env
::current_exe().unwrap().parent().unwrap().parent().unwrap().to_path_buf())),
68 search_paths
: libs
.clone(),
69 crate_types
: vec
![config
::CrateTypeDylib
],
70 externs
: externs
.clone(),
71 unstable_features
: UnstableFeatures
::from_environment(),
72 lint_cap
: Some(::rustc
::lint
::Level
::Allow
),
73 actually_rustdoc
: true,
74 ..config
::basic_options().clone()
77 let codemap
= Rc
::new(CodeMap
::new(sessopts
.file_path_mapping()));
79 errors
::Handler
::with_tty_emitter(ColorConfig
::Auto
,
81 Some(codemap
.clone()));
83 let mut sess
= session
::build_session_(
84 sessopts
, Some(input_path
.to_owned()), handler
, codemap
.clone(),
86 let trans
= rustc_driver
::get_trans(&sess
);
87 let cstore
= Rc
::new(CStore
::new(trans
.metadata_loader()));
88 rustc_lint
::register_builtins(&mut sess
.lint_store
.borrow_mut(), Some(&sess
));
89 sess
.parse_sess
.config
=
90 config
::build_configuration(&sess
, config
::parse_cfgspecs(cfgs
.clone()));
92 let krate
= panictry
!(driver
::phase_1_parse_input(&driver
::CompileController
::basic(),
95 let driver
::ExpansionResult { defs, mut hir_forest, .. }
= {
96 phase_2_configure_and_expand(
105 ).expect("phase_2_configure_and_expand aborted in rustdoc!")
108 let crate_name
= crate_name
.unwrap_or_else(|| {
109 ::rustc_trans_utils
::link
::find_crate_name(None
, &hir_forest
.krate().attrs
, &input
)
111 let opts
= scrape_test_config(hir_forest
.krate());
112 let mut collector
= Collector
::new(crate_name
,
125 let map
= hir
::map
::map_crate(&sess
, &*cstore
, &mut hir_forest
, &defs
);
126 let krate
= map
.krate();
127 let mut hir_collector
= HirCollector
{
129 collector
: &mut collector
,
132 hir_collector
.visit_testable("".to_string(), &krate
.attrs
, |this
| {
133 intravisit
::walk_crate(this
, krate
);
137 test_args
.insert(0, "rustdoctest".to_string());
139 testing
::test_main(&test_args
,
140 collector
.tests
.into_iter().collect(),
141 testing
::Options
::new().display_output(display_warnings
));
145 // Look for #![doc(test(no_crate_inject))], used by crates in the std facade
146 fn scrape_test_config(krate
: &::rustc
::hir
::Crate
) -> TestOptions
{
147 use syntax
::print
::pprust
;
149 let mut opts
= TestOptions
{
150 no_crate_inject
: false,
154 let test_attrs
: Vec
<_
> = krate
.attrs
.iter()
155 .filter(|a
| a
.check_name("doc"))
156 .flat_map(|a
| a
.meta_item_list().unwrap_or_else(Vec
::new
))
157 .filter(|a
| a
.check_name("test"))
159 let attrs
= test_attrs
.iter().flat_map(|a
| a
.meta_item_list().unwrap_or(&[]));
162 if attr
.check_name("no_crate_inject") {
163 opts
.no_crate_inject
= true;
165 if attr
.check_name("attr") {
166 if let Some(l
) = attr
.meta_item_list() {
168 opts
.attrs
.push(pprust
::meta_list_item_to_string(item
));
177 fn run_test(test
: &str, cratename
: &str, filename
: &FileName
, line
: usize,
178 cfgs
: Vec
<String
>, libs
: SearchPaths
,
180 should_panic
: bool
, no_run
: bool
, as_test_harness
: bool
,
181 compile_fail
: bool
, mut error_codes
: Vec
<String
>, opts
: &TestOptions
,
182 maybe_sysroot
: Option
<PathBuf
>,
183 linker
: Option
<PathBuf
>) {
184 // the test harness wants its own `main` & top level functions, so
185 // never wrap the test in `fn main() { ... }`
186 let (test
, line_offset
) = make_test(test
, Some(cratename
), as_test_harness
, opts
);
187 // FIXME(#44940): if doctests ever support path remapping, then this filename
188 // needs to be the result of CodeMap::span_to_unmapped_path
189 let input
= config
::Input
::Str
{
190 name
: filename
.to_owned(),
191 input
: test
.to_owned(),
193 let outputs
= OutputTypes
::new(&[(OutputType
::Exe
, None
)]);
195 let sessopts
= config
::Options
{
196 maybe_sysroot
: maybe_sysroot
.or_else(
197 || Some(env
::current_exe().unwrap().parent().unwrap().parent().unwrap().to_path_buf())),
199 crate_types
: vec
![config
::CrateTypeExecutable
],
200 output_types
: outputs
,
202 cg
: config
::CodegenOptions
{
203 prefer_dynamic
: true,
205 .. config
::basic_codegen_options()
207 test
: as_test_harness
,
208 unstable_features
: UnstableFeatures
::from_environment(),
209 ..config
::basic_options().clone()
212 // Shuffle around a few input and output handles here. We're going to pass
213 // an explicit handle into rustc to collect output messages, but we also
214 // want to catch the error message that rustc prints when it fails.
216 // We take our thread-local stderr (likely set by the test runner) and replace
217 // it with a sink that is also passed to rustc itself. When this function
218 // returns the output of the sink is copied onto the output of our own thread.
220 // The basic idea is to not use a default Handler for rustc, and then also
221 // not print things by default to the actual stderr.
222 struct Sink(Arc
<Mutex
<Vec
<u8>>>);
223 impl Write
for Sink
{
224 fn write(&mut self, data
: &[u8]) -> io
::Result
<usize> {
225 Write
::write(&mut *self.0.lock().unwrap(), data
)
227 fn flush(&mut self) -> io
::Result
<()> { Ok(()) }
229 struct Bomb(Arc
<Mutex
<Vec
<u8>>>, Box
<Write
+Send
>);
232 let _
= self.1.write_all(&self.0.lock().unwrap());
235 let data
= Arc
::new(Mutex
::new(Vec
::new()));
236 let codemap
= Rc
::new(CodeMap
::new_doctest(
237 sessopts
.file_path_mapping(), filename
.clone(), line
as isize - line_offset
as isize
239 let emitter
= errors
::emitter
::EmitterWriter
::new(box Sink(data
.clone()),
240 Some(codemap
.clone()),
243 let old
= io
::set_panic(Some(box Sink(data
.clone())));
244 let _bomb
= Bomb(data
.clone(), old
.unwrap_or(box io
::stdout()));
247 let diagnostic_handler
= errors
::Handler
::with_emitter(true, false, box emitter
);
249 let mut sess
= session
::build_session_(
250 sessopts
, None
, diagnostic_handler
, codemap
,
252 let trans
= rustc_driver
::get_trans(&sess
);
253 let cstore
= Rc
::new(CStore
::new(trans
.metadata_loader()));
254 rustc_lint
::register_builtins(&mut sess
.lint_store
.borrow_mut(), Some(&sess
));
256 let outdir
= Mutex
::new(TempDir
::new("rustdoctest").ok().expect("rustdoc needs a tempdir"));
257 let libdir
= sess
.target_filesearch(PathKind
::All
).get_lib_path();
258 let mut control
= driver
::CompileController
::basic();
259 sess
.parse_sess
.config
=
260 config
::build_configuration(&sess
, config
::parse_cfgspecs(cfgs
.clone()));
261 let out
= Some(outdir
.lock().unwrap().path().to_path_buf());
264 control
.after_analysis
.stop
= Compilation
::Stop
;
267 let res
= panic
::catch_unwind(AssertUnwindSafe(|| {
268 driver
::compile_input(
281 let compile_result
= match res
{
282 Ok(Ok(())) | Ok(Err(CompileIncomplete
::Stopped
)) => Ok(()),
283 Err(_
) | Ok(Err(CompileIncomplete
::Errored(_
))) => Err(())
286 match (compile_result
, compile_fail
) {
288 panic
!("test compiled while it wasn't supposed to")
290 (Ok(()), false) => {}
292 if error_codes
.len() > 0 {
293 let out
= String
::from_utf8(data
.lock().unwrap().to_vec()).unwrap();
294 error_codes
.retain(|err
| !out
.contains(err
));
297 (Err(()), false) => {
298 panic
!("couldn't compile the test")
302 if error_codes
.len() > 0 {
303 panic
!("Some expected error codes were not found: {:?}", error_codes
);
310 // We're careful to prepend the *target* dylib search path to the child's
311 // environment to ensure that the target loads the right libraries at
312 // runtime. It would be a sad day if the *host* libraries were loaded as a
314 let mut cmd
= Command
::new(&outdir
.lock().unwrap().path().join("rust_out"));
315 let var
= DynamicLibrary
::envvar();
317 let path
= env
::var_os(var
).unwrap_or(OsString
::new());
318 let mut path
= env
::split_paths(&path
).collect
::<Vec
<_
>>();
319 path
.insert(0, libdir
.clone());
320 env
::join_paths(path
).unwrap()
322 cmd
.env(var
, &newpath
);
325 Err(e
) => panic
!("couldn't run the test: {}{}", e
,
326 if e
.kind() == io
::ErrorKind
::PermissionDenied
{
327 " - maybe your tempdir is mounted with noexec?"
330 if should_panic
&& out
.status
.success() {
331 panic
!("test executable succeeded when it should have failed");
332 } else if !should_panic
&& !out
.status
.success() {
333 panic
!("test executable failed:\n{}\n{}\n",
334 str::from_utf8(&out
.stdout
).unwrap_or(""),
335 str::from_utf8(&out
.stderr
).unwrap_or(""));
341 /// Makes the test file. Also returns the number of lines before the code begins
342 pub fn make_test(s
: &str,
343 cratename
: Option
<&str>,
344 dont_insert_main
: bool
,
347 let (crate_attrs
, everything_else
) = partition_source(s
);
348 let mut line_offset
= 0;
349 let mut prog
= String
::new();
351 if opts
.attrs
.is_empty() {
352 // If there aren't any attributes supplied by #![doc(test(attr(...)))], then allow some
353 // lints that are commonly triggered in doctests. The crate-level test attributes are
354 // commonly used to make tests fail in case they trigger warnings, so having this there in
355 // that case may cause some tests to pass when they shouldn't have.
356 prog
.push_str("#![allow(unused)]\n");
360 // Next, any attributes that came from the crate root via #![doc(test(attr(...)))].
361 for attr
in &opts
.attrs
{
362 prog
.push_str(&format
!("#![{}]\n", attr
));
366 // Now push any outer attributes from the example, assuming they
367 // are intended to be crate attributes.
368 prog
.push_str(&crate_attrs
);
370 // Don't inject `extern crate std` because it's already injected by the
372 if !s
.contains("extern crate") && !opts
.no_crate_inject
&& cratename
!= Some("std") {
373 if let Some(cratename
) = cratename
{
374 if s
.contains(cratename
) {
375 prog
.push_str(&format
!("extern crate {};\n", cratename
));
381 // FIXME (#21299): prefer libsyntax or some other actual parser over this
382 // best-effort ad hoc approach
383 let already_has_main
= s
.lines()
385 let comment
= line
.find("//");
386 if let Some(comment_begins
) = comment
{
387 &line
[0..comment_begins
]
392 .any(|code
| code
.contains("fn main"));
394 if dont_insert_main
|| already_has_main
{
395 prog
.push_str(&everything_else
);
397 prog
.push_str("fn main() {\n");
399 prog
.push_str(&everything_else
);
400 prog
= prog
.trim().into();
401 prog
.push_str("\n}");
404 info
!("final test program: {}", prog
);
409 // FIXME(aburka): use a real parser to deal with multiline attributes
410 fn partition_source(s
: &str) -> (String
, String
) {
411 use std_unicode
::str::UnicodeStr
;
413 let mut after_header
= false;
414 let mut before
= String
::new();
415 let mut after
= String
::new();
417 for line
in s
.lines() {
418 let trimline
= line
.trim();
419 let header
= trimline
.is_whitespace() ||
420 trimline
.starts_with("#![");
421 if !header
|| after_header
{
423 after
.push_str(line
);
424 after
.push_str("\n");
426 before
.push_str(line
);
427 before
.push_str("\n");
434 pub struct Collector
{
435 pub tests
: Vec
<testing
::TestDescAndFn
>,
436 // to be removed when hoedown will be definitely gone
437 pub old_tests
: HashMap
<FileName
, Vec
<String
>>,
439 // The name of the test displayed to the user, separated by `::`.
441 // In tests from Rust source, this is the path to the item
442 // e.g. `["std", "vec", "Vec", "push"]`.
444 // In tests from a markdown file, this is the titles of all headers (h1~h6)
445 // of the sections that contain the code block, e.g. if the markdown file is
458 // the `names` vector of that test will be `["Title", "Subtitle"]`.
467 maybe_sysroot
: Option
<PathBuf
>,
469 codemap
: Option
<Rc
<CodeMap
>>,
470 filename
: Option
<PathBuf
>,
471 // to be removed when hoedown will be removed as well
472 pub render_type
: RenderType
,
473 linker
: Option
<PathBuf
>,
477 pub fn new(cratename
: String
, cfgs
: Vec
<String
>, libs
: SearchPaths
, externs
: Externs
,
478 use_headers
: bool
, opts
: TestOptions
, maybe_sysroot
: Option
<PathBuf
>,
479 codemap
: Option
<Rc
<CodeMap
>>, filename
: Option
<PathBuf
>,
480 render_type
: RenderType
, linker
: Option
<PathBuf
>) -> Collector
{
483 old_tests
: HashMap
::new(),
500 fn generate_name(&self, line
: usize, filename
: &FileName
) -> String
{
501 format
!("{} - {} (line {})", filename
, self.names
.join("::"), line
)
504 pub fn add_old_test(&mut self, test
: String
, filename
: FileName
) {
505 let entry
= self.old_tests
.entry(filename
.clone())
506 .or_insert(Vec
::new());
507 entry
.push(test
.trim().to_owned());
510 pub fn add_test(&mut self, test
: String
,
511 should_panic
: bool
, no_run
: bool
, should_ignore
: bool
,
512 as_test_harness
: bool
, compile_fail
: bool
, error_codes
: Vec
<String
>,
513 line
: usize, filename
: FileName
, allow_fail
: bool
) {
514 let name
= self.generate_name(line
, &filename
);
515 // to be removed when hoedown is removed
516 if self.render_type
== RenderType
::Pulldown
{
517 let mut found
= false;
518 let test
= test
.trim().to_owned();
519 if let Some(entry
) = self.old_tests
.get_mut(&filename
) {
520 found
= entry
.remove_item(&test
).is_some();
523 eprintln
!("WARNING: {} Code block is not currently run as a test, but will \
524 in future versions of rustdoc. Please ensure this code block is \
525 a runnable test, or use the `ignore` directive.",
530 let cfgs
= self.cfgs
.clone();
531 let libs
= self.libs
.clone();
532 let externs
= self.externs
.clone();
533 let cratename
= self.cratename
.to_string();
534 let opts
= self.opts
.clone();
535 let maybe_sysroot
= self.maybe_sysroot
.clone();
536 let linker
= self.linker
.clone();
537 debug
!("Creating test {}: {}", name
, test
);
538 self.tests
.push(testing
::TestDescAndFn
{
539 desc
: testing
::TestDesc
{
540 name
: testing
::DynTestName(name
),
541 ignore
: should_ignore
,
542 // compiler failures are test failures
543 should_panic
: testing
::ShouldPanic
::No
,
546 testfn
: testing
::DynTestFn(box move || {
547 let panic
= io
::set_panic(None
);
548 let print
= io
::set_print(None
);
550 rustc_driver
::in_rustc_thread(move || {
551 io
::set_panic(panic
);
552 io
::set_print(print
);
571 Err(err
) => panic
::resume_unwind(err
),
577 pub fn get_line(&self) -> usize {
578 if let Some(ref codemap
) = self.codemap
{
579 let line
= self.position
.lo().to_usize();
580 let line
= codemap
.lookup_char_pos(BytePos(line
as u32)).line
;
581 if line
> 0 { line - 1 }
else { line }
587 pub fn set_position(&mut self, position
: Span
) {
588 self.position
= position
;
591 pub fn get_filename(&self) -> FileName
{
592 if let Some(ref codemap
) = self.codemap
{
593 let filename
= codemap
.span_to_filename(self.position
);
594 if let FileName
::Real(ref filename
) = filename
{
595 if let Ok(cur_dir
) = env
::current_dir() {
596 if let Ok(path
) = filename
.strip_prefix(&cur_dir
) {
597 return path
.to_owned().into();
602 } else if let Some(ref filename
) = self.filename
{
603 filename
.clone().into()
605 FileName
::Custom("input".to_owned())
609 pub fn register_header(&mut self, name
: &str, level
: u32) {
610 if self.use_headers
{
611 // we use these headings as test names, so it's good if
612 // they're valid identifiers.
613 let name
= name
.chars().enumerate().map(|(i
, c
)| {
614 if (i
== 0 && c
.is_xid_start()) ||
615 (i
!= 0 && c
.is_xid_continue()) {
620 }).collect
::<String
>();
622 // Here we try to efficiently assemble the header titles into the
623 // test name in the form of `h1::h2::h3::h4::h5::h6`.
625 // Suppose originally `self.names` contains `[h1, h2, h3]`...
626 let level
= level
as usize;
627 if level
<= self.names
.len() {
628 // ... Consider `level == 2`. All headers in the lower levels
629 // are irrelevant in this new level. So we should reset
630 // `self.names` to contain headers until <h2>, and replace that
631 // slot with the new name: `[h1, name]`.
632 self.names
.truncate(level
);
633 self.names
[level
- 1] = name
;
635 // ... On the other hand, consider `level == 5`. This means we
636 // need to extend `self.names` to contain five headers. We fill
637 // in the missing level (<h4>) with `_`. Thus `self.names` will
638 // become `[h1, h2, h3, "_", name]`.
639 if level
- 1 > self.names
.len() {
640 self.names
.resize(level
- 1, "_".to_owned());
642 self.names
.push(name
);
648 struct HirCollector
<'a
, 'hir
: 'a
> {
649 sess
: &'a session
::Session
,
650 collector
: &'a
mut Collector
,
651 map
: &'a hir
::map
::Map
<'hir
>
654 impl<'a
, 'hir
> HirCollector
<'a
, 'hir
> {
655 fn visit_testable
<F
: FnOnce(&mut Self)>(&mut self,
657 attrs
: &[ast
::Attribute
],
659 let mut attrs
= Attributes
::from_ast(self.sess
.diagnostic(), attrs
);
660 if let Some(ref cfg
) = attrs
.cfg
{
661 if !cfg
.matches(&self.sess
.parse_sess
, Some(&self.sess
.features
.borrow())) {
666 let has_name
= !name
.is_empty();
668 self.collector
.names
.push(name
);
671 attrs
.collapse_doc_comments();
672 attrs
.unindent_doc_comments();
673 // the collapse-docs pass won't combine sugared/raw doc attributes, or included files with
674 // anything else, this will combine them for us
675 if let Some(doc
) = attrs
.collapsed_doc_value() {
676 if self.collector
.render_type
== RenderType
::Pulldown
{
677 markdown
::old_find_testable_code(&doc
, self.collector
,
678 attrs
.span
.unwrap_or(DUMMY_SP
));
679 markdown
::find_testable_code(&doc
, self.collector
,
680 attrs
.span
.unwrap_or(DUMMY_SP
));
682 markdown
::old_find_testable_code(&doc
, self.collector
,
683 attrs
.span
.unwrap_or(DUMMY_SP
));
690 self.collector
.names
.pop();
695 impl<'a
, 'hir
> intravisit
::Visitor
<'hir
> for HirCollector
<'a
, 'hir
> {
696 fn nested_visit_map
<'this
>(&'this
mut self) -> intravisit
::NestedVisitorMap
<'this
, 'hir
> {
697 intravisit
::NestedVisitorMap
::All(&self.map
)
700 fn visit_item(&mut self, item
: &'hir hir
::Item
) {
701 let name
= if let hir
::ItemImpl(.., ref ty
, _
) = item
.node
{
702 self.map
.node_to_pretty_string(ty
.id
)
704 item
.name
.to_string()
707 self.visit_testable(name
, &item
.attrs
, |this
| {
708 intravisit
::walk_item(this
, item
);
712 fn visit_trait_item(&mut self, item
: &'hir hir
::TraitItem
) {
713 self.visit_testable(item
.name
.to_string(), &item
.attrs
, |this
| {
714 intravisit
::walk_trait_item(this
, item
);
718 fn visit_impl_item(&mut self, item
: &'hir hir
::ImplItem
) {
719 self.visit_testable(item
.name
.to_string(), &item
.attrs
, |this
| {
720 intravisit
::walk_impl_item(this
, item
);
724 fn visit_foreign_item(&mut self, item
: &'hir hir
::ForeignItem
) {
725 self.visit_testable(item
.name
.to_string(), &item
.attrs
, |this
| {
726 intravisit
::walk_foreign_item(this
, item
);
730 fn visit_variant(&mut self,
731 v
: &'hir hir
::Variant
,
732 g
: &'hir hir
::Generics
,
733 item_id
: ast
::NodeId
) {
734 self.visit_testable(v
.node
.name
.to_string(), &v
.node
.attrs
, |this
| {
735 intravisit
::walk_variant(this
, v
, g
, item_id
);
739 fn visit_struct_field(&mut self, f
: &'hir hir
::StructField
) {
740 self.visit_testable(f
.name
.to_string(), &f
.attrs
, |this
| {
741 intravisit
::walk_struct_field(this
, f
);
745 fn visit_macro_def(&mut self, macro_def
: &'hir hir
::MacroDef
) {
746 self.visit_testable(macro_def
.name
.to_string(), ¯o_def
.attrs
, |_
| ());