]> git.lizzy.rs Git - rust.git/blob - src/libsyntax_ext/test_harness.rs
Rollup merge of #63055 - Mark-Simulacrum:save-analysis-clean-2, r=Xanewok
[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::build::AstBuilder;
10 use syntax::ext::expand::ExpansionConfig;
11 use syntax::ext::hygiene::{ExpnId, MacroKind};
12 use syntax::feature_gate::Features;
13 use syntax::mut_visit::{*, ExpectOne};
14 use syntax::parse::ParseSess;
15 use syntax::ptr::P;
16 use syntax::source_map::{ExpnInfo, ExpnKind, dummy_spanned};
17 use syntax::symbol::{kw, sym, Symbol};
18 use syntax_pos::{Span, DUMMY_SP};
19
20 use std::{iter, mem};
21
22 struct Test {
23     span: Span,
24     path: Vec<Ident>,
25 }
26
27 struct TestCtxt<'a> {
28     span_diagnostic: &'a errors::Handler,
29     path: Vec<Ident>,
30     ext_cx: ExtCtxt<'a>,
31     test_cases: Vec<Test>,
32     reexport_test_harness_main: Option<Symbol>,
33     test_runner: Option<ast::Path>,
34     // top-level re-export submodule, filled out after folding is finished
35     toplevel_reexport: Option<Ident>,
36 }
37
38 // Traverse the crate, collecting all the test functions, eliding any
39 // existing main functions, and synthesizing a main test harness
40 pub fn inject(
41     sess: &ParseSess,
42     resolver: &mut dyn Resolver,
43     should_test: bool,
44     krate: &mut ast::Crate,
45     span_diagnostic: &errors::Handler,
46     features: &Features,
47 ) {
48     // Check for #[reexport_test_harness_main = "some_name"] which
49     // creates a `use __test::main as some_name;`. This needs to be
50     // unconditional, so that the attribute is still marked as used in
51     // non-test builds.
52     let reexport_test_harness_main =
53         attr::first_attr_value_str_by_name(&krate.attrs, sym::reexport_test_harness_main);
54
55     // Do this here so that the test_runner crate attribute gets marked as used
56     // even in non-test builds
57     let test_runner = get_test_runner(span_diagnostic, &krate);
58
59     if should_test {
60         generate_test_harness(sess, resolver, reexport_test_harness_main,
61                               krate, span_diagnostic, features, test_runner)
62     }
63 }
64
65 struct TestHarnessGenerator<'a> {
66     cx: TestCtxt<'a>,
67     tests: Vec<Ident>,
68
69     // submodule name, gensym'd identifier for re-exports
70     tested_submods: Vec<(Ident, Ident)>,
71 }
72
73 impl<'a> MutVisitor for TestHarnessGenerator<'a> {
74     fn visit_crate(&mut self, c: &mut ast::Crate) {
75         noop_visit_crate(c, self);
76
77         // Create a main function to run our tests
78         let test_main = {
79             let unresolved = mk_main(&mut self.cx);
80             self.cx.ext_cx.monotonic_expander().flat_map_item(unresolved).pop().unwrap()
81         };
82
83         c.module.items.push(test_main);
84     }
85
86     fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> {
87         let ident = i.ident;
88         if ident.name != kw::Invalid {
89             self.cx.path.push(ident);
90         }
91         debug!("current path: {}", path_name_i(&self.cx.path));
92
93         let mut item = i.into_inner();
94         if is_test_case(&item) {
95             debug!("this is a test item");
96
97             let test = Test {
98                 span: item.span,
99                 path: self.cx.path.clone(),
100             };
101             self.cx.test_cases.push(test);
102             self.tests.push(item.ident);
103         }
104
105         // We don't want to recurse into anything other than mods, since
106         // mods or tests inside of functions will break things
107         if let ast::ItemKind::Mod(mut module) = item.node {
108             let tests = mem::take(&mut self.tests);
109             let tested_submods = mem::take(&mut self.tested_submods);
110             noop_visit_mod(&mut module, self);
111             let tests = mem::replace(&mut self.tests, tests);
112             let tested_submods = mem::replace(&mut self.tested_submods, tested_submods);
113
114             if !tests.is_empty() || !tested_submods.is_empty() {
115                 let (it, sym) = mk_reexport_mod(&mut self.cx, item.id, tests, tested_submods);
116                 module.items.push(it);
117
118                 if !self.cx.path.is_empty() {
119                     self.tested_submods.push((self.cx.path[self.cx.path.len()-1], sym));
120                 } else {
121                     debug!("pushing nothing, sym: {:?}", sym);
122                     self.cx.toplevel_reexport = Some(sym);
123                 }
124             }
125             item.node = ast::ItemKind::Mod(module);
126         }
127         if ident.name != kw::Invalid {
128             self.cx.path.pop();
129         }
130         smallvec![P(item)]
131     }
132
133     fn visit_mac(&mut self, _mac: &mut ast::Mac) {
134         // Do nothing.
135     }
136 }
137
138 /// A folder used to remove any entry points (like fn main) because the harness
139 /// generator will provide its own
140 struct EntryPointCleaner {
141     // Current depth in the ast
142     depth: usize,
143 }
144
145 impl MutVisitor for EntryPointCleaner {
146     fn flat_map_item(&mut self, i: P<ast::Item>) -> SmallVec<[P<ast::Item>; 1]> {
147         self.depth += 1;
148         let item = noop_flat_map_item(i, self).expect_one("noop did something");
149         self.depth -= 1;
150
151         // Remove any #[main] or #[start] from the AST so it doesn't
152         // clash with the one we're going to add, but mark it as
153         // #[allow(dead_code)] to avoid printing warnings.
154         let item = match entry::entry_point_type(&item, self.depth) {
155             EntryPointType::MainNamed |
156             EntryPointType::MainAttr |
157             EntryPointType::Start =>
158                 item.map(|ast::Item {id, ident, attrs, node, vis, span, tokens}| {
159                     let allow_ident = Ident::with_empty_ctxt(sym::allow);
160                     let dc_nested = attr::mk_nested_word_item(Ident::from_str("dead_code"));
161                     let allow_dead_code_item = attr::mk_list_item(DUMMY_SP, allow_ident,
162                                                                   vec![dc_nested]);
163                     let allow_dead_code = attr::mk_attr_outer(DUMMY_SP,
164                                                               attr::mk_attr_id(),
165                                                               allow_dead_code_item);
166
167                     ast::Item {
168                         id,
169                         ident,
170                         attrs: attrs.into_iter()
171                             .filter(|attr| {
172                                 !attr.check_name(sym::main) && !attr.check_name(sym::start)
173                             })
174                             .chain(iter::once(allow_dead_code))
175                             .collect(),
176                         node,
177                         vis,
178                         span,
179                         tokens,
180                     }
181                 }),
182             EntryPointType::None |
183             EntryPointType::OtherMain => item,
184         };
185
186         smallvec![item]
187     }
188
189     fn visit_mac(&mut self, _mac: &mut ast::Mac) {
190         // Do nothing.
191     }
192 }
193
194 /// Creates an item (specifically a module) that "pub use"s the tests passed in.
195 /// Each tested submodule will contain a similar reexport module that we will export
196 /// under the name of the original module. That is, `submod::__test_reexports` is
197 /// reexported like so `pub use submod::__test_reexports as submod`.
198 fn mk_reexport_mod(cx: &mut TestCtxt<'_>,
199                    parent: ast::NodeId,
200                    tests: Vec<Ident>,
201                    tested_submods: Vec<(Ident, Ident)>)
202                    -> (P<ast::Item>, Ident) {
203     let super_ = Ident::with_empty_ctxt(kw::Super);
204
205     let items = tests.into_iter().map(|r| {
206         cx.ext_cx.item_use_simple(DUMMY_SP, dummy_spanned(ast::VisibilityKind::Public),
207                                   cx.ext_cx.path(DUMMY_SP, vec![super_, r]))
208     }).chain(tested_submods.into_iter().map(|(r, sym)| {
209         let path = cx.ext_cx.path(DUMMY_SP, vec![super_, r, sym]);
210         cx.ext_cx.item_use_simple_(DUMMY_SP, dummy_spanned(ast::VisibilityKind::Public),
211                                    Some(r), path)
212     })).collect();
213
214     let reexport_mod = ast::Mod {
215         inline: true,
216         inner: DUMMY_SP,
217         items,
218     };
219
220     let name = Ident::from_str("__test_reexports").gensym();
221     let parent = if parent == ast::DUMMY_NODE_ID { ast::CRATE_NODE_ID } else { parent };
222     cx.ext_cx.current_expansion.id = cx.ext_cx.resolver.get_module_scope(parent);
223     let it = cx.ext_cx.monotonic_expander().flat_map_item(P(ast::Item {
224         ident: name,
225         attrs: Vec::new(),
226         id: ast::DUMMY_NODE_ID,
227         node: ast::ItemKind::Mod(reexport_mod),
228         vis: dummy_spanned(ast::VisibilityKind::Public),
229         span: DUMMY_SP,
230         tokens: None,
231     })).pop().unwrap();
232
233     (it, name)
234 }
235
236 /// Crawl over the crate, inserting test reexports and the test main function
237 fn generate_test_harness(sess: &ParseSess,
238                          resolver: &mut dyn Resolver,
239                          reexport_test_harness_main: Option<Symbol>,
240                          krate: &mut ast::Crate,
241                          sd: &errors::Handler,
242                          features: &Features,
243                          test_runner: Option<ast::Path>) {
244     // Remove the entry points
245     let mut cleaner = EntryPointCleaner { depth: 0 };
246     cleaner.visit_crate(krate);
247
248     let mut econfig = ExpansionConfig::default("test".to_string());
249     econfig.features = Some(features);
250
251     let cx = TestCtxt {
252         span_diagnostic: sd,
253         ext_cx: ExtCtxt::new(sess, econfig, resolver),
254         path: Vec::new(),
255         test_cases: Vec::new(),
256         reexport_test_harness_main,
257         toplevel_reexport: None,
258         test_runner
259     };
260
261     TestHarnessGenerator {
262         cx,
263         tests: Vec::new(),
264         tested_submods: Vec::new(),
265     }.visit_crate(krate);
266 }
267
268 /// Creates a function item for use as the main function of a test build.
269 /// This function will call the `test_runner` as specified by the crate attribute
270 fn mk_main(cx: &mut TestCtxt<'_>) -> P<ast::Item> {
271     // Writing this out by hand:
272     //        pub fn main() {
273     //            #![main]
274     //            test::test_main_static(&[..tests]);
275     //        }
276     let sp = DUMMY_SP.fresh_expansion(ExpnId::root(), ExpnInfo::allow_unstable(
277         ExpnKind::Macro(MacroKind::Attr, sym::test_case), DUMMY_SP, cx.ext_cx.parse_sess.edition,
278         [sym::main, sym::test, sym::rustc_attrs][..].into(),
279     ));
280     let ecx = &cx.ext_cx;
281     let test_id = Ident::with_empty_ctxt(sym::test);
282
283     // test::test_main_static(...)
284     let mut test_runner = cx.test_runner.clone().unwrap_or(
285         ecx.path(sp, vec![
286             test_id, ecx.ident_of("test_main_static")
287         ]));
288
289     test_runner.span = sp;
290
291     let test_main_path_expr = ecx.expr_path(test_runner);
292     let call_test_main = ecx.expr_call(sp, test_main_path_expr,
293                                        vec![mk_tests_slice(cx)]);
294     let call_test_main = ecx.stmt_expr(call_test_main);
295
296     // #![main]
297     let main_meta = ecx.meta_word(sp, sym::main);
298     let main_attr = ecx.attribute(sp, main_meta);
299
300     // extern crate test as test_gensym
301     let test_extern_stmt = ecx.stmt_item(sp, ecx.item(sp,
302         test_id,
303         vec![],
304         ast::ItemKind::ExternCrate(None)
305     ));
306
307     // pub fn main() { ... }
308     let main_ret_ty = ecx.ty(sp, ast::TyKind::Tup(vec![]));
309
310     // If no test runner is provided we need to import the test crate
311     let main_body = if cx.test_runner.is_none() {
312         ecx.block(sp, vec![test_extern_stmt, call_test_main])
313     } else {
314         ecx.block(sp, vec![call_test_main])
315     };
316
317     let main = ast::ItemKind::Fn(ecx.fn_decl(vec![], ast::FunctionRetTy::Ty(main_ret_ty)),
318                            ast::FnHeader::default(),
319                            ast::Generics::default(),
320                            main_body);
321
322     // Honor the reexport_test_harness_main attribute
323     let main_id = match cx.reexport_test_harness_main {
324         Some(sym) => Ident::new(sym, sp),
325         None => Ident::from_str_and_span("main", sp).gensym(),
326     };
327
328     P(ast::Item {
329         ident: main_id,
330         attrs: vec![main_attr],
331         id: ast::DUMMY_NODE_ID,
332         node: main,
333         vis: dummy_spanned(ast::VisibilityKind::Public),
334         span: sp,
335         tokens: None,
336     })
337
338 }
339
340 fn path_name_i(idents: &[Ident]) -> String {
341     let mut path_name = "".to_string();
342     let mut idents_iter = idents.iter().peekable();
343     while let Some(ident) = idents_iter.next() {
344         path_name.push_str(&ident.as_str());
345         if idents_iter.peek().is_some() {
346             path_name.push_str("::")
347         }
348     }
349     path_name
350 }
351
352 /// Creates a slice containing every test like so:
353 /// &[path::to::test1, path::to::test2]
354 fn mk_tests_slice(cx: &TestCtxt<'_>) -> P<ast::Expr> {
355     debug!("building test vector from {} tests", cx.test_cases.len());
356     let ref ecx = cx.ext_cx;
357
358     ecx.expr_vec_slice(DUMMY_SP,
359         cx.test_cases.iter().map(|test| {
360             ecx.expr_addr_of(test.span,
361                 ecx.expr_path(ecx.path(test.span, visible_path(cx, &test.path))))
362         }).collect())
363 }
364
365 /// Creates a path from the top-level __test module to the test via __test_reexports
366 fn visible_path(cx: &TestCtxt<'_>, path: &[Ident]) -> Vec<Ident>{
367     let mut visible_path = vec![];
368     match cx.toplevel_reexport {
369         Some(id) => visible_path.push(id),
370         None => {
371             cx.span_diagnostic.bug("expected to find top-level re-export name, but found None");
372         }
373     }
374     visible_path.extend_from_slice(path);
375     visible_path
376 }
377
378 fn is_test_case(i: &ast::Item) -> bool {
379     attr::contains_name(&i.attrs, sym::rustc_test_marker)
380 }
381
382 fn get_test_runner(sd: &errors::Handler, krate: &ast::Crate) -> Option<ast::Path> {
383     let test_attr = attr::find_by_name(&krate.attrs, sym::test_runner)?;
384     test_attr.meta_item_list().map(|meta_list| {
385         if meta_list.len() != 1 {
386             sd.span_fatal(test_attr.span,
387                 "`#![test_runner(..)]` accepts exactly 1 argument").raise()
388         }
389         match meta_list[0].meta_item() {
390             Some(meta_item) if meta_item.is_word() => meta_item.path.clone(),
391             _ => sd.span_fatal(test_attr.span, "`test_runner` argument must be a path").raise()
392         }
393     })
394 }