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    let sp = ecx.with_def_site_ctxt(attr_sp);
38    let (mut item, is_stmt) = match anno_item {
39        Annotatable::Item(item) => (item, false),
40        Annotatable::Stmt(stmt) if let ast::StmtKind::Item(_) = stmt.kind => {
41            if let ast::StmtKind::Item(i) = stmt.kind {
42                (i, true)
43            } else {
44                unreachable!()
45            }
46        }
47        _ => {
48            ecx.dcx().emit_err(errors::TestCaseNonItem { span: anno_item.span() });
49            return vec![];
50        }
51    };
52
53    if !ecx.ecfg.should_test {
54        return vec![];
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    let (item, is_stmt) = match item {
117        Annotatable::Item(i) => (i, false),
118        Annotatable::Stmt(box ast::Stmt { kind: ast::StmtKind::Item(i), .. }) => (i, true),
119        other => {
120            not_testable_error(cx, is_bench, attr_sp, None);
121            return vec![other];
122        }
123    };
124
125    let ast::ItemKind::Fn(fn_) = &item.kind else {
126        not_testable_error(cx, is_bench, attr_sp, Some(&item));
127        return if is_stmt {
128            vec![Annotatable::Stmt(Box::new(cx.stmt_item(item.span, item)))]
129        } else {
130            vec![Annotatable::Item(item)]
131        };
132    };
133
134    // If we're not in test configuration, remove the annotated item
135    if !cx.ecfg.should_test {
136        return vec![];
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        // avoid name collisions by using the function name within the identifier, see bug #148275
211        let bencher_param =
212            Ident::from_str_and_span(&format!("__bench_{}", fn_.ident.name), attr_sp);
213        cx.expr_call(
214            sp,
215            cx.expr_path(test_path("StaticBenchFn")),
216            thin_vec![
217                // #[coverage(off)]
218                // |__bench_fn_name| 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(__bench_fn_name)
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, bencher_param)],
230                            ),
231                        ],
232                    ),
233                    bencher_param,
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                    rhs: Some(ast::ConstItemRhs::Body(
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<'_>, is_bench: bool, attr_sp: Span, item: Option<&ast::Item>) {
409    let dcx = cx.dcx();
410    let name = if is_bench { "bench" } else { "test" };
411    let msg = format!("the `#[{name}]` attribute may only be used on a free function");
412    let level = match item.map(|i| &i.kind) {
413        // These were a warning before #92959 and need to continue being that to avoid breaking
414        // stable user code (#94508).
415        Some(ast::ItemKind::MacCall(_)) => Level::Warning,
416        _ => Level::Error,
417    };
418    let mut err = Diag::<()>::new(dcx, level, msg);
419    err.span(attr_sp);
420    if let Some(item) = item {
421        err.span_label(
422            item.span,
423            format!(
424                "expected a non-associated function, found {} {}",
425                item.kind.article(),
426                item.kind.descr()
427            ),
428        );
429    }
430    err.span_label(attr_sp, format!("the `#[{name}]` macro causes a function to be run as a test and has no effect on non-functions"));
431
432    if !is_bench {
433        err.with_span_suggestion(attr_sp,
434            "replace with conditional compilation to make the item only exist when tests are being run",
435            "#[cfg(test)]",
436            Applicability::MaybeIncorrect).emit();
437    } else {
438        err.emit();
439    }
440}
441
442fn get_location_info(cx: &ExtCtxt<'_>, fn_: &ast::Fn) -> (Symbol, usize, usize, usize, usize) {
443    let span = fn_.ident.span;
444    let (source_file, lo_line, lo_col, hi_line, hi_col) =
445        cx.sess.source_map().span_to_location_info(span);
446
447    let file_name = match source_file {
448        Some(sf) => sf.name.display(FileNameDisplayPreference::Remapped).to_string(),
449        None => "no-location".to_string(),
450    };
451
452    (Symbol::intern(&file_name), lo_line, lo_col, hi_line, hi_col)
453}
454
455fn item_path(mod_path: &[Ident], item_ident: &Ident) -> String {
456    join_path_idents(mod_path.iter().chain(iter::once(item_ident)))
457}
458
459enum ShouldPanic {
460    No,
461    Yes(Option<Symbol>),
462}
463
464fn should_ignore(i: &ast::Item) -> bool {
465    attr::contains_name(&i.attrs, sym::ignore)
466}
467
468fn should_ignore_message(i: &ast::Item) -> Option<Symbol> {
469    match attr::find_by_name(&i.attrs, sym::ignore) {
470        Some(attr) => {
471            match attr.meta_item_list() {
472                // Handle #[ignore(bar = "foo")]
473                Some(_) => None,
474                // Handle #[ignore] and #[ignore = "message"]
475                None => attr.value_str(),
476            }
477        }
478        None => None,
479    }
480}
481
482fn should_panic(cx: &ExtCtxt<'_>, i: &ast::Item) -> ShouldPanic {
483    if let Some(Attribute::Parsed(AttributeKind::ShouldPanic { reason, .. })) =
484        AttributeParser::parse_limited(
485            cx.sess,
486            &i.attrs,
487            sym::should_panic,
488            i.span,
489            i.node_id(),
490            None,
491        )
492    {
493        ShouldPanic::Yes(reason)
494    } else {
495        ShouldPanic::No
496    }
497}
498
499enum TestType {
500    UnitTest,
501    IntegrationTest,
502    Unknown,
503}
504
505/// Attempts to determine the type of test.
506/// Since doctests are created without macro expanding, only possible variants here
507/// are `UnitTest`, `IntegrationTest` or `Unknown`.
508fn test_type(cx: &ExtCtxt<'_>) -> TestType {
509    // Root path from context contains the topmost sources directory of the crate.
510    // I.e., for `project` with sources in `src` and tests in `tests` folders
511    // (no matter how many nested folders lie inside),
512    // there will be two different root paths: `/project/src` and `/project/tests`.
513    let crate_path = cx.root_path.as_path();
514
515    if crate_path.ends_with("src") {
516        // `/src` folder contains unit-tests.
517        TestType::UnitTest
518    } else if crate_path.ends_with("tests") {
519        // `/tests` folder contains integration tests.
520        TestType::IntegrationTest
521    } else {
522        // Crate layout doesn't match expected one, test type is unknown.
523        TestType::Unknown
524    }
525}
526
527fn check_test_signature(
528    cx: &ExtCtxt<'_>,
529    i: &ast::Item,
530    f: &ast::Fn,
531) -> Result<(), ErrorGuaranteed> {
532    let has_should_panic_attr = attr::contains_name(&i.attrs, sym::should_panic);
533    let dcx = cx.dcx();
534
535    if let ast::Safety::Unsafe(span) = f.sig.header.safety {
536        return Err(dcx.emit_err(errors::TestBadFn { span: i.span, cause: span, kind: "unsafe" }));
537    }
538
539    if let Some(coroutine_kind) = f.sig.header.coroutine_kind {
540        match coroutine_kind {
541            ast::CoroutineKind::Async { span, .. } => {
542                return Err(dcx.emit_err(errors::TestBadFn {
543                    span: i.span,
544                    cause: span,
545                    kind: "async",
546                }));
547            }
548            ast::CoroutineKind::Gen { span, .. } => {
549                return Err(dcx.emit_err(errors::TestBadFn {
550                    span: i.span,
551                    cause: span,
552                    kind: "gen",
553                }));
554            }
555            ast::CoroutineKind::AsyncGen { span, .. } => {
556                return Err(dcx.emit_err(errors::TestBadFn {
557                    span: i.span,
558                    cause: span,
559                    kind: "async gen",
560                }));
561            }
562        }
563    }
564
565    // If the termination trait is active, the compiler will check that the output
566    // type implements the `Termination` trait as `libtest` enforces that.
567    let has_output = match &f.sig.decl.output {
568        ast::FnRetTy::Default(..) => false,
569        ast::FnRetTy::Ty(t) if t.kind.is_unit() => false,
570        _ => true,
571    };
572
573    if !f.sig.decl.inputs.is_empty() {
574        return Err(dcx.span_err(i.span, "functions used as tests can not have any arguments"));
575    }
576
577    if has_should_panic_attr && has_output {
578        return Err(dcx.span_err(i.span, "functions using `#[should_panic]` must return `()`"));
579    }
580
581    if f.generics.params.iter().any(|param| !matches!(param.kind, GenericParamKind::Lifetime)) {
582        return Err(dcx.span_err(
583            i.span,
584            "functions used as tests can not have any non-lifetime generic parameters",
585        ));
586    }
587
588    Ok(())
589}
590
591fn check_bench_signature(
592    cx: &ExtCtxt<'_>,
593    i: &ast::Item,
594    f: &ast::Fn,
595) -> Result<(), ErrorGuaranteed> {
596    // N.B., inadequate check, but we're running
597    // well before resolve, can't get too deep.
598    if f.sig.decl.inputs.len() != 1 {
599        return Err(cx.dcx().emit_err(errors::BenchSig { span: i.span }));
600    }
601    Ok(())
602}