]> git.lizzy.rs Git - rust.git/blob - src/librustc_builtin_macros/test_harness.rs
Rollup merge of #73949 - wesleywiser:simplify_try_fixes, r=oli-obk
[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;
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, ResolverExpand};
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, Ident, 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 ResolverExpand,
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 ResolverExpand,
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 /// ```
237 /// #[main]
238 /// pub fn main() {
239 ///     extern crate test;
240 ///     test::test_main_static(&[
241 ///         &test_const1,
242 ///         &test_const2,
243 ///         &test_const3,
244 ///     ]);
245 /// }
246 /// ```
247 ///
248 /// Most of the Ident have the usual def-site hygiene for the AST pass. The
249 /// exception is the `test_const`s. These have a syntax context that has two
250 /// opaque marks: one from the expansion of `test` or `test_case`, and one
251 /// generated  in `TestHarnessGenerator::flat_map_item`. When resolving this
252 /// identifier after failing to find a matching identifier in the root module
253 /// we remove the outer mark, and try resolving at its def-site, which will
254 /// then resolve to `test_const`.
255 ///
256 /// The expansion here can be controlled by two attributes:
257 ///
258 /// [`TestCtxt::reexport_test_harness_main`] provides a different name for the `main`
259 /// function and [`TestCtxt::test_runner`] provides a path that replaces
260 /// `test::test_main_static`.
261 fn mk_main(cx: &mut TestCtxt<'_>) -> P<ast::Item> {
262     let sp = cx.def_site;
263     let ecx = &cx.ext_cx;
264     let test_id = Ident::new(sym::test, sp);
265
266     let runner_name = match cx.panic_strategy {
267         PanicStrategy::Unwind => "test_main_static",
268         PanicStrategy::Abort => "test_main_static_abort",
269     };
270
271     // test::test_main_static(...)
272     let mut test_runner = cx
273         .test_runner
274         .clone()
275         .unwrap_or(ecx.path(sp, vec![test_id, ecx.ident_of(runner_name, sp)]));
276
277     test_runner.span = sp;
278
279     let test_main_path_expr = ecx.expr_path(test_runner);
280     let call_test_main = ecx.expr_call(sp, test_main_path_expr, vec![mk_tests_slice(cx, sp)]);
281     let call_test_main = ecx.stmt_expr(call_test_main);
282
283     // extern crate test
284     let test_extern_stmt =
285         ecx.stmt_item(sp, ecx.item(sp, test_id, vec![], ast::ItemKind::ExternCrate(None)));
286
287     // #[main]
288     let main_meta = ecx.meta_word(sp, sym::main);
289     let main_attr = ecx.attribute(main_meta);
290
291     // pub fn main() { ... }
292     let main_ret_ty = ecx.ty(sp, ast::TyKind::Tup(vec![]));
293
294     // If no test runner is provided we need to import the test crate
295     let main_body = if cx.test_runner.is_none() {
296         ecx.block(sp, vec![test_extern_stmt, call_test_main])
297     } else {
298         ecx.block(sp, vec![call_test_main])
299     };
300
301     let decl = ecx.fn_decl(vec![], ast::FnRetTy::Ty(main_ret_ty));
302     let sig = ast::FnSig { decl, header: ast::FnHeader::default() };
303     let def = ast::Defaultness::Final;
304     let main = ast::ItemKind::Fn(def, sig, ast::Generics::default(), Some(main_body));
305
306     // Honor the reexport_test_harness_main attribute
307     let main_id = match cx.reexport_test_harness_main {
308         Some(sym) => Ident::new(sym, sp.with_ctxt(SyntaxContext::root())),
309         None => Ident::new(sym::main, sp),
310     };
311
312     let main = P(ast::Item {
313         ident: main_id,
314         attrs: vec![main_attr],
315         id: ast::DUMMY_NODE_ID,
316         kind: main,
317         vis: respan(sp, ast::VisibilityKind::Public),
318         span: sp,
319         tokens: None,
320     });
321
322     // Integrate the new item into existing module structures.
323     let main = AstFragment::Items(smallvec![main]);
324     cx.ext_cx.monotonic_expander().fully_expand_fragment(main).make_items().pop().unwrap()
325 }
326
327 /// Creates a slice containing every test like so:
328 /// &[&test1, &test2]
329 fn mk_tests_slice(cx: &TestCtxt<'_>, sp: Span) -> P<ast::Expr> {
330     debug!("building test vector from {} tests", cx.test_cases.len());
331     let ecx = &cx.ext_cx;
332
333     ecx.expr_vec_slice(
334         sp,
335         cx.test_cases
336             .iter()
337             .map(|test| {
338                 ecx.expr_addr_of(test.span, ecx.expr_path(ecx.path(test.span, vec![test.ident])))
339             })
340             .collect(),
341     )
342 }
343
344 fn is_test_case(i: &ast::Item) -> bool {
345     attr::contains_name(&i.attrs, sym::rustc_test_marker)
346 }
347
348 fn get_test_runner(sd: &rustc_errors::Handler, krate: &ast::Crate) -> Option<ast::Path> {
349     let test_attr = attr::find_by_name(&krate.attrs, sym::test_runner)?;
350     let meta_list = test_attr.meta_item_list()?;
351     let span = test_attr.span;
352     match &*meta_list {
353         [single] => match single.meta_item() {
354             Some(meta_item) if meta_item.is_word() => return Some(meta_item.path.clone()),
355             _ => sd.struct_span_err(span, "`test_runner` argument must be a path").emit(),
356         },
357         _ => sd.struct_span_err(span, "`#![test_runner(..)]` accepts exactly 1 argument").emit(),
358     }
359     None
360 }