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