]> git.lizzy.rs Git - rust.git/blob - src/librustdoc/test.rs
report the total number of errors on compilation failure
[rust.git] / src / librustdoc / test.rs
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.
4 //
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.
10
11 use std::collections::HashMap;
12 use std::env;
13 use std::ffi::OsString;
14 use std::io::prelude::*;
15 use std::io;
16 use std::path::{Path, PathBuf};
17 use std::panic::{self, AssertUnwindSafe};
18 use std::process::Command;
19 use std::rc::Rc;
20 use std::str;
21 use std::sync::{Arc, Mutex};
22
23 use testing;
24 use rustc_lint;
25 use rustc::dep_graph::DepGraph;
26 use rustc::hir;
27 use rustc::hir::intravisit;
28 use rustc::session::{self, CompileIncomplete, config};
29 use rustc::session::config::{OutputType, OutputTypes, Externs};
30 use rustc::session::search_paths::{SearchPaths, PathKind};
31 use rustc_back::dynamic_lib::DynamicLibrary;
32 use rustc_back::tempdir::TempDir;
33 use rustc_driver::{self, driver, Compilation};
34 use rustc_driver::driver::phase_2_configure_and_expand;
35 use rustc_metadata::cstore::CStore;
36 use rustc_resolve::MakeGlobMap;
37 use rustc_trans;
38 use rustc_trans::back::link;
39 use syntax::ast;
40 use syntax::codemap::CodeMap;
41 use syntax::feature_gate::UnstableFeatures;
42 use syntax_pos::{BytePos, DUMMY_SP, Pos, Span};
43 use errors;
44 use errors::emitter::ColorConfig;
45
46 use clean::Attributes;
47 use html::markdown::{self, RenderType};
48
49 #[derive(Clone, Default)]
50 pub struct TestOptions {
51     pub no_crate_inject: bool,
52     pub attrs: Vec<String>,
53 }
54
55 pub fn run(input: &str,
56            cfgs: Vec<String>,
57            libs: SearchPaths,
58            externs: Externs,
59            mut test_args: Vec<String>,
60            crate_name: Option<String>,
61            maybe_sysroot: Option<PathBuf>,
62            render_type: RenderType,
63            display_warnings: bool)
64            -> isize {
65     let input_path = PathBuf::from(input);
66     let input = config::Input::File(input_path.clone());
67
68     let sessopts = config::Options {
69         maybe_sysroot: maybe_sysroot.clone().or_else(
70             || Some(env::current_exe().unwrap().parent().unwrap().parent().unwrap().to_path_buf())),
71         search_paths: libs.clone(),
72         crate_types: vec![config::CrateTypeDylib],
73         externs: externs.clone(),
74         unstable_features: UnstableFeatures::from_environment(),
75         actually_rustdoc: true,
76         ..config::basic_options().clone()
77     };
78
79     let codemap = Rc::new(CodeMap::new(sessopts.file_path_mapping()));
80     let handler =
81         errors::Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(codemap.clone()));
82
83     let dep_graph = DepGraph::new(false);
84     let _ignore = dep_graph.in_ignore();
85     let cstore = Rc::new(CStore::new(&dep_graph, box rustc_trans::LlvmMetadataLoader));
86     let mut sess = session::build_session_(
87         sessopts, &dep_graph, Some(input_path.clone()), handler, codemap.clone(), cstore.clone(),
88     );
89     rustc_trans::init(&sess);
90     rustc_lint::register_builtins(&mut sess.lint_store.borrow_mut(), Some(&sess));
91     sess.parse_sess.config =
92         config::build_configuration(&sess, config::parse_cfgspecs(cfgs.clone()));
93
94     let krate = panictry!(driver::phase_1_parse_input(&sess, &input));
95     let driver::ExpansionResult { defs, mut hir_forest, .. } = {
96         phase_2_configure_and_expand(
97             &sess, &cstore, krate, None, "rustdoc-test", None, MakeGlobMap::No, |_| Ok(())
98         ).expect("phase_2_configure_and_expand aborted in rustdoc!")
99     };
100
101     let crate_name = crate_name.unwrap_or_else(|| {
102         link::find_crate_name(None, &hir_forest.krate().attrs, &input)
103     });
104     let opts = scrape_test_config(hir_forest.krate());
105     let mut collector = Collector::new(crate_name,
106                                        cfgs,
107                                        libs,
108                                        externs,
109                                        false,
110                                        opts,
111                                        maybe_sysroot,
112                                        Some(codemap),
113                                        None,
114                                        render_type);
115
116     {
117         let dep_graph = DepGraph::new(false);
118         let _ignore = dep_graph.in_ignore();
119         let map = hir::map::map_crate(&mut hir_forest, defs);
120         let krate = map.krate();
121         let mut hir_collector = HirCollector {
122             collector: &mut collector,
123             map: &map
124         };
125         hir_collector.visit_testable("".to_string(), &krate.attrs, |this| {
126             intravisit::walk_crate(this, krate);
127         });
128     }
129
130     test_args.insert(0, "rustdoctest".to_string());
131
132     testing::test_main(&test_args,
133                        collector.tests.into_iter().collect(),
134                        testing::Options::new().display_output(display_warnings));
135     0
136 }
137
138 // Look for #![doc(test(no_crate_inject))], used by crates in the std facade
139 fn scrape_test_config(krate: &::rustc::hir::Crate) -> TestOptions {
140     use syntax::print::pprust;
141
142     let mut opts = TestOptions {
143         no_crate_inject: false,
144         attrs: Vec::new(),
145     };
146
147     let test_attrs: Vec<_> = krate.attrs.iter()
148         .filter(|a| a.check_name("doc"))
149         .flat_map(|a| a.meta_item_list().unwrap_or_else(Vec::new))
150         .filter(|a| a.check_name("test"))
151         .collect();
152     let attrs = test_attrs.iter().flat_map(|a| a.meta_item_list().unwrap_or(&[]));
153
154     for attr in attrs {
155         if attr.check_name("no_crate_inject") {
156             opts.no_crate_inject = true;
157         }
158         if attr.check_name("attr") {
159             if let Some(l) = attr.meta_item_list() {
160                 for item in l {
161                     opts.attrs.push(pprust::meta_list_item_to_string(item));
162                 }
163             }
164         }
165     }
166
167     opts
168 }
169
170 fn runtest(test: &str, cratename: &str, cfgs: Vec<String>, libs: SearchPaths,
171            externs: Externs,
172            should_panic: bool, no_run: bool, as_test_harness: bool,
173            compile_fail: bool, mut error_codes: Vec<String>, opts: &TestOptions,
174            maybe_sysroot: Option<PathBuf>) {
175     // the test harness wants its own `main` & top level functions, so
176     // never wrap the test in `fn main() { ... }`
177     let test = maketest(test, Some(cratename), as_test_harness, opts);
178     let input = config::Input::Str {
179         name: driver::anon_src(),
180         input: test.to_owned(),
181     };
182     let outputs = OutputTypes::new(&[(OutputType::Exe, None)]);
183
184     let sessopts = config::Options {
185         maybe_sysroot: maybe_sysroot.or_else(
186             || Some(env::current_exe().unwrap().parent().unwrap().parent().unwrap().to_path_buf())),
187         search_paths: libs,
188         crate_types: vec![config::CrateTypeExecutable],
189         output_types: outputs,
190         externs: externs,
191         cg: config::CodegenOptions {
192             prefer_dynamic: true,
193             .. config::basic_codegen_options()
194         },
195         test: as_test_harness,
196         unstable_features: UnstableFeatures::from_environment(),
197         ..config::basic_options().clone()
198     };
199
200     // Shuffle around a few input and output handles here. We're going to pass
201     // an explicit handle into rustc to collect output messages, but we also
202     // want to catch the error message that rustc prints when it fails.
203     //
204     // We take our thread-local stderr (likely set by the test runner) and replace
205     // it with a sink that is also passed to rustc itself. When this function
206     // returns the output of the sink is copied onto the output of our own thread.
207     //
208     // The basic idea is to not use a default Handler for rustc, and then also
209     // not print things by default to the actual stderr.
210     struct Sink(Arc<Mutex<Vec<u8>>>);
211     impl Write for Sink {
212         fn write(&mut self, data: &[u8]) -> io::Result<usize> {
213             Write::write(&mut *self.0.lock().unwrap(), data)
214         }
215         fn flush(&mut self) -> io::Result<()> { Ok(()) }
216     }
217     struct Bomb(Arc<Mutex<Vec<u8>>>, Box<Write+Send>);
218     impl Drop for Bomb {
219         fn drop(&mut self) {
220             let _ = self.1.write_all(&self.0.lock().unwrap());
221         }
222     }
223     let data = Arc::new(Mutex::new(Vec::new()));
224     let codemap = Rc::new(CodeMap::new(sessopts.file_path_mapping()));
225     let emitter = errors::emitter::EmitterWriter::new(box Sink(data.clone()),
226                                                       Some(codemap.clone()));
227     let old = io::set_panic(Some(box Sink(data.clone())));
228     let _bomb = Bomb(data.clone(), old.unwrap_or(box io::stdout()));
229
230     // Compile the code
231     let diagnostic_handler = errors::Handler::with_emitter(true, false, box emitter);
232
233     let dep_graph = DepGraph::new(false);
234     let cstore = Rc::new(CStore::new(&dep_graph, box rustc_trans::LlvmMetadataLoader));
235     let mut sess = session::build_session_(
236         sessopts, &dep_graph, None, diagnostic_handler, codemap, cstore.clone(),
237     );
238     rustc_trans::init(&sess);
239     rustc_lint::register_builtins(&mut sess.lint_store.borrow_mut(), Some(&sess));
240
241     let outdir = Mutex::new(TempDir::new("rustdoctest").ok().expect("rustdoc needs a tempdir"));
242     let libdir = sess.target_filesearch(PathKind::All).get_lib_path();
243     let mut control = driver::CompileController::basic();
244     sess.parse_sess.config =
245         config::build_configuration(&sess, config::parse_cfgspecs(cfgs.clone()));
246     let out = Some(outdir.lock().unwrap().path().to_path_buf());
247
248     if no_run {
249         control.after_analysis.stop = Compilation::Stop;
250     }
251
252     let res = panic::catch_unwind(AssertUnwindSafe(|| {
253         driver::compile_input(&sess, &cstore, &input, &out, &None, None, &control)
254     }));
255
256     let compile_result = match res {
257         Ok(Ok(())) | Ok(Err(CompileIncomplete::Stopped)) => Ok(()),
258         Err(_) | Ok(Err(CompileIncomplete::Errored(_))) => Err(())
259     };
260
261     match (compile_result, compile_fail) {
262         (Ok(()), true) => {
263             panic!("test compiled while it wasn't supposed to")
264         }
265         (Ok(()), false) => {}
266         (Err(()), true) => {
267             if error_codes.len() > 0 {
268                 let out = String::from_utf8(data.lock().unwrap().to_vec()).unwrap();
269                 error_codes.retain(|err| !out.contains(err));
270             }
271         }
272         (Err(()), false) => {
273             panic!("couldn't compile the test")
274         }
275     }
276
277     if error_codes.len() > 0 {
278         panic!("Some expected error codes were not found: {:?}", error_codes);
279     }
280
281     if no_run { return }
282
283     // Run the code!
284     //
285     // We're careful to prepend the *target* dylib search path to the child's
286     // environment to ensure that the target loads the right libraries at
287     // runtime. It would be a sad day if the *host* libraries were loaded as a
288     // mistake.
289     let mut cmd = Command::new(&outdir.lock().unwrap().path().join("rust_out"));
290     let var = DynamicLibrary::envvar();
291     let newpath = {
292         let path = env::var_os(var).unwrap_or(OsString::new());
293         let mut path = env::split_paths(&path).collect::<Vec<_>>();
294         path.insert(0, libdir.clone());
295         env::join_paths(path).unwrap()
296     };
297     cmd.env(var, &newpath);
298
299     match cmd.output() {
300         Err(e) => panic!("couldn't run the test: {}{}", e,
301                         if e.kind() == io::ErrorKind::PermissionDenied {
302                             " - maybe your tempdir is mounted with noexec?"
303                         } else { "" }),
304         Ok(out) => {
305             if should_panic && out.status.success() {
306                 panic!("test executable succeeded when it should have failed");
307             } else if !should_panic && !out.status.success() {
308                 panic!("test executable failed:\n{}\n{}\n",
309                        str::from_utf8(&out.stdout).unwrap_or(""),
310                        str::from_utf8(&out.stderr).unwrap_or(""));
311             }
312         }
313     }
314 }
315
316 pub fn maketest(s: &str, cratename: Option<&str>, dont_insert_main: bool,
317                 opts: &TestOptions) -> String {
318     let (crate_attrs, everything_else) = partition_source(s);
319
320     let mut prog = String::new();
321
322     // First push any outer attributes from the example, assuming they
323     // are intended to be crate attributes.
324     prog.push_str(&crate_attrs);
325
326     // Next, any attributes for other aspects such as lints.
327     for attr in &opts.attrs {
328         prog.push_str(&format!("#![{}]\n", attr));
329     }
330
331     // Don't inject `extern crate std` because it's already injected by the
332     // compiler.
333     if !s.contains("extern crate") && !opts.no_crate_inject && cratename != Some("std") {
334         if let Some(cratename) = cratename {
335             if s.contains(cratename) {
336                 prog.push_str(&format!("extern crate {};\n", cratename));
337             }
338         }
339     }
340     if dont_insert_main || s.contains("fn main") {
341         prog.push_str(&everything_else);
342     } else {
343         prog.push_str("fn main() {\n");
344         prog.push_str(&everything_else);
345         prog = prog.trim().into();
346         prog.push_str("\n}");
347     }
348
349     info!("final test program: {}", prog);
350
351     prog
352 }
353
354 // FIXME(aburka): use a real parser to deal with multiline attributes
355 fn partition_source(s: &str) -> (String, String) {
356     use std_unicode::str::UnicodeStr;
357
358     let mut after_header = false;
359     let mut before = String::new();
360     let mut after = String::new();
361
362     for line in s.lines() {
363         let trimline = line.trim();
364         let header = trimline.is_whitespace() ||
365             trimline.starts_with("#![");
366         if !header || after_header {
367             after_header = true;
368             after.push_str(line);
369             after.push_str("\n");
370         } else {
371             before.push_str(line);
372             before.push_str("\n");
373         }
374     }
375
376     (before, after)
377 }
378
379 pub struct Collector {
380     pub tests: Vec<testing::TestDescAndFn>,
381     // to be removed when hoedown will be definitely gone
382     pub old_tests: HashMap<String, Vec<String>>,
383     names: Vec<String>,
384     cfgs: Vec<String>,
385     libs: SearchPaths,
386     externs: Externs,
387     cnt: usize,
388     use_headers: bool,
389     current_header: Option<String>,
390     cratename: String,
391     opts: TestOptions,
392     maybe_sysroot: Option<PathBuf>,
393     position: Span,
394     codemap: Option<Rc<CodeMap>>,
395     filename: Option<String>,
396     // to be removed when hoedown will be removed as well
397     pub render_type: RenderType,
398 }
399
400 impl Collector {
401     pub fn new(cratename: String, cfgs: Vec<String>, libs: SearchPaths, externs: Externs,
402                use_headers: bool, opts: TestOptions, maybe_sysroot: Option<PathBuf>,
403                codemap: Option<Rc<CodeMap>>, filename: Option<String>,
404                render_type: RenderType) -> Collector {
405         Collector {
406             tests: Vec::new(),
407             old_tests: HashMap::new(),
408             names: Vec::new(),
409             cfgs: cfgs,
410             libs: libs,
411             externs: externs,
412             cnt: 0,
413             use_headers: use_headers,
414             current_header: None,
415             cratename: cratename,
416             opts: opts,
417             maybe_sysroot: maybe_sysroot,
418             position: DUMMY_SP,
419             codemap: codemap,
420             filename: filename,
421             render_type: render_type,
422         }
423     }
424
425     fn generate_name(&self, line: usize, filename: &str) -> String {
426         if self.use_headers {
427             if let Some(ref header) = self.current_header {
428                 format!("{} - {} (line {})", filename, header, line)
429             } else {
430                 format!("{} - (line {})", filename, line)
431             }
432         } else {
433             format!("{} - {} (line {})", filename, self.names.join("::"), line)
434         }
435     }
436
437     // to be removed once hoedown is gone
438     fn generate_name_beginning(&self, filename: &str) -> String {
439         if self.use_headers {
440             if let Some(ref header) = self.current_header {
441                 format!("{} - {} (line", filename, header)
442             } else {
443                 format!("{} - (line", filename)
444             }
445         } else {
446             format!("{} - {} (line", filename, self.names.join("::"))
447         }
448     }
449
450     pub fn add_old_test(&mut self, test: String, filename: String) {
451         let name_beg = self.generate_name_beginning(&filename);
452         let entry = self.old_tests.entry(name_beg)
453                                   .or_insert(Vec::new());
454         entry.push(test.trim().to_owned());
455     }
456
457     pub fn add_test(&mut self, test: String,
458                     should_panic: bool, no_run: bool, should_ignore: bool,
459                     as_test_harness: bool, compile_fail: bool, error_codes: Vec<String>,
460                     line: usize, filename: String, allow_fail: bool) {
461         let name = self.generate_name(line, &filename);
462         // to be removed when hoedown is removed
463         if self.render_type == RenderType::Pulldown {
464             let name_beg = self.generate_name_beginning(&filename);
465             let mut found = false;
466             let test = test.trim().to_owned();
467             if let Some(entry) = self.old_tests.get_mut(&name_beg) {
468                 found = entry.remove_item(&test).is_some();
469             }
470             if !found {
471                 let _ = writeln!(&mut io::stderr(),
472                                  "WARNING: {} Code block is not currently run as a test, but will \
473                                   in future versions of rustdoc. Please ensure this code block is \
474                                   a runnable test, or use the `ignore` directive.",
475                                  name);
476                 return
477             }
478         }
479         let cfgs = self.cfgs.clone();
480         let libs = self.libs.clone();
481         let externs = self.externs.clone();
482         let cratename = self.cratename.to_string();
483         let opts = self.opts.clone();
484         let maybe_sysroot = self.maybe_sysroot.clone();
485         debug!("Creating test {}: {}", name, test);
486         self.tests.push(testing::TestDescAndFn {
487             desc: testing::TestDesc {
488                 name: testing::DynTestName(name),
489                 ignore: should_ignore,
490                 // compiler failures are test failures
491                 should_panic: testing::ShouldPanic::No,
492                 allow_fail: allow_fail,
493             },
494             testfn: testing::DynTestFn(box move |()| {
495                 let panic = io::set_panic(None);
496                 let print = io::set_print(None);
497                 match {
498                     rustc_driver::in_rustc_thread(move || {
499                         io::set_panic(panic);
500                         io::set_print(print);
501                         runtest(&test,
502                                 &cratename,
503                                 cfgs,
504                                 libs,
505                                 externs,
506                                 should_panic,
507                                 no_run,
508                                 as_test_harness,
509                                 compile_fail,
510                                 error_codes,
511                                 &opts,
512                                 maybe_sysroot)
513                     })
514                 } {
515                     Ok(()) => (),
516                     Err(err) => panic::resume_unwind(err),
517                 }
518             }),
519         });
520     }
521
522     pub fn get_line(&self) -> usize {
523         if let Some(ref codemap) = self.codemap {
524             let line = self.position.lo.to_usize();
525             let line = codemap.lookup_char_pos(BytePos(line as u32)).line;
526             if line > 0 { line - 1 } else { line }
527         } else {
528             0
529         }
530     }
531
532     pub fn set_position(&mut self, position: Span) {
533         self.position = position;
534     }
535
536     pub fn get_filename(&self) -> String {
537         if let Some(ref codemap) = self.codemap {
538             let filename = codemap.span_to_filename(self.position);
539             if let Ok(cur_dir) = env::current_dir() {
540                 if let Ok(path) = Path::new(&filename).strip_prefix(&cur_dir) {
541                     if let Some(path) = path.to_str() {
542                         return path.to_owned();
543                     }
544                 }
545             }
546             filename
547         } else if let Some(ref filename) = self.filename {
548             filename.clone()
549         } else {
550             "<input>".to_owned()
551         }
552     }
553
554     pub fn register_header(&mut self, name: &str, level: u32) {
555         if self.use_headers && level == 1 {
556             // we use these headings as test names, so it's good if
557             // they're valid identifiers.
558             let name = name.chars().enumerate().map(|(i, c)| {
559                     if (i == 0 && c.is_xid_start()) ||
560                         (i != 0 && c.is_xid_continue()) {
561                         c
562                     } else {
563                         '_'
564                     }
565                 }).collect::<String>();
566
567             // new header => reset count.
568             self.cnt = 0;
569             self.current_header = Some(name);
570         }
571     }
572 }
573
574 struct HirCollector<'a, 'hir: 'a> {
575     collector: &'a mut Collector,
576     map: &'a hir::map::Map<'hir>
577 }
578
579 impl<'a, 'hir> HirCollector<'a, 'hir> {
580     fn visit_testable<F: FnOnce(&mut Self)>(&mut self,
581                                             name: String,
582                                             attrs: &[ast::Attribute],
583                                             nested: F) {
584         let has_name = !name.is_empty();
585         if has_name {
586             self.collector.names.push(name);
587         }
588
589         let mut attrs = Attributes::from_ast(attrs);
590         attrs.collapse_doc_comments();
591         attrs.unindent_doc_comments();
592         if let Some(doc) = attrs.doc_value() {
593             self.collector.cnt = 0;
594             if self.collector.render_type == RenderType::Pulldown {
595                 markdown::old_find_testable_code(doc, self.collector,
596                                                  attrs.span.unwrap_or(DUMMY_SP));
597                 markdown::find_testable_code(doc, self.collector,
598                                              attrs.span.unwrap_or(DUMMY_SP));
599             } else {
600                 markdown::old_find_testable_code(doc, self.collector,
601                                                  attrs.span.unwrap_or(DUMMY_SP));
602             }
603         }
604
605         nested(self);
606
607         if has_name {
608             self.collector.names.pop();
609         }
610     }
611 }
612
613 impl<'a, 'hir> intravisit::Visitor<'hir> for HirCollector<'a, 'hir> {
614     fn nested_visit_map<'this>(&'this mut self) -> intravisit::NestedVisitorMap<'this, 'hir> {
615         intravisit::NestedVisitorMap::All(&self.map)
616     }
617
618     fn visit_item(&mut self, item: &'hir hir::Item) {
619         let name = if let hir::ItemImpl(.., ref ty, _) = item.node {
620             self.map.node_to_pretty_string(ty.id)
621         } else {
622             item.name.to_string()
623         };
624
625         self.visit_testable(name, &item.attrs, |this| {
626             intravisit::walk_item(this, item);
627         });
628     }
629
630     fn visit_trait_item(&mut self, item: &'hir hir::TraitItem) {
631         self.visit_testable(item.name.to_string(), &item.attrs, |this| {
632             intravisit::walk_trait_item(this, item);
633         });
634     }
635
636     fn visit_impl_item(&mut self, item: &'hir hir::ImplItem) {
637         self.visit_testable(item.name.to_string(), &item.attrs, |this| {
638             intravisit::walk_impl_item(this, item);
639         });
640     }
641
642     fn visit_foreign_item(&mut self, item: &'hir hir::ForeignItem) {
643         self.visit_testable(item.name.to_string(), &item.attrs, |this| {
644             intravisit::walk_foreign_item(this, item);
645         });
646     }
647
648     fn visit_variant(&mut self,
649                      v: &'hir hir::Variant,
650                      g: &'hir hir::Generics,
651                      item_id: ast::NodeId) {
652         self.visit_testable(v.node.name.to_string(), &v.node.attrs, |this| {
653             intravisit::walk_variant(this, v, g, item_id);
654         });
655     }
656
657     fn visit_struct_field(&mut self, f: &'hir hir::StructField) {
658         self.visit_testable(f.name.to_string(), &f.attrs, |this| {
659             intravisit::walk_struct_field(this, f);
660         });
661     }
662
663     fn visit_macro_def(&mut self, macro_def: &'hir hir::MacroDef) {
664         self.visit_testable(macro_def.name.to_string(), &macro_def.attrs, |_| ());
665     }
666 }