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