]> git.lizzy.rs Git - rust.git/blob - src/libsyntax_ext/test.rs
Auto merge of #61140 - estebank:attr-diagnostics, r=michaelwoerister
[rust.git] / src / libsyntax_ext / test.rs
1 /// The expansion from a test function to the appropriate test struct for libtest
2 /// Ideally, this code would be in libtest but for efficiency and error messages it lives here.
3
4 use syntax::ext::base::*;
5 use syntax::ext::build::AstBuilder;
6 use syntax::ext::hygiene::{Mark, SyntaxContext};
7 use syntax::attr;
8 use syntax::ast;
9 use syntax::print::pprust;
10 use syntax::symbol::{Symbol, sym};
11 use syntax_pos::{DUMMY_SP, Span};
12 use syntax::source_map::{ExpnInfo, MacroAttribute};
13 use std::iter;
14
15 pub fn expand_test(
16     cx: &mut ExtCtxt<'_>,
17     attr_sp: Span,
18     _meta_item: &ast::MetaItem,
19     item: Annotatable,
20 ) -> Vec<Annotatable> {
21     expand_test_or_bench(cx, attr_sp, item, false)
22 }
23
24 pub fn expand_bench(
25     cx: &mut ExtCtxt<'_>,
26     attr_sp: Span,
27     _meta_item: &ast::MetaItem,
28     item: Annotatable,
29 ) -> Vec<Annotatable> {
30     expand_test_or_bench(cx, attr_sp, item, true)
31 }
32
33 pub fn expand_test_or_bench(
34     cx: &mut ExtCtxt<'_>,
35     attr_sp: Span,
36     item: Annotatable,
37     is_bench: bool
38 ) -> Vec<Annotatable> {
39     // If we're not in test configuration, remove the annotated item
40     if !cx.ecfg.should_test { return vec![]; }
41
42     let item =
43         if let Annotatable::Item(i) = item { i }
44         else {
45             cx.parse_sess.span_diagnostic.span_fatal(item.span(),
46                 "#[test] attribute is only allowed on non associated functions").raise();
47         };
48
49     if let ast::ItemKind::Mac(_) = item.node {
50         cx.parse_sess.span_diagnostic.span_warn(item.span,
51             "#[test] attribute should not be used on macros. Use #[cfg(test)] instead.");
52         return vec![Annotatable::Item(item)];
53     }
54
55     // has_*_signature will report any errors in the type so compilation
56     // will fail. We shouldn't try to expand in this case because the errors
57     // would be spurious.
58     if (!is_bench && !has_test_signature(cx, &item)) ||
59         (is_bench && !has_bench_signature(cx, &item)) {
60         return vec![Annotatable::Item(item)];
61     }
62
63     let (sp, attr_sp) = {
64         let mark = Mark::fresh(Mark::root());
65         mark.set_expn_info(ExpnInfo {
66             call_site: DUMMY_SP,
67             def_site: None,
68             format: MacroAttribute(sym::test),
69             allow_internal_unstable: Some(vec![sym::rustc_attrs, sym::test].into()),
70             allow_internal_unsafe: false,
71             local_inner_macros: false,
72             edition: cx.parse_sess.edition,
73         });
74         (item.span.with_ctxt(SyntaxContext::empty().apply_mark(mark)),
75          attr_sp.with_ctxt(SyntaxContext::empty().apply_mark(mark)))
76     };
77
78     // Gensym "test" so we can extern crate without conflicting with any local names
79     let test_id = cx.ident_of("test").gensym();
80
81     // creates test::$name
82     let test_path = |name| {
83         cx.path(sp, vec![test_id, cx.ident_of(name)])
84     };
85
86     // creates test::ShouldPanic::$name
87     let should_panic_path = |name| {
88         cx.path(sp, vec![test_id, cx.ident_of("ShouldPanic"), cx.ident_of(name)])
89     };
90
91     // creates $name: $expr
92     let field = |name, expr| cx.field_imm(sp, cx.ident_of(name), expr);
93
94     let test_fn = if is_bench {
95         // A simple ident for a lambda
96         let b = cx.ident_of("b");
97
98         cx.expr_call(sp, cx.expr_path(test_path("StaticBenchFn")), vec![
99             // |b| self::test::assert_test_result(
100             cx.lambda1(sp,
101                 cx.expr_call(sp, cx.expr_path(test_path("assert_test_result")), vec![
102                     // super::$test_fn(b)
103                     cx.expr_call(sp,
104                         cx.expr_path(cx.path(sp, vec![item.ident])),
105                         vec![cx.expr_ident(sp, b)])
106                 ]),
107                 b
108             )
109             // )
110         ])
111     } else {
112         cx.expr_call(sp, cx.expr_path(test_path("StaticTestFn")), vec![
113             // || {
114             cx.lambda0(sp,
115                 // test::assert_test_result(
116                 cx.expr_call(sp, cx.expr_path(test_path("assert_test_result")), vec![
117                     // $test_fn()
118                     cx.expr_call(sp, cx.expr_path(cx.path(sp, vec![item.ident])), vec![])
119                 // )
120                 ])
121             // }
122             )
123         // )
124         ])
125     };
126
127     let mut test_const = cx.item(sp, ast::Ident::new(item.ident.name, sp).gensym(),
128         vec![
129             // #[cfg(test)]
130             cx.attribute(attr_sp, cx.meta_list(attr_sp, sym::cfg, vec![
131                 cx.meta_list_item_word(attr_sp, sym::test)
132             ])),
133             // #[rustc_test_marker]
134             cx.attribute(attr_sp, cx.meta_word(attr_sp, sym::rustc_test_marker)),
135         ],
136         // const $ident: test::TestDescAndFn =
137         ast::ItemKind::Const(cx.ty(sp, ast::TyKind::Path(None, test_path("TestDescAndFn"))),
138             // test::TestDescAndFn {
139             cx.expr_struct(sp, test_path("TestDescAndFn"), vec![
140                 // desc: test::TestDesc {
141                 field("desc", cx.expr_struct(sp, test_path("TestDesc"), vec![
142                     // name: "path::to::test"
143                     field("name", cx.expr_call(sp, cx.expr_path(test_path("StaticTestName")),
144                         vec![
145                             cx.expr_str(sp, Symbol::intern(&item_path(
146                                 // skip the name of the root module
147                                 &cx.current_expansion.module.mod_path[1..],
148                                 &item.ident
149                             )))
150                         ])),
151                     // ignore: true | false
152                     field("ignore", cx.expr_bool(sp, should_ignore(&item))),
153                     // allow_fail: true | false
154                     field("allow_fail", cx.expr_bool(sp, should_fail(&item))),
155                     // should_panic: ...
156                     field("should_panic", match should_panic(cx, &item) {
157                         // test::ShouldPanic::No
158                         ShouldPanic::No => cx.expr_path(should_panic_path("No")),
159                         // test::ShouldPanic::Yes
160                         ShouldPanic::Yes(None) => cx.expr_path(should_panic_path("Yes")),
161                         // test::ShouldPanic::YesWithMessage("...")
162                         ShouldPanic::Yes(Some(sym)) => cx.expr_call(sp,
163                             cx.expr_path(should_panic_path("YesWithMessage")),
164                             vec![cx.expr_str(sp, sym)]),
165                     }),
166                 // },
167                 ])),
168                 // testfn: test::StaticTestFn(...) | test::StaticBenchFn(...)
169                 field("testfn", test_fn)
170             // }
171             ])
172         // }
173         ));
174     test_const = test_const.map(|mut tc| { tc.vis.node = ast::VisibilityKind::Public; tc});
175
176     // extern crate test as test_gensym
177     let test_extern = cx.item(sp,
178         test_id,
179         vec![],
180         ast::ItemKind::ExternCrate(Some(sym::test))
181     );
182
183     log::debug!("Synthetic test item:\n{}\n", pprust::item_to_string(&test_const));
184
185     vec![
186         // Access to libtest under a gensymed name
187         Annotatable::Item(test_extern),
188         // The generated test case
189         Annotatable::Item(test_const),
190         // The original item
191         Annotatable::Item(item)
192     ]
193 }
194
195 fn item_path(mod_path: &[ast::Ident], item_ident: &ast::Ident) -> String {
196     mod_path.iter().chain(iter::once(item_ident))
197         .map(|x| x.to_string()).collect::<Vec<String>>().join("::")
198 }
199
200 enum ShouldPanic {
201     No,
202     Yes(Option<Symbol>),
203 }
204
205 fn should_ignore(i: &ast::Item) -> bool {
206     attr::contains_name(&i.attrs, sym::ignore)
207 }
208
209 fn should_fail(i: &ast::Item) -> bool {
210     attr::contains_name(&i.attrs, sym::allow_fail)
211 }
212
213 fn should_panic(cx: &ExtCtxt<'_>, i: &ast::Item) -> ShouldPanic {
214     match attr::find_by_name(&i.attrs, sym::should_panic) {
215         Some(attr) => {
216             let ref sd = cx.parse_sess.span_diagnostic;
217
218             match attr.meta_item_list() {
219                 // Handle #[should_panic(expected = "foo")]
220                 Some(list) => {
221                     let msg = list.iter()
222                         .find(|mi| mi.check_name(sym::expected))
223                         .and_then(|mi| mi.meta_item())
224                         .and_then(|mi| mi.value_str());
225                     if list.len() != 1 || msg.is_none() {
226                         sd.struct_span_warn(
227                             attr.span,
228                             "argument must be of the form: \
229                              `expected = \"error message\"`"
230                         ).note("Errors in this attribute were erroneously \
231                                 allowed and will become a hard error in a \
232                                 future release.").emit();
233                         ShouldPanic::Yes(None)
234                     } else {
235                         ShouldPanic::Yes(msg)
236                     }
237                 },
238                 // Handle #[should_panic] and #[should_panic = "expected"]
239                 None => ShouldPanic::Yes(attr.value_str())
240             }
241         }
242         None => ShouldPanic::No,
243     }
244 }
245
246 fn has_test_signature(cx: &ExtCtxt<'_>, i: &ast::Item) -> bool {
247     let has_should_panic_attr = attr::contains_name(&i.attrs, sym::should_panic);
248     let ref sd = cx.parse_sess.span_diagnostic;
249     if let ast::ItemKind::Fn(ref decl, ref header, ref generics, _) = i.node {
250         if header.unsafety == ast::Unsafety::Unsafe {
251             sd.span_err(
252                 i.span,
253                 "unsafe functions cannot be used for tests"
254             );
255             return false
256         }
257         if header.asyncness.node.is_async() {
258             sd.span_err(
259                 i.span,
260                 "async functions cannot be used for tests"
261             );
262             return false
263         }
264
265
266         // If the termination trait is active, the compiler will check that the output
267         // type implements the `Termination` trait as `libtest` enforces that.
268         let has_output = match decl.output {
269             ast::FunctionRetTy::Default(..) => false,
270             ast::FunctionRetTy::Ty(ref t) if t.node.is_unit() => false,
271             _ => true
272         };
273
274         if !decl.inputs.is_empty() {
275             sd.span_err(i.span, "functions used as tests can not have any arguments");
276             return false;
277         }
278
279         match (has_output, has_should_panic_attr) {
280             (true, true) => {
281                 sd.span_err(i.span, "functions using `#[should_panic]` must return `()`");
282                 false
283             },
284             (true, false) => if !generics.params.is_empty() {
285                 sd.span_err(i.span,
286                                 "functions used as tests must have signature fn() -> ()");
287                 false
288             } else {
289                 true
290             },
291             (false, _) => true
292         }
293     } else {
294         sd.span_err(i.span, "only functions may be used as tests");
295         false
296     }
297 }
298
299 fn has_bench_signature(cx: &ExtCtxt<'_>, i: &ast::Item) -> bool {
300     let has_sig = if let ast::ItemKind::Fn(ref decl, _, _, _) = i.node {
301         // N.B., inadequate check, but we're running
302         // well before resolve, can't get too deep.
303         decl.inputs.len() == 1
304     } else {
305         false
306     };
307
308     if !has_sig {
309         cx.parse_sess.span_diagnostic.span_err(i.span, "functions used as benches must have \
310             signature `fn(&mut Bencher) -> impl Termination`");
311     }
312
313     has_sig
314 }