]> git.lizzy.rs Git - rust.git/blob - src/libsyntax_ext/test_harness.rs
Rename `Stmt.node` to `Stmt.kind`
[rust.git] / src / libsyntax_ext / test_harness.rs
1 // Code that generates a test runner to run all the tests in a crate
2
3 use log::debug;
4 use smallvec::{smallvec, SmallVec};
5 use syntax::ast::{self, Ident};
6 use syntax::attr;
7 use syntax::entry::{self, EntryPointType};
8 use syntax::ext::base::{ExtCtxt, Resolver};
9 use syntax::ext::expand::{AstFragment, ExpansionConfig};
10 use syntax::feature_gate::Features;
11 use syntax::mut_visit::{*, ExpectOne};
12 use syntax::parse::ParseSess;
13 use syntax::ptr::P;
14 use syntax::source_map::respan;
15 use syntax::symbol::{sym, Symbol};
16 use syntax_pos::{Span, DUMMY_SP};
17 use syntax_pos::hygiene::{AstPass, SyntaxContext, Transparency};
18
19 use std::{iter, mem};
20
21 struct Test {
22     span: Span,
23     ident: Ident,
24 }
25
26 struct TestCtxt<'a> {
27     ext_cx: ExtCtxt<'a>,
28     def_site: Span,
29     test_cases: Vec<Test>,
30     reexport_test_harness_main: Option<Symbol>,
31     test_runner: Option<ast::Path>,
32 }
33
34 // Traverse the crate, collecting all the test functions, eliding any
35 // existing main functions, and synthesizing a main test harness
36 pub fn inject(
37     sess: &ParseSess,
38     resolver: &mut dyn Resolver,
39     should_test: bool,
40     krate: &mut ast::Crate,
41     span_diagnostic: &errors::Handler,
42     features: &Features,
43 ) {
44     // Check for #![reexport_test_harness_main = "some_name"] which gives the
45     // main test function the name `some_name` without hygiene. This needs to be
46     // unconditional, so that the attribute is still marked as used in
47     // non-test builds.
48     let reexport_test_harness_main =
49         attr::first_attr_value_str_by_name(&krate.attrs, sym::reexport_test_harness_main);
50
51     // Do this here so that the test_runner crate attribute gets marked as used
52     // even in non-test builds
53     let test_runner = get_test_runner(span_diagnostic, &krate);
54
55     if should_test {
56         generate_test_harness(sess, resolver, reexport_test_harness_main,
57                               krate, features, test_runner)
58     }
59 }
60
61 struct TestHarnessGenerator<'a> {
62     cx: TestCtxt<'a>,
63     tests: Vec<Test>,
64 }
65
66 impl<'a> MutVisitor for TestHarnessGenerator<'a> {
67     fn visit_crate(&mut self, c: &mut ast::Crate) {
68         noop_visit_crate(c, self);
69
70         // Create a main function to run our tests
71         c.module.items.push(mk_main(&mut self.cx));
72     }
73
74     fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> {
75         let mut item = i.into_inner();
76         if is_test_case(&item) {
77             debug!("this is a test item");
78
79             let test = Test {
80                 span: item.span,
81                 ident: item.ident,
82             };
83             self.tests.push(test);
84         }
85
86         // We don't want to recurse into anything other than mods, since
87         // mods or tests inside of functions will break things
88         if let ast::ItemKind::Mod(mut module) = item.node {
89             let tests = mem::take(&mut self.tests);
90             noop_visit_mod(&mut module, self);
91             let mut tests = mem::replace(&mut self.tests, tests);
92
93             if !tests.is_empty() {
94                 let parent = if item.id == ast::DUMMY_NODE_ID {
95                     ast::CRATE_NODE_ID
96                 } else {
97                     item.id
98                 };
99                 // Create an identifier that will hygienically resolve the test
100                 // case name, even in another module.
101                 let expn_id = self.cx.ext_cx.resolver.expansion_for_ast_pass(
102                     module.inner,
103                     AstPass::TestHarness,
104                     &[],
105                     Some(parent),
106                 );
107                 for test in &mut tests {
108                     // See the comment on `mk_main` for why we're using
109                     // `apply_mark` directly.
110                     test.ident.span = test.ident.span.apply_mark(expn_id, Transparency::Opaque);
111                 }
112                 self.cx.test_cases.extend(tests);
113             }
114             item.node = ast::ItemKind::Mod(module);
115         }
116         smallvec![P(item)]
117     }
118
119     fn visit_mac(&mut self, _mac: &mut ast::Mac) {
120         // Do nothing.
121     }
122 }
123
124 /// A folder used to remove any entry points (like fn main) because the harness
125 /// generator will provide its own
126 struct EntryPointCleaner {
127     // Current depth in the ast
128     depth: usize,
129     def_site: Span,
130 }
131
132 impl MutVisitor for EntryPointCleaner {
133     fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> {
134         self.depth += 1;
135         let item = noop_flat_map_item(i, self).expect_one("noop did something");
136         self.depth -= 1;
137
138         // Remove any #[main] or #[start] from the AST so it doesn't
139         // clash with the one we're going to add, but mark it as
140         // #[allow(dead_code)] to avoid printing warnings.
141         let item = match entry::entry_point_type(&item, self.depth) {
142             EntryPointType::MainNamed |
143             EntryPointType::MainAttr |
144             EntryPointType::Start =>
145                 item.map(|ast::Item {id, ident, attrs, node, vis, span, tokens}| {
146                     let allow_ident = Ident::new(sym::allow, self.def_site);
147                     let dc_nested = attr::mk_nested_word_item(
148                         Ident::from_str_and_span("dead_code", self.def_site),
149                     );
150                     let allow_dead_code_item = attr::mk_list_item(allow_ident, vec![dc_nested]);
151                     let allow_dead_code = attr::mk_attr_outer(allow_dead_code_item);
152
153                     ast::Item {
154                         id,
155                         ident,
156                         attrs: attrs.into_iter()
157                             .filter(|attr| {
158                                 !attr.check_name(sym::main) && !attr.check_name(sym::start)
159                             })
160                             .chain(iter::once(allow_dead_code))
161                             .collect(),
162                         node,
163                         vis,
164                         span,
165                         tokens,
166                     }
167                 }),
168             EntryPointType::None |
169             EntryPointType::OtherMain => item,
170         };
171
172         smallvec![item]
173     }
174
175     fn visit_mac(&mut self, _mac: &mut ast::Mac) {
176         // Do nothing.
177     }
178 }
179
180 /// Crawl over the crate, inserting test reexports and the test main function
181 fn generate_test_harness(sess: &ParseSess,
182                          resolver: &mut dyn Resolver,
183                          reexport_test_harness_main: Option<Symbol>,
184                          krate: &mut ast::Crate,
185                          features: &Features,
186                          test_runner: Option<ast::Path>) {
187     let mut econfig = ExpansionConfig::default("test".to_string());
188     econfig.features = Some(features);
189
190     let ext_cx = ExtCtxt::new(sess, econfig, resolver);
191
192     let expn_id = ext_cx.resolver.expansion_for_ast_pass(
193         DUMMY_SP,
194         AstPass::TestHarness,
195         &[sym::main, sym::test, sym::rustc_attrs],
196         None,
197     );
198     let def_site = DUMMY_SP.with_def_site_ctxt(expn_id);
199
200     // Remove the entry points
201     let mut cleaner = EntryPointCleaner { depth: 0, def_site };
202     cleaner.visit_crate(krate);
203
204     let cx = TestCtxt {
205         ext_cx,
206         def_site,
207         test_cases: Vec::new(),
208         reexport_test_harness_main,
209         test_runner
210     };
211
212     TestHarnessGenerator {
213         cx,
214         tests: Vec::new(),
215     }.visit_crate(krate);
216 }
217
218 /// Creates a function item for use as the main function of a test build.
219 /// This function will call the `test_runner` as specified by the crate attribute
220 ///
221 /// By default this expands to
222 ///
223 /// #[main]
224 /// pub fn main() {
225 ///     extern crate test;
226 ///     test::test_main_static(&[
227 ///         &test_const1,
228 ///         &test_const2,
229 ///         &test_const3,
230 ///     ]);
231 /// }
232 ///
233 /// Most of the Ident have the usual def-site hygiene for the AST pass. The
234 /// exception is the `test_const`s. These have a syntax context that has two
235 /// opaque marks: one from the expansion of `test` or `test_case`, and one
236 /// generated  in `TestHarnessGenerator::flat_map_item`. When resolving this
237 /// identifier after failing to find a matching identifier in the root module
238 /// we remove the outer mark, and try resolving at its def-site, which will
239 /// then resolve to `test_const`.
240 ///
241 /// The expansion here can be controlled by two attributes:
242 ///
243 /// `reexport_test_harness_main` provides a different name for the `main`
244 /// function and `test_runner` provides a path that replaces
245 /// `test::test_main_static`.
246 fn mk_main(cx: &mut TestCtxt<'_>) -> P<ast::Item> {
247     let sp = cx.def_site;
248     let ecx = &cx.ext_cx;
249     let test_id = Ident::new(sym::test, sp);
250
251     // test::test_main_static(...)
252     let mut test_runner = cx.test_runner.clone().unwrap_or(
253         ecx.path(sp, vec![test_id, ecx.ident_of("test_main_static", sp)]));
254
255     test_runner.span = sp;
256
257     let test_main_path_expr = ecx.expr_path(test_runner);
258     let call_test_main = ecx.expr_call(sp, test_main_path_expr,
259                                        vec![mk_tests_slice(cx, sp)]);
260     let call_test_main = ecx.stmt_expr(call_test_main);
261
262     // extern crate test
263     let test_extern_stmt = ecx.stmt_item(sp, ecx.item(sp,
264         test_id,
265         vec![],
266         ast::ItemKind::ExternCrate(None)
267     ));
268
269     // #[main]
270     let main_meta = ecx.meta_word(sp, sym::main);
271     let main_attr = ecx.attribute(main_meta);
272
273     // pub fn main() { ... }
274     let main_ret_ty = ecx.ty(sp, ast::TyKind::Tup(vec![]));
275
276     // If no test runner is provided we need to import the test crate
277     let main_body = if cx.test_runner.is_none() {
278         ecx.block(sp, vec![test_extern_stmt, call_test_main])
279     } else {
280         ecx.block(sp, vec![call_test_main])
281     };
282
283     let main = ast::ItemKind::Fn(ecx.fn_decl(vec![], ast::FunctionRetTy::Ty(main_ret_ty)),
284                            ast::FnHeader::default(),
285                            ast::Generics::default(),
286                            main_body);
287
288     // Honor the reexport_test_harness_main attribute
289     let main_id = match cx.reexport_test_harness_main {
290         Some(sym) => Ident::new(sym, sp.with_ctxt(SyntaxContext::root())),
291         None => Ident::new(sym::main, sp),
292     };
293
294     let main = P(ast::Item {
295         ident: main_id,
296         attrs: vec![main_attr],
297         id: ast::DUMMY_NODE_ID,
298         node: main,
299         vis: respan(sp, ast::VisibilityKind::Public),
300         span: sp,
301         tokens: None,
302     });
303
304     // Integrate the new item into existing module structures.
305     let main = AstFragment::Items(smallvec![main]);
306     cx.ext_cx.monotonic_expander().fully_expand_fragment(main).make_items().pop().unwrap()
307 }
308
309 /// Creates a slice containing every test like so:
310 /// &[&test1, &test2]
311 fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> P<ast::Expr> {
312     debug!("building test vector from {} tests", cx.test_cases.len());
313     let ref ecx = cx.ext_cx;
314
315
316     ecx.expr_vec_slice(sp,
317         cx.test_cases.iter().map(|test| {
318             ecx.expr_addr_of(test.span,
319                 ecx.expr_path(ecx.path(test.span, vec![test.ident])))
320         }).collect())
321 }
322
323 fn is_test_case(i: &ast::Item) -> bool {
324     attr::contains_name(&i.attrs, sym::rustc_test_marker)
325 }
326
327 fn get_test_runner(sd: &errors::Handler, krate: &ast::Crate) -> Option<ast::Path> {
328     let test_attr = attr::find_by_name(&krate.attrs, sym::test_runner)?;
329     test_attr.meta_item_list().map(|meta_list| {
330         if meta_list.len() != 1 {
331             sd.span_fatal(test_attr.span,
332                 "`#![test_runner(..)]` accepts exactly 1 argument").raise()
333         }
334         match meta_list[0].meta_item() {
335             Some(meta_item) if meta_item.is_word() => meta_item.path.clone(),
336             _ => sd.span_fatal(test_attr.span, "`test_runner` argument must be a path").raise()
337         }
338     })
339 }