1use 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
19pub(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 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 &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 !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 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 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 let test_path = |name| cx.path(ret_ty_sp, vec![test_ident, Ident::from_str_and_span(name, sp)]);
176
177 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 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 let field = |name, expr| cx.field_imm(sp, Ident::from_str_and_span(name, sp), expr);
203
204 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 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(cx.lambda1(
225 sp,
226 cx.expr_call(
227 sp,
228 cx.expr_path(test_path("assert_test_result")),
229 thin_vec![
230 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 )), ],
241 )
242 } else {
243 cx.expr_call(
244 sp,
245 cx.expr_path(test_path("StaticTestFn")),
246 thin_vec![
247 coverage_off(cx.lambda0(
250 sp,
251 cx.expr_call(
253 sp,
254 cx.expr_path(test_path("assert_test_result")),
255 thin_vec![
256 cx.expr_call(
258 ret_ty_sp,
259 cx.expr_path(cx.path(sp, vec![fn_.ident])),
260 ThinVec::new(),
261 ), ],
263 ), )), ],
266 )
267 };
268
269 let test_path_symbol = Symbol::intern(&item_path(
270 &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 cx.attr_nested_word(sym::cfg, sym::test, attr_sp),
283 cx.attr_name_value_str(sym::rustc_test_marker, test_path_symbol, attr_sp),
285 cx.attr_nested_word(sym::doc, sym::hidden, attr_sp),
287 ],
288 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 expr: Some(
298 cx.expr_struct(
299 sp,
300 test_path("TestDescAndFn"),
301 thin_vec![
302 field(
304 "desc",
305 cx.expr_struct(sp, test_path("TestDesc"), thin_vec![
306 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 field("ignore", cx.expr_bool(sp, should_ignore(&item)),),
317 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 field("source_file", cx.expr_str(sp, location_info.0)),
328 field("start_line", cx.expr_usize(sp, location_info.1)),
330 field("start_col", cx.expr_usize(sp, location_info.2)),
332 field("end_line", cx.expr_usize(sp, location_info.3)),
334 field("end_col", cx.expr_usize(sp, location_info.4)),
336 field("compile_fail", cx.expr_bool(sp, false)),
338 field("no_run", cx.expr_bool(sp, false)),
340 field("should_panic", match should_panic(cx, &item) {
342 ShouldPanic::No => {
344 cx.expr_path(should_panic_path("No"))
345 }
346 ShouldPanic::Yes(None) => {
348 cx.expr_path(should_panic_path("Yes"))
349 }
350 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 field("test_type", match test_type(cx) {
359 TestType::UnitTest => {
361 cx.expr_path(test_type_path("UnitTest"))
362 }
363 TestType::IntegrationTest => {
365 cx.expr_path(test_type_path("IntegrationTest"))
366 }
367 TestType::Unknown => {
369 cx.expr_path(test_type_path("Unknown"))
370 }
371 },),
372 ],),
374 ),
375 field("testfn", test_fn), ],
378 ), ),
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 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 Annotatable::Stmt(P(cx.stmt_item(sp, test_extern))),
399 Annotatable::Stmt(P(cx.stmt_item(sp, test_const))),
401 Annotatable::Stmt(P(cx.stmt_item(sp, item))),
403 ]
404 } else {
405 vec![
406 Annotatable::Item(test_extern),
408 Annotatable::Item(test_const),
410 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 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 Some(_) => None,
482 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 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 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
533fn test_type(cx: &ExtCtxt<'_>) -> TestType {
537 let crate_path = cx.root_path.as_path();
542
543 if crate_path.ends_with("src") {
544 TestType::UnitTest
546 } else if crate_path.ends_with("tests") {
547 TestType::IntegrationTest
549 } else {
550 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 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 if f.sig.decl.inputs.len() != 1 {
627 return Err(cx.dcx().emit_err(errors::BenchSig { span: i.span }));
628 }
629 Ok(())
630}