]> git.lizzy.rs Git - rust.git/blob - src/librustc_builtin_macros/test_harness.rs
Rollup merge of #71767 - tshepang:stack-stuff, r=jonas-schievink
[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_ast::ast::{self, Ident};
5 use rustc_ast::attr;
6 use rustc_ast::entry::{self, EntryPointType};
7 use rustc_ast::mut_visit::{ExpectOne, *};
8 use rustc_ast::ptr::P;
9 use rustc_expand::base::{ExtCtxt, Resolver};
10 use rustc_expand::expand::{AstFragment, ExpansionConfig};
11 use rustc_feature::Features;
12 use rustc_session::parse::ParseSess;
13 use rustc_span::hygiene::{AstPass, SyntaxContext, Transparency};
14 use rustc_span::source_map::respan;
15 use rustc_span::symbol::{sym, Symbol};
16 use rustc_span::{Span, DUMMY_SP};
17 use rustc_target::spec::PanicStrategy;
18 use smallvec::{smallvec, SmallVec};
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: &rustc_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::MacCall) {
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                     let attrs = attrs
174                         .into_iter()
175                         .filter(|attr| !attr.check_name(sym::main) && !attr.check_name(sym::start))
176                         .chain(iter::once(allow_dead_code))
177                         .collect();
178
179                     ast::Item { id, ident, attrs, kind, vis, span, tokens }
180                 }),
181             EntryPointType::None | EntryPointType::OtherMain => item,
182         };
183
184         smallvec![item]
185     }
186
187     fn visit_mac(&mut self, _mac: &mut ast::MacCall) {
188         // Do nothing.
189     }
190 }
191
192 /// Crawl over the crate, inserting test reexports and the test main function
193 fn generate_test_harness(
194     sess: &ParseSess,
195     resolver: &mut dyn Resolver,
196     reexport_test_harness_main: Option<Symbol>,
197     krate: &mut ast::Crate,
198     features: &Features,
199     panic_strategy: PanicStrategy,
200     test_runner: Option<ast::Path>,
201 ) {
202     let mut econfig = ExpansionConfig::default("test".to_string());
203     econfig.features = Some(features);
204
205     let ext_cx = ExtCtxt::new(sess, econfig, resolver, None);
206
207     let expn_id = ext_cx.resolver.expansion_for_ast_pass(
208         DUMMY_SP,
209         AstPass::TestHarness,
210         &[sym::main, sym::test, sym::rustc_attrs],
211         None,
212     );
213     let def_site = DUMMY_SP.with_def_site_ctxt(expn_id);
214
215     // Remove the entry points
216     let mut cleaner = EntryPointCleaner { depth: 0, def_site };
217     cleaner.visit_crate(krate);
218
219     let cx = TestCtxt {
220         ext_cx,
221         panic_strategy,
222         def_site,
223         test_cases: Vec::new(),
224         reexport_test_harness_main,
225         test_runner,
226     };
227
228     TestHarnessGenerator { cx, tests: Vec::new() }.visit_crate(krate);
229 }
230
231 /// Creates a function item for use as the main function of a test build.
232 /// This function will call the `test_runner` as specified by the crate attribute
233 ///
234 /// By default this expands to
235 ///
236 /// #[main]
237 /// pub fn main() {
238 ///     extern crate test;
239 ///     test::test_main_static(&[
240 ///         &test_const1,
241 ///         &test_const2,
242 ///         &test_const3,
243 ///     ]);
244 /// }
245 ///
246 /// Most of the Ident have the usual def-site hygiene for the AST pass. The
247 /// exception is the `test_const`s. These have a syntax context that has two
248 /// opaque marks: one from the expansion of `test` or `test_case`, and one
249 /// generated  in `TestHarnessGenerator::flat_map_item`. When resolving this
250 /// identifier after failing to find a matching identifier in the root module
251 /// we remove the outer mark, and try resolving at its def-site, which will
252 /// then resolve to `test_const`.
253 ///
254 /// The expansion here can be controlled by two attributes:
255 ///
256 /// `reexport_test_harness_main` provides a different name for the `main`
257 /// function and `test_runner` provides a path that replaces
258 /// `test::test_main_static`.
259 fn mk_main(cx: &mut TestCtxt<'_>) -> P<ast::Item> {
260     let sp = cx.def_site;
261     let ecx = &cx.ext_cx;
262     let test_id = Ident::new(sym::test, sp);
263
264     let runner_name = match cx.panic_strategy {
265         PanicStrategy::Unwind => "test_main_static",
266         PanicStrategy::Abort => "test_main_static_abort",
267     };
268
269     // test::test_main_static(...)
270     let mut test_runner = cx
271         .test_runner
272         .clone()
273         .unwrap_or(ecx.path(sp, vec![test_id, ecx.ident_of(runner_name, sp)]));
274
275     test_runner.span = sp;
276
277     let test_main_path_expr = ecx.expr_path(test_runner);
278     let call_test_main = ecx.expr_call(sp, test_main_path_expr, vec![mk_tests_slice(cx, sp)]);
279     let call_test_main = ecx.stmt_expr(call_test_main);
280
281     // extern crate test
282     let test_extern_stmt =
283         ecx.stmt_item(sp, ecx.item(sp, test_id, vec![], ast::ItemKind::ExternCrate(None)));
284
285     // #[main]
286     let main_meta = ecx.meta_word(sp, sym::main);
287     let main_attr = ecx.attribute(main_meta);
288
289     // pub fn main() { ... }
290     let main_ret_ty = ecx.ty(sp, ast::TyKind::Tup(vec![]));
291
292     // If no test runner is provided we need to import the test crate
293     let main_body = if cx.test_runner.is_none() {
294         ecx.block(sp, vec![test_extern_stmt, call_test_main])
295     } else {
296         ecx.block(sp, vec![call_test_main])
297     };
298
299     let decl = ecx.fn_decl(vec![], ast::FnRetTy::Ty(main_ret_ty));
300     let sig = ast::FnSig { decl, header: ast::FnHeader::default() };
301     let def = ast::Defaultness::Final;
302     let main = ast::ItemKind::Fn(def, sig, ast::Generics::default(), Some(main_body));
303
304     // Honor the reexport_test_harness_main attribute
305     let main_id = match cx.reexport_test_harness_main {
306         Some(sym) => Ident::new(sym, sp.with_ctxt(SyntaxContext::root())),
307         None => Ident::new(sym::main, sp),
308     };
309
310     let main = P(ast::Item {
311         ident: main_id,
312         attrs: vec![main_attr],
313         id: ast::DUMMY_NODE_ID,
314         kind: main,
315         vis: respan(sp, ast::VisibilityKind::Public),
316         span: sp,
317         tokens: None,
318     });
319
320     // Integrate the new item into existing module structures.
321     let main = AstFragment::Items(smallvec![main]);
322     cx.ext_cx.monotonic_expander().fully_expand_fragment(main).make_items().pop().unwrap()
323 }
324
325 /// Creates a slice containing every test like so:
326 /// &[&test1, &test2]
327 fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> P<ast::Expr> {
328     debug!("building test vector from {} tests", cx.test_cases.len());
329     let ecx = &cx.ext_cx;
330
331     ecx.expr_vec_slice(
332         sp,
333         cx.test_cases
334             .iter()
335             .map(|test| {
336                 ecx.expr_addr_of(test.span, ecx.expr_path(ecx.path(test.span, vec![test.ident])))
337             })
338             .collect(),
339     )
340 }
341
342 fn is_test_case(i: &ast::Item) -> bool {
343     attr::contains_name(&i.attrs, sym::rustc_test_marker)
344 }
345
346 fn get_test_runner(sd: &rustc_errors::Handler, krate: &ast::Crate) -> Option<ast::Path> {
347     let test_attr = attr::find_by_name(&krate.attrs, sym::test_runner)?;
348     let meta_list = test_attr.meta_item_list()?;
349     let span = test_attr.span;
350     match &*meta_list {
351         [single] => match single.meta_item() {
352             Some(meta_item) if meta_item.is_word() => return Some(meta_item.path.clone()),
353             _ => sd.struct_span_err(span, "`test_runner` argument must be a path").emit(),
354         },
355         _ => sd.struct_span_err(span, "`#![test_runner(..)]` accepts exactly 1 argument").emit(),
356     }
357     None
358 }