rustc_builtin_macros/
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
4use std::assert_matches::assert_matches;
5use std::iter;
6
7use rustc_ast::{self as ast, GenericParamKind, HasNodeId, attr, join_path_idents};
8use rustc_ast_pretty::pprust;
9use rustc_attr_parsing::AttributeParser;
10use rustc_errors::{Applicability, Diag, Level};
11use rustc_expand::base::*;
12use rustc_hir::Attribute;
13use rustc_hir::attrs::AttributeKind;
14use rustc_span::{ErrorGuaranteed, FileNameDisplayPreference, Ident, Span, Symbol, sym};
15use thin_vec::{ThinVec, thin_vec};
16use tracing::debug;
17
18use crate::errors;
19use crate::util::{check_builtin_macro_attribute, warn_on_duplicate_attribute};
20
21/// #[test_case] is used by custom test authors to mark tests
22/// When building for test, it needs to make the item public and gensym the name
23/// Otherwise, we'll omit the item. This behavior means that any item annotated
24/// with #[test_case] is never addressable.
25///
26/// We mark item with an inert attribute "rustc_test_marker" which the test generation
27/// logic will pick up on.
28pub(crate) fn expand_test_case(
29    ecx: &mut ExtCtxt<'_>,
30    attr_sp: Span,
31    meta_item: &ast::MetaItem,
32    anno_item: Annotatable,
33) -> Vec<Annotatable> {
34    check_builtin_macro_attribute(ecx, meta_item, sym::test_case);
35    warn_on_duplicate_attribute(ecx, &anno_item, sym::test_case);
36
37    if !ecx.ecfg.should_test {
38        return vec![];
39    }
40
41    let sp = ecx.with_def_site_ctxt(attr_sp);
42    let (mut item, is_stmt) = match anno_item {
43        Annotatable::Item(item) => (item, false),
44        Annotatable::Stmt(stmt) if let ast::StmtKind::Item(_) = stmt.kind => {
45            if let ast::StmtKind::Item(i) = stmt.kind {
46                (i, true)
47            } else {
48                unreachable!()
49            }
50        }
51        _ => {
52            ecx.dcx().emit_err(errors::TestCaseNonItem { span: anno_item.span() });
53            return vec![];
54        }
55    };
56
57    // `#[test_case]` is valid on functions, consts, and statics. Only modify
58    // the item in those cases.
59    match &mut item.kind {
60        ast::ItemKind::Fn(box ast::Fn { ident, .. })
61        | ast::ItemKind::Const(box ast::ConstItem { ident, .. })
62        | ast::ItemKind::Static(box ast::StaticItem { ident, .. }) => {
63            ident.span = ident.span.with_ctxt(sp.ctxt());
64            let test_path_symbol = Symbol::intern(&item_path(
65                // skip the name of the root module
66                &ecx.current_expansion.module.mod_path[1..],
67                ident,
68            ));
69            item.vis = ast::Visibility {
70                span: item.vis.span,
71                kind: ast::VisibilityKind::Public,
72                tokens: None,
73            };
74            item.attrs.push(ecx.attr_name_value_str(sym::rustc_test_marker, test_path_symbol, sp));
75        }
76        _ => {}
77    }
78
79    let ret = if is_stmt {
80        Annotatable::Stmt(Box::new(ecx.stmt_item(item.span, item)))
81    } else {
82        Annotatable::Item(item)
83    };
84
85    vec![ret]
86}
87
88pub(crate) fn expand_test(
89    cx: &mut ExtCtxt<'_>,
90    attr_sp: Span,
91    meta_item: &ast::MetaItem,
92    item: Annotatable,
93) -> Vec<Annotatable> {
94    check_builtin_macro_attribute(cx, meta_item, sym::test);
95    warn_on_duplicate_attribute(cx, &item, sym::test);
96    expand_test_or_bench(cx, attr_sp, item, false)
97}
98
99pub(crate) fn expand_bench(
100    cx: &mut ExtCtxt<'_>,
101    attr_sp: Span,
102    meta_item: &ast::MetaItem,
103    item: Annotatable,
104) -> Vec<Annotatable> {
105    check_builtin_macro_attribute(cx, meta_item, sym::bench);
106    warn_on_duplicate_attribute(cx, &item, sym::bench);
107    expand_test_or_bench(cx, attr_sp, item, true)
108}
109
110pub(crate) fn expand_test_or_bench(
111    cx: &ExtCtxt<'_>,
112    attr_sp: Span,
113    item: Annotatable,
114    is_bench: bool,
115) -> Vec<Annotatable> {
116    // If we're not in test configuration, remove the annotated item
117    if !cx.ecfg.should_test {
118        return vec![];
119    }
120
121    let (item, is_stmt) = match item {
122        Annotatable::Item(i) => (i, false),
123        Annotatable::Stmt(box ast::Stmt { kind: ast::StmtKind::Item(i), .. }) => (i, true),
124        other => {
125            not_testable_error(cx, attr_sp, None);
126            return vec![other];
127        }
128    };
129
130    let ast::ItemKind::Fn(fn_) = &item.kind else {
131        not_testable_error(cx, attr_sp, Some(&item));
132        return if is_stmt {
133            vec![Annotatable::Stmt(Box::new(cx.stmt_item(item.span, item)))]
134        } else {
135            vec![Annotatable::Item(item)]
136        };
137    };
138
139    if let Some(attr) = attr::find_by_name(&item.attrs, sym::naked) {
140        cx.dcx().emit_err(errors::NakedFunctionTestingAttribute {
141            testing_span: attr_sp,
142            naked_span: attr.span,
143        });
144        return vec![Annotatable::Item(item)];
145    }
146
147    // check_*_signature will report any errors in the type so compilation
148    // will fail. We shouldn't try to expand in this case because the errors
149    // would be spurious.
150    let check_result = if is_bench {
151        check_bench_signature(cx, &item, fn_)
152    } else {
153        check_test_signature(cx, &item, fn_)
154    };
155    if check_result.is_err() {
156        return if is_stmt {
157            vec![Annotatable::Stmt(Box::new(cx.stmt_item(item.span, item)))]
158        } else {
159            vec![Annotatable::Item(item)]
160        };
161    }
162
163    let sp = cx.with_def_site_ctxt(item.span);
164    let ret_ty_sp = cx.with_def_site_ctxt(fn_.sig.decl.output.span());
165    let attr_sp = cx.with_def_site_ctxt(attr_sp);
166
167    let test_ident = Ident::new(sym::test, attr_sp);
168
169    // creates test::$name
170    let test_path = |name| cx.path(ret_ty_sp, vec![test_ident, Ident::from_str_and_span(name, sp)]);
171
172    // creates test::ShouldPanic::$name
173    let should_panic_path = |name| {
174        cx.path(
175            sp,
176            vec![
177                test_ident,
178                Ident::from_str_and_span("ShouldPanic", sp),
179                Ident::from_str_and_span(name, sp),
180            ],
181        )
182    };
183
184    // creates test::TestType::$name
185    let test_type_path = |name| {
186        cx.path(
187            sp,
188            vec![
189                test_ident,
190                Ident::from_str_and_span("TestType", sp),
191                Ident::from_str_and_span(name, sp),
192            ],
193        )
194    };
195
196    // creates $name: $expr
197    let field = |name, expr| cx.field_imm(sp, Ident::from_str_and_span(name, sp), expr);
198
199    // Adds `#[coverage(off)]` to a closure, so it won't be instrumented in
200    // `-Cinstrument-coverage` builds.
201    // This requires `#[allow_internal_unstable(coverage_attribute)]` on the
202    // corresponding macro declaration in `core::macros`.
203    let coverage_off = |mut expr: Box<ast::Expr>| {
204        assert_matches!(expr.kind, ast::ExprKind::Closure(_));
205        expr.attrs.push(cx.attr_nested_word(sym::coverage, sym::off, sp));
206        expr
207    };
208
209    let test_fn = if is_bench {
210        // A simple ident for a lambda
211        let b = Ident::from_str_and_span("b", attr_sp);
212
213        cx.expr_call(
214            sp,
215            cx.expr_path(test_path("StaticBenchFn")),
216            thin_vec![
217                // #[coverage(off)]
218                // |b| self::test::assert_test_result(
219                coverage_off(cx.lambda1(
220                    sp,
221                    cx.expr_call(
222                        sp,
223                        cx.expr_path(test_path("assert_test_result")),
224                        thin_vec![
225                            // super::$test_fn(b)
226                            cx.expr_call(
227                                ret_ty_sp,
228                                cx.expr_path(cx.path(sp, vec![fn_.ident])),
229                                thin_vec![cx.expr_ident(sp, b)],
230                            ),
231                        ],
232                    ),
233                    b,
234                )), // )
235            ],
236        )
237    } else {
238        cx.expr_call(
239            sp,
240            cx.expr_path(test_path("StaticTestFn")),
241            thin_vec![
242                // #[coverage(off)]
243                // || {
244                coverage_off(cx.lambda0(
245                    sp,
246                    // test::assert_test_result(
247                    cx.expr_call(
248                        sp,
249                        cx.expr_path(test_path("assert_test_result")),
250                        thin_vec![
251                            // $test_fn()
252                            cx.expr_call(
253                                ret_ty_sp,
254                                cx.expr_path(cx.path(sp, vec![fn_.ident])),
255                                ThinVec::new(),
256                            ), // )
257                        ],
258                    ), // }
259                )), // )
260            ],
261        )
262    };
263
264    let test_path_symbol = Symbol::intern(&item_path(
265        // skip the name of the root module
266        &cx.current_expansion.module.mod_path[1..],
267        &fn_.ident,
268    ));
269
270    let location_info = get_location_info(cx, &fn_);
271
272    let mut test_const =
273        cx.item(
274            sp,
275            thin_vec![
276                // #[cfg(test)]
277                cx.attr_nested_word(sym::cfg, sym::test, attr_sp),
278                // #[rustc_test_marker = "test_case_sort_key"]
279                cx.attr_name_value_str(sym::rustc_test_marker, test_path_symbol, attr_sp),
280                // #[doc(hidden)]
281                cx.attr_nested_word(sym::doc, sym::hidden, attr_sp),
282            ],
283            // const $ident: test::TestDescAndFn =
284            ast::ItemKind::Const(
285                ast::ConstItem {
286                    defaultness: ast::Defaultness::Final,
287                    ident: Ident::new(fn_.ident.name, sp),
288                    generics: ast::Generics::default(),
289                    ty: cx.ty(sp, ast::TyKind::Path(None, test_path("TestDescAndFn"))),
290                    define_opaque: None,
291                    // test::TestDescAndFn {
292                    expr: Some(
293                        cx.expr_struct(
294                            sp,
295                            test_path("TestDescAndFn"),
296                            thin_vec![
297                        // desc: test::TestDesc {
298                        field(
299                            "desc",
300                            cx.expr_struct(sp, test_path("TestDesc"), thin_vec![
301                                // name: "path::to::test"
302                                field(
303                                    "name",
304                                    cx.expr_call(
305                                        sp,
306                                        cx.expr_path(test_path("StaticTestName")),
307                                        thin_vec![cx.expr_str(sp, test_path_symbol)],
308                                    ),
309                                ),
310                                // ignore: true | false
311                                field("ignore", cx.expr_bool(sp, should_ignore(&item)),),
312                                // ignore_message: Some("...") | None
313                                field(
314                                    "ignore_message",
315                                    if let Some(msg) = should_ignore_message(&item) {
316                                        cx.expr_some(sp, cx.expr_str(sp, msg))
317                                    } else {
318                                        cx.expr_none(sp)
319                                    },
320                                ),
321                                // source_file: <relative_path_of_source_file>
322                                field("source_file", cx.expr_str(sp, location_info.0)),
323                                // start_line: start line of the test fn identifier.
324                                field("start_line", cx.expr_usize(sp, location_info.1)),
325                                // start_col: start column of the test fn identifier.
326                                field("start_col", cx.expr_usize(sp, location_info.2)),
327                                // end_line: end line of the test fn identifier.
328                                field("end_line", cx.expr_usize(sp, location_info.3)),
329                                // end_col: end column of the test fn identifier.
330                                field("end_col", cx.expr_usize(sp, location_info.4)),
331                                // compile_fail: true | false
332                                field("compile_fail", cx.expr_bool(sp, false)),
333                                // no_run: true | false
334                                field("no_run", cx.expr_bool(sp, false)),
335                                // should_panic: ...
336                                field("should_panic", match should_panic(cx, &item) {
337                                    // test::ShouldPanic::No
338                                    ShouldPanic::No => {
339                                        cx.expr_path(should_panic_path("No"))
340                                    }
341                                    // test::ShouldPanic::Yes
342                                    ShouldPanic::Yes(None) => {
343                                        cx.expr_path(should_panic_path("Yes"))
344                                    }
345                                    // test::ShouldPanic::YesWithMessage("...")
346                                    ShouldPanic::Yes(Some(sym)) => cx.expr_call(
347                                        sp,
348                                        cx.expr_path(should_panic_path("YesWithMessage")),
349                                        thin_vec![cx.expr_str(sp, sym)],
350                                    ),
351                                },),
352                                // test_type: ...
353                                field("test_type", match test_type(cx) {
354                                    // test::TestType::UnitTest
355                                    TestType::UnitTest => {
356                                        cx.expr_path(test_type_path("UnitTest"))
357                                    }
358                                    // test::TestType::IntegrationTest
359                                    TestType::IntegrationTest => {
360                                        cx.expr_path(test_type_path("IntegrationTest"))
361                                    }
362                                    // test::TestPath::Unknown
363                                    TestType::Unknown => {
364                                        cx.expr_path(test_type_path("Unknown"))
365                                    }
366                                },),
367                                // },
368                            ],),
369                        ),
370                        // testfn: test::StaticTestFn(...) | test::StaticBenchFn(...)
371                        field("testfn", test_fn), // }
372                    ],
373                        ), // }
374                    ),
375                }
376                .into(),
377            ),
378        );
379    test_const.vis.kind = ast::VisibilityKind::Public;
380
381    // extern crate test
382    let test_extern =
383        cx.item(sp, ast::AttrVec::new(), ast::ItemKind::ExternCrate(None, test_ident));
384
385    debug!("synthetic test item:\n{}\n", pprust::item_to_string(&test_const));
386
387    if is_stmt {
388        vec![
389            // Access to libtest under a hygienic name
390            Annotatable::Stmt(Box::new(cx.stmt_item(sp, test_extern))),
391            // The generated test case
392            Annotatable::Stmt(Box::new(cx.stmt_item(sp, test_const))),
393            // The original item
394            Annotatable::Stmt(Box::new(cx.stmt_item(sp, item))),
395        ]
396    } else {
397        vec![
398            // Access to libtest under a hygienic name
399            Annotatable::Item(test_extern),
400            // The generated test case
401            Annotatable::Item(test_const),
402            // The original item
403            Annotatable::Item(item),
404        ]
405    }
406}
407
408fn not_testable_error(cx: &ExtCtxt<'_>, attr_sp: Span, item: Option<&ast::Item>) {
409    let dcx = cx.dcx();
410    let msg = "the `#[test]` attribute may only be used on a non-associated function";
411    let level = match item.map(|i| &i.kind) {
412        // These were a warning before #92959 and need to continue being that to avoid breaking
413        // stable user code (#94508).
414        Some(ast::ItemKind::MacCall(_)) => Level::Warning,
415        _ => Level::Error,
416    };
417    let mut err = Diag::<()>::new(dcx, level, msg);
418    err.span(attr_sp);
419    if let Some(item) = item {
420        err.span_label(
421            item.span,
422            format!(
423                "expected a non-associated function, found {} {}",
424                item.kind.article(),
425                item.kind.descr()
426            ),
427        );
428    }
429    err.with_span_label(attr_sp, "the `#[test]` macro causes a function to be run as a test and has no effect on non-functions")
430        .with_span_suggestion(attr_sp,
431            "replace with conditional compilation to make the item only exist when tests are being run",
432            "#[cfg(test)]",
433            Applicability::MaybeIncorrect)
434        .emit();
435}
436
437fn get_location_info(cx: &ExtCtxt<'_>, fn_: &ast::Fn) -> (Symbol, usize, usize, usize, usize) {
438    let span = fn_.ident.span;
439    let (source_file, lo_line, lo_col, hi_line, hi_col) =
440        cx.sess.source_map().span_to_location_info(span);
441
442    let file_name = match source_file {
443        Some(sf) => sf.name.display(FileNameDisplayPreference::Remapped).to_string(),
444        None => "no-location".to_string(),
445    };
446
447    (Symbol::intern(&file_name), lo_line, lo_col, hi_line, hi_col)
448}
449
450fn item_path(mod_path: &[Ident], item_ident: &Ident) -> String {
451    join_path_idents(mod_path.iter().chain(iter::once(item_ident)))
452}
453
454enum ShouldPanic {
455    No,
456    Yes(Option<Symbol>),
457}
458
459fn should_ignore(i: &ast::Item) -> bool {
460    attr::contains_name(&i.attrs, sym::ignore)
461}
462
463fn should_ignore_message(i: &ast::Item) -> Option<Symbol> {
464    match attr::find_by_name(&i.attrs, sym::ignore) {
465        Some(attr) => {
466            match attr.meta_item_list() {
467                // Handle #[ignore(bar = "foo")]
468                Some(_) => None,
469                // Handle #[ignore] and #[ignore = "message"]
470                None => attr.value_str(),
471            }
472        }
473        None => None,
474    }
475}
476
477fn should_panic(cx: &ExtCtxt<'_>, i: &ast::Item) -> ShouldPanic {
478    if let Some(Attribute::Parsed(AttributeKind::ShouldPanic { reason, .. })) =
479        AttributeParser::parse_limited(
480            cx.sess,
481            &i.attrs,
482            sym::should_panic,
483            i.span,
484            i.node_id(),
485            None,
486        )
487    {
488        ShouldPanic::Yes(reason)
489    } else {
490        ShouldPanic::No
491    }
492}
493
494enum TestType {
495    UnitTest,
496    IntegrationTest,
497    Unknown,
498}
499
500/// Attempts to determine the type of test.
501/// Since doctests are created without macro expanding, only possible variants here
502/// are `UnitTest`, `IntegrationTest` or `Unknown`.
503fn test_type(cx: &ExtCtxt<'_>) -> TestType {
504    // Root path from context contains the topmost sources directory of the crate.
505    // I.e., for `project` with sources in `src` and tests in `tests` folders
506    // (no matter how many nested folders lie inside),
507    // there will be two different root paths: `/project/src` and `/project/tests`.
508    let crate_path = cx.root_path.as_path();
509
510    if crate_path.ends_with("src") {
511        // `/src` folder contains unit-tests.
512        TestType::UnitTest
513    } else if crate_path.ends_with("tests") {
514        // `/tests` folder contains integration tests.
515        TestType::IntegrationTest
516    } else {
517        // Crate layout doesn't match expected one, test type is unknown.
518        TestType::Unknown
519    }
520}
521
522fn check_test_signature(
523    cx: &ExtCtxt<'_>,
524    i: &ast::Item,
525    f: &ast::Fn,
526) -> Result<(), ErrorGuaranteed> {
527    let has_should_panic_attr = attr::contains_name(&i.attrs, sym::should_panic);
528    let dcx = cx.dcx();
529
530    if let ast::Safety::Unsafe(span) = f.sig.header.safety {
531        return Err(dcx.emit_err(errors::TestBadFn { span: i.span, cause: span, kind: "unsafe" }));
532    }
533
534    if let Some(coroutine_kind) = f.sig.header.coroutine_kind {
535        match coroutine_kind {
536            ast::CoroutineKind::Async { span, .. } => {
537                return Err(dcx.emit_err(errors::TestBadFn {
538                    span: i.span,
539                    cause: span,
540                    kind: "async",
541                }));
542            }
543            ast::CoroutineKind::Gen { span, .. } => {
544                return Err(dcx.emit_err(errors::TestBadFn {
545                    span: i.span,
546                    cause: span,
547                    kind: "gen",
548                }));
549            }
550            ast::CoroutineKind::AsyncGen { span, .. } => {
551                return Err(dcx.emit_err(errors::TestBadFn {
552                    span: i.span,
553                    cause: span,
554                    kind: "async gen",
555                }));
556            }
557        }
558    }
559
560    // If the termination trait is active, the compiler will check that the output
561    // type implements the `Termination` trait as `libtest` enforces that.
562    let has_output = match &f.sig.decl.output {
563        ast::FnRetTy::Default(..) => false,
564        ast::FnRetTy::Ty(t) if t.kind.is_unit() => false,
565        _ => true,
566    };
567
568    if !f.sig.decl.inputs.is_empty() {
569        return Err(dcx.span_err(i.span, "functions used as tests can not have any arguments"));
570    }
571
572    if has_should_panic_attr && has_output {
573        return Err(dcx.span_err(i.span, "functions using `#[should_panic]` must return `()`"));
574    }
575
576    if f.generics.params.iter().any(|param| !matches!(param.kind, GenericParamKind::Lifetime)) {
577        return Err(dcx.span_err(
578            i.span,
579            "functions used as tests can not have any non-lifetime generic parameters",
580        ));
581    }
582
583    Ok(())
584}
585
586fn check_bench_signature(
587    cx: &ExtCtxt<'_>,
588    i: &ast::Item,
589    f: &ast::Fn,
590) -> Result<(), ErrorGuaranteed> {
591    // N.B., inadequate check, but we're running
592    // well before resolve, can't get too deep.
593    if f.sig.decl.inputs.len() != 1 {
594        return Err(cx.dcx().emit_err(errors::BenchSig { span: i.span }));
595    }
596    Ok(())
597}