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 use crate::util::check_builtin_macro_attribute;
4 
5 use rustc_ast as ast;
6 use rustc_ast::attr;
7 use rustc_ast::ptr::P;
8 use rustc_ast_pretty::pprust;
9 use rustc_expand::base::*;
10 use rustc_session::Session;
11 use rustc_span::symbol::{sym, Ident, Symbol};
12 use rustc_span::Span;
13 
14 use std::iter;
15 
16 // #[test_case] is used by custom test authors to mark tests
17 // When building for test, it needs to make the item public and gensym the name
18 // Otherwise, we'll omit the item. This behavior means that any item annotated
19 // with #[test_case] is never addressable.
20 //
21 // We mark item with an inert attribute "rustc_test_marker" which the test generation
22 // logic will pick up on.
expand_test_case( ecx: &mut ExtCtxt<'_>, attr_sp: Span, meta_item: &ast::MetaItem, anno_item: Annotatable, ) -> Vec<Annotatable>23 pub fn expand_test_case(
24     ecx: &mut ExtCtxt<'_>,
25     attr_sp: Span,
26     meta_item: &ast::MetaItem,
27     anno_item: Annotatable,
28 ) -> Vec<Annotatable> {
29     check_builtin_macro_attribute(ecx, meta_item, sym::test_case);
30 
31     if !ecx.ecfg.should_test {
32         return vec![];
33     }
34 
35     let sp = ecx.with_def_site_ctxt(attr_sp);
36     let mut item = anno_item.expect_item();
37     item = item.map(|mut item| {
38         item.vis = ast::Visibility {
39             span: item.vis.span,
40             kind: ast::VisibilityKind::Public,
41             tokens: None,
42         };
43         item.ident.span = item.ident.span.with_ctxt(sp.ctxt());
44         item.attrs.push(ecx.attribute(ecx.meta_word(sp, sym::rustc_test_marker)));
45         item
46     });
47 
48     return vec![Annotatable::Item(item)];
49 }
50 
expand_test( cx: &mut ExtCtxt<'_>, attr_sp: Span, meta_item: &ast::MetaItem, item: Annotatable, ) -> Vec<Annotatable>51 pub fn expand_test(
52     cx: &mut ExtCtxt<'_>,
53     attr_sp: Span,
54     meta_item: &ast::MetaItem,
55     item: Annotatable,
56 ) -> Vec<Annotatable> {
57     check_builtin_macro_attribute(cx, meta_item, sym::test);
58     expand_test_or_bench(cx, attr_sp, item, false)
59 }
60 
expand_bench( cx: &mut ExtCtxt<'_>, attr_sp: Span, meta_item: &ast::MetaItem, item: Annotatable, ) -> Vec<Annotatable>61 pub fn expand_bench(
62     cx: &mut ExtCtxt<'_>,
63     attr_sp: Span,
64     meta_item: &ast::MetaItem,
65     item: Annotatable,
66 ) -> Vec<Annotatable> {
67     check_builtin_macro_attribute(cx, meta_item, sym::bench);
68     expand_test_or_bench(cx, attr_sp, item, true)
69 }
70 
expand_test_or_bench( cx: &mut ExtCtxt<'_>, attr_sp: Span, item: Annotatable, is_bench: bool, ) -> Vec<Annotatable>71 pub fn expand_test_or_bench(
72     cx: &mut ExtCtxt<'_>,
73     attr_sp: Span,
74     item: Annotatable,
75     is_bench: bool,
76 ) -> Vec<Annotatable> {
77     // If we're not in test configuration, remove the annotated item
78     if !cx.ecfg.should_test {
79         return vec![];
80     }
81 
82     let (item, is_stmt) = match item {
83         Annotatable::Item(i) => (i, false),
84         Annotatable::Stmt(stmt) if matches!(stmt.kind, ast::StmtKind::Item(_)) => {
85             // FIXME: Use an 'if let' guard once they are implemented
86             if let ast::StmtKind::Item(i) = stmt.into_inner().kind {
87                 (i, true)
88             } else {
89                 unreachable!()
90             }
91         }
92         other => {
93             cx.struct_span_err(
94                 other.span(),
95                 "`#[test]` attribute is only allowed on non associated functions",
96             )
97             .emit();
98             return vec![other];
99         }
100     };
101 
102     if let ast::ItemKind::MacCall(_) = item.kind {
103         cx.sess.parse_sess.span_diagnostic.span_warn(
104             item.span,
105             "`#[test]` attribute should not be used on macros. Use `#[cfg(test)]` instead.",
106         );
107         return vec![Annotatable::Item(item)];
108     }
109 
110     // has_*_signature will report any errors in the type so compilation
111     // will fail. We shouldn't try to expand in this case because the errors
112     // would be spurious.
113     if (!is_bench && !has_test_signature(cx, &item))
114         || (is_bench && !has_bench_signature(cx, &item))
115     {
116         return vec![Annotatable::Item(item)];
117     }
118 
119     let (sp, attr_sp) = (cx.with_def_site_ctxt(item.span), cx.with_def_site_ctxt(attr_sp));
120 
121     let test_id = Ident::new(sym::test, attr_sp);
122 
123     // creates test::$name
124     let test_path = |name| cx.path(sp, vec![test_id, Ident::from_str_and_span(name, sp)]);
125 
126     // creates test::ShouldPanic::$name
127     let should_panic_path = |name| {
128         cx.path(
129             sp,
130             vec![
131                 test_id,
132                 Ident::from_str_and_span("ShouldPanic", sp),
133                 Ident::from_str_and_span(name, sp),
134             ],
135         )
136     };
137 
138     // creates test::TestType::$name
139     let test_type_path = |name| {
140         cx.path(
141             sp,
142             vec![
143                 test_id,
144                 Ident::from_str_and_span("TestType", sp),
145                 Ident::from_str_and_span(name, sp),
146             ],
147         )
148     };
149 
150     // creates $name: $expr
151     let field = |name, expr| cx.field_imm(sp, Ident::from_str_and_span(name, sp), expr);
152 
153     let test_fn = if is_bench {
154         // A simple ident for a lambda
155         let b = Ident::from_str_and_span("b", attr_sp);
156 
157         cx.expr_call(
158             sp,
159             cx.expr_path(test_path("StaticBenchFn")),
160             vec![
161                 // |b| self::test::assert_test_result(
162                 cx.lambda1(
163                     sp,
164                     cx.expr_call(
165                         sp,
166                         cx.expr_path(test_path("assert_test_result")),
167                         vec![
168                             // super::$test_fn(b)
169                             cx.expr_call(
170                                 sp,
171                                 cx.expr_path(cx.path(sp, vec![item.ident])),
172                                 vec![cx.expr_ident(sp, b)],
173                             ),
174                         ],
175                     ),
176                     b,
177                 ), // )
178             ],
179         )
180     } else {
181         cx.expr_call(
182             sp,
183             cx.expr_path(test_path("StaticTestFn")),
184             vec![
185                 // || {
186                 cx.lambda0(
187                     sp,
188                     // test::assert_test_result(
189                     cx.expr_call(
190                         sp,
191                         cx.expr_path(test_path("assert_test_result")),
192                         vec![
193                             // $test_fn()
194                             cx.expr_call(sp, cx.expr_path(cx.path(sp, vec![item.ident])), vec![]), // )
195                         ],
196                     ), // }
197                 ), // )
198             ],
199         )
200     };
201 
202     let mut test_const = cx.item(
203         sp,
204         Ident::new(item.ident.name, sp),
205         vec![
206             // #[cfg(test)]
207             cx.attribute(attr::mk_list_item(
208                 Ident::new(sym::cfg, attr_sp),
209                 vec![attr::mk_nested_word_item(Ident::new(sym::test, attr_sp))],
210             )),
211             // #[rustc_test_marker]
212             cx.attribute(cx.meta_word(attr_sp, sym::rustc_test_marker)),
213         ],
214         // const $ident: test::TestDescAndFn =
215         ast::ItemKind::Const(
216             ast::Defaultness::Final,
217             cx.ty(sp, ast::TyKind::Path(None, test_path("TestDescAndFn"))),
218             // test::TestDescAndFn {
219             Some(
220                 cx.expr_struct(
221                     sp,
222                     test_path("TestDescAndFn"),
223                     vec![
224                         // desc: test::TestDesc {
225                         field(
226                             "desc",
227                             cx.expr_struct(
228                                 sp,
229                                 test_path("TestDesc"),
230                                 vec![
231                                     // name: "path::to::test"
232                                     field(
233                                         "name",
234                                         cx.expr_call(
235                                             sp,
236                                             cx.expr_path(test_path("StaticTestName")),
237                                             vec![cx.expr_str(
238                                                 sp,
239                                                 Symbol::intern(&item_path(
240                                                     // skip the name of the root module
241                                                     &cx.current_expansion.module.mod_path[1..],
242                                                     &item.ident,
243                                                 )),
244                                             )],
245                                         ),
246                                     ),
247                                     // ignore: true | false
248                                     field(
249                                         "ignore",
250                                         cx.expr_bool(sp, should_ignore(&cx.sess, &item)),
251                                     ),
252                                     // allow_fail: true | false
253                                     field(
254                                         "allow_fail",
255                                         cx.expr_bool(sp, should_fail(&cx.sess, &item)),
256                                     ),
257                                     // compile_fail: true | false
258                                     field("compile_fail", cx.expr_bool(sp, false)),
259                                     // no_run: true | false
260                                     field("no_run", cx.expr_bool(sp, false)),
261                                     // should_panic: ...
262                                     field(
263                                         "should_panic",
264                                         match should_panic(cx, &item) {
265                                             // test::ShouldPanic::No
266                                             ShouldPanic::No => {
267                                                 cx.expr_path(should_panic_path("No"))
268                                             }
269                                             // test::ShouldPanic::Yes
270                                             ShouldPanic::Yes(None) => {
271                                                 cx.expr_path(should_panic_path("Yes"))
272                                             }
273                                             // test::ShouldPanic::YesWithMessage("...")
274                                             ShouldPanic::Yes(Some(sym)) => cx.expr_call(
275                                                 sp,
276                                                 cx.expr_path(should_panic_path("YesWithMessage")),
277                                                 vec![cx.expr_str(sp, sym)],
278                                             ),
279                                         },
280                                     ),
281                                     // test_type: ...
282                                     field(
283                                         "test_type",
284                                         match test_type(cx) {
285                                             // test::TestType::UnitTest
286                                             TestType::UnitTest => {
287                                                 cx.expr_path(test_type_path("UnitTest"))
288                                             }
289                                             // test::TestType::IntegrationTest
290                                             TestType::IntegrationTest => {
291                                                 cx.expr_path(test_type_path("IntegrationTest"))
292                                             }
293                                             // test::TestPath::Unknown
294                                             TestType::Unknown => {
295                                                 cx.expr_path(test_type_path("Unknown"))
296                                             }
297                                         },
298                                     ),
299                                     // },
300                                 ],
301                             ),
302                         ),
303                         // testfn: test::StaticTestFn(...) | test::StaticBenchFn(...)
304                         field("testfn", test_fn), // }
305                     ],
306                 ), // }
307             ),
308         ),
309     );
310     test_const = test_const.map(|mut tc| {
311         tc.vis.kind = ast::VisibilityKind::Public;
312         tc
313     });
314 
315     // extern crate test
316     let test_extern = cx.item(sp, test_id, vec![], ast::ItemKind::ExternCrate(None));
317 
318     tracing::debug!("synthetic test item:\n{}\n", pprust::item_to_string(&test_const));
319 
320     if is_stmt {
321         vec![
322             // Access to libtest under a hygienic name
323             Annotatable::Stmt(P(cx.stmt_item(sp, test_extern))),
324             // The generated test case
325             Annotatable::Stmt(P(cx.stmt_item(sp, test_const))),
326             // The original item
327             Annotatable::Stmt(P(cx.stmt_item(sp, item))),
328         ]
329     } else {
330         vec![
331             // Access to libtest under a hygienic name
332             Annotatable::Item(test_extern),
333             // The generated test case
334             Annotatable::Item(test_const),
335             // The original item
336             Annotatable::Item(item),
337         ]
338     }
339 }
340 
item_path(mod_path: &[Ident], item_ident: &Ident) -> String341 fn item_path(mod_path: &[Ident], item_ident: &Ident) -> String {
342     mod_path
343         .iter()
344         .chain(iter::once(item_ident))
345         .map(|x| x.to_string())
346         .collect::<Vec<String>>()
347         .join("::")
348 }
349 
350 enum ShouldPanic {
351     No,
352     Yes(Option<Symbol>),
353 }
354 
should_ignore(sess: &Session, i: &ast::Item) -> bool355 fn should_ignore(sess: &Session, i: &ast::Item) -> bool {
356     sess.contains_name(&i.attrs, sym::ignore)
357 }
358 
should_fail(sess: &Session, i: &ast::Item) -> bool359 fn should_fail(sess: &Session, i: &ast::Item) -> bool {
360     sess.contains_name(&i.attrs, sym::allow_fail)
361 }
362 
should_panic(cx: &ExtCtxt<'_>, i: &ast::Item) -> ShouldPanic363 fn should_panic(cx: &ExtCtxt<'_>, i: &ast::Item) -> ShouldPanic {
364     match cx.sess.find_by_name(&i.attrs, sym::should_panic) {
365         Some(attr) => {
366             let sd = &cx.sess.parse_sess.span_diagnostic;
367 
368             match attr.meta_item_list() {
369                 // Handle #[should_panic(expected = "foo")]
370                 Some(list) => {
371                     let msg = list
372                         .iter()
373                         .find(|mi| mi.has_name(sym::expected))
374                         .and_then(|mi| mi.meta_item())
375                         .and_then(|mi| mi.value_str());
376                     if list.len() != 1 || msg.is_none() {
377                         sd.struct_span_warn(
378                             attr.span,
379                             "argument must be of the form: \
380                              `expected = \"error message\"`",
381                         )
382                         .note(
383                             "errors in this attribute were erroneously \
384                                 allowed and will become a hard error in a \
385                                 future release",
386                         )
387                         .emit();
388                         ShouldPanic::Yes(None)
389                     } else {
390                         ShouldPanic::Yes(msg)
391                     }
392                 }
393                 // Handle #[should_panic] and #[should_panic = "expected"]
394                 None => ShouldPanic::Yes(attr.value_str()),
395             }
396         }
397         None => ShouldPanic::No,
398     }
399 }
400 
401 enum TestType {
402     UnitTest,
403     IntegrationTest,
404     Unknown,
405 }
406 
407 /// Attempts to determine the type of test.
408 /// Since doctests are created without macro expanding, only possible variants here
409 /// are `UnitTest`, `IntegrationTest` or `Unknown`.
test_type(cx: &ExtCtxt<'_>) -> TestType410 fn test_type(cx: &ExtCtxt<'_>) -> TestType {
411     // Root path from context contains the topmost sources directory of the crate.
412     // I.e., for `project` with sources in `src` and tests in `tests` folders
413     // (no matter how many nested folders lie inside),
414     // there will be two different root paths: `/project/src` and `/project/tests`.
415     let crate_path = cx.root_path.as_path();
416 
417     if crate_path.ends_with("src") {
418         // `/src` folder contains unit-tests.
419         TestType::UnitTest
420     } else if crate_path.ends_with("tests") {
421         // `/tests` folder contains integration tests.
422         TestType::IntegrationTest
423     } else {
424         // Crate layout doesn't match expected one, test type is unknown.
425         TestType::Unknown
426     }
427 }
428 
has_test_signature(cx: &ExtCtxt<'_>, i: &ast::Item) -> bool429 fn has_test_signature(cx: &ExtCtxt<'_>, i: &ast::Item) -> bool {
430     let has_should_panic_attr = cx.sess.contains_name(&i.attrs, sym::should_panic);
431     let sd = &cx.sess.parse_sess.span_diagnostic;
432     if let ast::ItemKind::Fn(box ast::Fn { ref sig, ref generics, .. }) = i.kind {
433         if let ast::Unsafe::Yes(span) = sig.header.unsafety {
434             sd.struct_span_err(i.span, "unsafe functions cannot be used for tests")
435                 .span_label(span, "`unsafe` because of this")
436                 .emit();
437             return false;
438         }
439         if let ast::Async::Yes { span, .. } = sig.header.asyncness {
440             sd.struct_span_err(i.span, "async functions cannot be used for tests")
441                 .span_label(span, "`async` because of this")
442                 .emit();
443             return false;
444         }
445 
446         // If the termination trait is active, the compiler will check that the output
447         // type implements the `Termination` trait as `libtest` enforces that.
448         let has_output = match sig.decl.output {
449             ast::FnRetTy::Default(..) => false,
450             ast::FnRetTy::Ty(ref t) if t.kind.is_unit() => false,
451             _ => true,
452         };
453 
454         if !sig.decl.inputs.is_empty() {
455             sd.span_err(i.span, "functions used as tests can not have any arguments");
456             return false;
457         }
458 
459         match (has_output, has_should_panic_attr) {
460             (true, true) => {
461                 sd.span_err(i.span, "functions using `#[should_panic]` must return `()`");
462                 false
463             }
464             (true, false) => {
465                 if !generics.params.is_empty() {
466                     sd.span_err(i.span, "functions used as tests must have signature fn() -> ()");
467                     false
468                 } else {
469                     true
470                 }
471             }
472             (false, _) => true,
473         }
474     } else {
475         sd.span_err(i.span, "only functions may be used as tests");
476         false
477     }
478 }
479 
has_bench_signature(cx: &ExtCtxt<'_>, i: &ast::Item) -> bool480 fn has_bench_signature(cx: &ExtCtxt<'_>, i: &ast::Item) -> bool {
481     let has_sig = if let ast::ItemKind::Fn(box ast::Fn { ref sig, .. }) = i.kind {
482         // N.B., inadequate check, but we're running
483         // well before resolve, can't get too deep.
484         sig.decl.inputs.len() == 1
485     } else {
486         false
487     };
488 
489     if !has_sig {
490         cx.sess.parse_sess.span_diagnostic.span_err(
491             i.span,
492             "functions used as benches must have \
493             signature `fn(&mut Bencher) -> impl Termination`",
494         );
495     }
496 
497     has_sig
498 }
499