2 /// The expansion from a test function to the appropriate test struct for libtest
3 /// Ideally, this code would be in libtest but for efficiency and error messages it lives here.
4 use crate::util
::{check_builtin_macro_attribute, warn_on_duplicate_attribute}
;
6 use rustc_ast
::{self as ast, attr, GenericParamKind}
;
7 use rustc_ast_pretty
::pprust
;
8 use rustc_errors
::Applicability
;
9 use rustc_expand
::base
::*;
10 use rustc_span
::symbol
::{sym, Ident, Symbol}
;
11 use rustc_span
::{ErrorGuaranteed, FileNameDisplayPreference, Span}
;
13 use thin_vec
::{thin_vec, ThinVec}
;
15 /// #[test_case] is used by custom test authors to mark tests
16 /// When building for test, it needs to make the item public and gensym the name
17 /// Otherwise, we'll omit the item. This behavior means that any item annotated
18 /// with #[test_case] is never addressable.
20 /// We mark item with an inert attribute "rustc_test_marker" which the test generation
21 /// logic will pick up on.
22 pub fn expand_test_case(
23 ecx
: &mut ExtCtxt
<'_
>,
25 meta_item
: &ast
::MetaItem
,
26 anno_item
: Annotatable
,
27 ) -> Vec
<Annotatable
> {
28 check_builtin_macro_attribute(ecx
, meta_item
, sym
::test_case
);
29 warn_on_duplicate_attribute(&ecx
, &anno_item
, sym
::test_case
);
31 if !ecx
.ecfg
.should_test
{
35 let sp
= ecx
.with_def_site_ctxt(attr_sp
);
36 let (mut item
, is_stmt
) = match anno_item
{
37 Annotatable
::Item(item
) => (item
, false),
38 Annotatable
::Stmt(stmt
) if let ast
::StmtKind
::Item(_
) = stmt
.kind
=> {
39 if let ast
::StmtKind
::Item(i
) = stmt
.into_inner().kind
{
46 ecx
.emit_err(errors
::TestCaseNonItem { span: anno_item.span() }
);
50 item
= item
.map(|mut item
| {
51 let test_path_symbol
= Symbol
::intern(&item_path(
52 // skip the name of the root module
53 &ecx
.current_expansion
.module
.mod_path
[1..],
56 item
.vis
= ast
::Visibility
{
58 kind
: ast
::VisibilityKind
::Public
,
61 item
.ident
.span
= item
.ident
.span
.with_ctxt(sp
.ctxt());
62 item
.attrs
.push(ecx
.attr_name_value_str(sym
::rustc_test_marker
, test_path_symbol
, sp
));
66 let ret
= if is_stmt
{
67 Annotatable
::Stmt(P(ecx
.stmt_item(item
.span
, item
)))
69 Annotatable
::Item(item
)
78 meta_item
: &ast
::MetaItem
,
80 ) -> Vec
<Annotatable
> {
81 check_builtin_macro_attribute(cx
, meta_item
, sym
::test
);
82 warn_on_duplicate_attribute(&cx
, &item
, sym
::test
);
83 expand_test_or_bench(cx
, attr_sp
, item
, false)
89 meta_item
: &ast
::MetaItem
,
91 ) -> Vec
<Annotatable
> {
92 check_builtin_macro_attribute(cx
, meta_item
, sym
::bench
);
93 warn_on_duplicate_attribute(&cx
, &item
, sym
::bench
);
94 expand_test_or_bench(cx
, attr_sp
, item
, true)
97 pub fn expand_test_or_bench(
102 ) -> Vec
<Annotatable
> {
103 // If we're not in test configuration, remove the annotated item
104 if !cx
.ecfg
.should_test
{
108 let (item
, is_stmt
) = match item
{
109 Annotatable
::Item(i
) => (i
, false),
110 Annotatable
::Stmt(stmt
) if matches
!(stmt
.kind
, ast
::StmtKind
::Item(_
)) => {
111 // FIXME: Use an 'if let' guard once they are implemented
112 if let ast
::StmtKind
::Item(i
) = stmt
.into_inner().kind
{
119 not_testable_error(cx
, attr_sp
, None
);
124 let ast
::ItemKind
::Fn(fn_
) = &item
.kind
else {
125 not_testable_error(cx
, attr_sp
, Some(&item
));
127 vec
![Annotatable
::Stmt(P(cx
.stmt_item(item
.span
, item
)))]
129 vec
![Annotatable
::Item(item
)]
133 // check_*_signature will report any errors in the type so compilation
134 // will fail. We shouldn't try to expand in this case because the errors
135 // would be spurious.
136 let check_result
= if is_bench
{
137 check_bench_signature(cx
, &item
, &fn_
)
139 check_test_signature(cx
, &item
, &fn_
)
141 if check_result
.is_err() {
143 vec
![Annotatable
::Stmt(P(cx
.stmt_item(item
.span
, item
)))]
145 vec
![Annotatable
::Item(item
)]
149 let sp
= cx
.with_def_site_ctxt(item
.span
);
150 let ret_ty_sp
= cx
.with_def_site_ctxt(fn_
.sig
.decl
.output
.span());
151 let attr_sp
= cx
.with_def_site_ctxt(attr_sp
);
153 let test_id
= Ident
::new(sym
::test
, attr_sp
);
155 // creates test::$name
156 let test_path
= |name
| cx
.path(ret_ty_sp
, vec
![test_id
, Ident
::from_str_and_span(name
, sp
)]);
158 // creates test::ShouldPanic::$name
159 let should_panic_path
= |name
| {
164 Ident
::from_str_and_span("ShouldPanic", sp
),
165 Ident
::from_str_and_span(name
, sp
),
170 // creates test::TestType::$name
171 let test_type_path
= |name
| {
176 Ident
::from_str_and_span("TestType", sp
),
177 Ident
::from_str_and_span(name
, sp
),
182 // creates $name: $expr
183 let field
= |name
, expr
| cx
.field_imm(sp
, Ident
::from_str_and_span(name
, sp
), expr
);
185 let test_fn
= if is_bench
{
186 // A simple ident for a lambda
187 let b
= Ident
::from_str_and_span("b", attr_sp
);
191 cx
.expr_path(test_path("StaticBenchFn")),
193 // |b| self::test::assert_test_result(
198 cx
.expr_path(test_path("assert_test_result")),
200 // super::$test_fn(b)
203 cx
.expr_path(cx
.path(sp
, vec
![item
.ident
])),
204 thin_vec
![cx
.expr_ident(sp
, b
)],
215 cx
.expr_path(test_path("StaticTestFn")),
220 // test::assert_test_result(
223 cx
.expr_path(test_path("assert_test_result")),
228 cx
.expr_path(cx
.path(sp
, vec
![item
.ident
])),
238 let test_path_symbol
= Symbol
::intern(&item_path(
239 // skip the name of the root module
240 &cx
.current_expansion
.module
.mod_path
[1..],
244 let location_info
= get_location_info(cx
, &item
);
249 Ident
::new(item
.ident
.name
, sp
),
252 cx
.attr_nested_word(sym
::cfg
, sym
::test
, attr_sp
),
253 // #[rustc_test_marker = "test_case_sort_key"]
254 cx
.attr_name_value_str(sym
::rustc_test_marker
, test_path_symbol
, attr_sp
),
256 // const $ident: test::TestDescAndFn =
257 ast
::ItemKind
::Const(
259 defaultness
: ast
::Defaultness
::Final
,
260 generics
: ast
::Generics
::default(),
261 ty
: cx
.ty(sp
, ast
::TyKind
::Path(None
, test_path("TestDescAndFn"))),
262 // test::TestDescAndFn {
266 test_path("TestDescAndFn"),
268 // desc: test::TestDesc {
273 test_path("TestDesc"),
275 // name: "path::to::test"
280 cx
.expr_path(test_path("StaticTestName")),
281 thin_vec
![cx
.expr_str(sp
, test_path_symbol
)],
284 // ignore: true | false
285 field("ignore", cx
.expr_bool(sp
, should_ignore(&item
)),),
286 // ignore_message: Some("...") | None
289 if let Some(msg
) = should_ignore_message(&item
) {
290 cx
.expr_some(sp
, cx
.expr_str(sp
, msg
))
295 // source_file: <relative_path_of_source_file>
296 field("source_file", cx
.expr_str(sp
, location_info
.0)),
297 // start_line: start line of the test fn identifier.
298 field("start_line", cx
.expr_usize(sp
, location_info
.1)),
299 // start_col: start column of the test fn identifier.
300 field("start_col", cx
.expr_usize(sp
, location_info
.2)),
301 // end_line: end line of the test fn identifier.
302 field("end_line", cx
.expr_usize(sp
, location_info
.3)),
303 // end_col: end column of the test fn identifier.
304 field("end_col", cx
.expr_usize(sp
, location_info
.4)),
305 // compile_fail: true | false
306 field("compile_fail", cx
.expr_bool(sp
, false)),
307 // no_run: true | false
308 field("no_run", cx
.expr_bool(sp
, false)),
312 match should_panic(cx
, &item
) {
313 // test::ShouldPanic::No
315 cx
.expr_path(should_panic_path("No"))
317 // test::ShouldPanic::Yes
318 ShouldPanic
::Yes(None
) => {
319 cx
.expr_path(should_panic_path("Yes"))
321 // test::ShouldPanic::YesWithMessage("...")
322 ShouldPanic
::Yes(Some(sym
)) => cx
.expr_call(
324 cx
.expr_path(should_panic_path("YesWithMessage")),
325 thin_vec
![cx
.expr_str(sp
, sym
)],
332 match test_type(cx
) {
333 // test::TestType::UnitTest
334 TestType
::UnitTest
=> {
335 cx
.expr_path(test_type_path("UnitTest"))
337 // test::TestType::IntegrationTest
338 TestType
::IntegrationTest
=> {
339 cx
.expr_path(test_type_path("IntegrationTest"))
341 // test::TestPath::Unknown
342 TestType
::Unknown
=> {
343 cx
.expr_path(test_type_path("Unknown"))
351 // testfn: test::StaticTestFn(...) | test::StaticBenchFn(...)
352 field("testfn", test_fn
), // }
360 test_const
= test_const
.map(|mut tc
| {
361 tc
.vis
.kind
= ast
::VisibilityKind
::Public
;
366 let test_extern
= cx
.item(sp
, test_id
, ast
::AttrVec
::new(), ast
::ItemKind
::ExternCrate(None
));
368 debug
!("synthetic test item:\n{}\n", pprust
::item_to_string(&test_const
));
372 // Access to libtest under a hygienic name
373 Annotatable
::Stmt(P(cx
.stmt_item(sp
, test_extern
))),
374 // The generated test case
375 Annotatable
::Stmt(P(cx
.stmt_item(sp
, test_const
))),
377 Annotatable
::Stmt(P(cx
.stmt_item(sp
, item
))),
381 // Access to libtest under a hygienic name
382 Annotatable
::Item(test_extern
),
383 // The generated test case
384 Annotatable
::Item(test_const
),
386 Annotatable
::Item(item
),
391 fn not_testable_error(cx
: &ExtCtxt
<'_
>, attr_sp
: Span
, item
: Option
<&ast
::Item
>) {
392 let diag
= &cx
.sess
.parse_sess
.span_diagnostic
;
393 let msg
= "the `#[test]` attribute may only be used on a non-associated function";
394 let mut err
= match item
.map(|i
| &i
.kind
) {
395 // These were a warning before #92959 and need to continue being that to avoid breaking
396 // stable user code (#94508).
397 Some(ast
::ItemKind
::MacCall(_
)) => diag
.struct_span_warn(attr_sp
, msg
),
398 // `.forget_guarantee()` needed to get these two arms to match types. Because of how
399 // locally close the `.emit()` call is I'm comfortable with it, but if it can be
400 // reworked in the future to not need it, it'd be nice.
401 _
=> diag
.struct_span_err(attr_sp
, msg
).forget_guarantee(),
403 if let Some(item
) = item
{
407 "expected a non-associated function, found {} {}",
413 err
.span_label(attr_sp
, "the `#[test]` macro causes a function to be run as a test and has no effect on non-functions")
414 .span_suggestion(attr_sp
,
415 "replace with conditional compilation to make the item only exist when tests are being run",
417 Applicability
::MaybeIncorrect
)
421 fn get_location_info(cx
: &ExtCtxt
<'_
>, item
: &ast
::Item
) -> (Symbol
, usize, usize, usize, usize) {
422 let span
= item
.ident
.span
;
423 let (source_file
, lo_line
, lo_col
, hi_line
, hi_col
) =
424 cx
.sess
.source_map().span_to_location_info(span
);
426 let file_name
= match source_file
{
427 Some(sf
) => sf
.name
.display(FileNameDisplayPreference
::Remapped
).to_string(),
428 None
=> "no-location".to_string(),
431 (Symbol
::intern(&file_name
), lo_line
, lo_col
, hi_line
, hi_col
)
434 fn item_path(mod_path
: &[Ident
], item_ident
: &Ident
) -> String
{
437 .chain(iter
::once(item_ident
))
438 .map(|x
| x
.to_string())
439 .collect
::<Vec
<String
>>()
448 fn should_ignore(i
: &ast
::Item
) -> bool
{
449 attr
::contains_name(&i
.attrs
, sym
::ignore
)
452 fn should_ignore_message(i
: &ast
::Item
) -> Option
<Symbol
> {
453 match attr
::find_by_name(&i
.attrs
, sym
::ignore
) {
455 match attr
.meta_item_list() {
456 // Handle #[ignore(bar = "foo")]
458 // Handle #[ignore] and #[ignore = "message"]
459 None
=> attr
.value_str(),
466 fn should_panic(cx
: &ExtCtxt
<'_
>, i
: &ast
::Item
) -> ShouldPanic
{
467 match attr
::find_by_name(&i
.attrs
, sym
::should_panic
) {
469 let sd
= &cx
.sess
.parse_sess
.span_diagnostic
;
471 match attr
.meta_item_list() {
472 // Handle #[should_panic(expected = "foo")]
476 .find(|mi
| mi
.has_name(sym
::expected
))
477 .and_then(|mi
| mi
.meta_item())
478 .and_then(|mi
| mi
.value_str());
479 if list
.len() != 1 || msg
.is_none() {
482 "argument must be of the form: \
483 `expected = \"error message\"`",
486 "errors in this attribute were erroneously \
487 allowed and will become a hard error in a \
491 ShouldPanic
::Yes(None
)
493 ShouldPanic
::Yes(msg
)
496 // Handle #[should_panic] and #[should_panic = "expected"]
497 None
=> ShouldPanic
::Yes(attr
.value_str()),
500 None
=> ShouldPanic
::No
,
510 /// Attempts to determine the type of test.
511 /// Since doctests are created without macro expanding, only possible variants here
512 /// are `UnitTest`, `IntegrationTest` or `Unknown`.
513 fn test_type(cx
: &ExtCtxt
<'_
>) -> TestType
{
514 // Root path from context contains the topmost sources directory of the crate.
515 // I.e., for `project` with sources in `src` and tests in `tests` folders
516 // (no matter how many nested folders lie inside),
517 // there will be two different root paths: `/project/src` and `/project/tests`.
518 let crate_path
= cx
.root_path
.as_path();
520 if crate_path
.ends_with("src") {
521 // `/src` folder contains unit-tests.
523 } else if crate_path
.ends_with("tests") {
524 // `/tests` folder contains integration tests.
525 TestType
::IntegrationTest
527 // Crate layout doesn't match expected one, test type is unknown.
532 fn check_test_signature(
536 ) -> Result
<(), ErrorGuaranteed
> {
537 let has_should_panic_attr
= attr
::contains_name(&i
.attrs
, sym
::should_panic
);
538 let sd
= &cx
.sess
.parse_sess
.span_diagnostic
;
540 if let ast
::Unsafe
::Yes(span
) = f
.sig
.header
.unsafety
{
541 return Err(sd
.emit_err(errors
::TestBadFn { span: i.span, cause: span, kind: "unsafe" }
));
544 if let ast
::Async
::Yes { span, .. }
= f
.sig
.header
.asyncness
{
545 return Err(sd
.emit_err(errors
::TestBadFn { span: i.span, cause: span, kind: "async" }
));
548 // If the termination trait is active, the compiler will check that the output
549 // type implements the `Termination` trait as `libtest` enforces that.
550 let has_output
= match &f
.sig
.decl
.output
{
551 ast
::FnRetTy
::Default(..) => false,
552 ast
::FnRetTy
::Ty(t
) if t
.kind
.is_unit() => false,
556 if !f
.sig
.decl
.inputs
.is_empty() {
557 return Err(sd
.span_err(i
.span
, "functions used as tests can not have any arguments"));
560 if has_should_panic_attr
&& has_output
{
561 return Err(sd
.span_err(i
.span
, "functions using `#[should_panic]` must return `()`"));
564 if f
.generics
.params
.iter().any(|param
| !matches
!(param
.kind
, GenericParamKind
::Lifetime
)) {
565 return Err(sd
.span_err(
567 "functions used as tests can not have any non-lifetime generic parameters",
574 fn check_bench_signature(
578 ) -> Result
<(), ErrorGuaranteed
> {
579 // N.B., inadequate check, but we're running
580 // well before resolve, can't get too deep.
581 if f
.sig
.decl
.inputs
.len() != 1 {
582 return Err(cx
.sess
.parse_sess
.span_diagnostic
.emit_err(errors
::BenchSig { span: i.span }
));