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