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 item = item.map(|mut item| {
55 let test_path_symbol = Symbol::intern(&item_path(
56 &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 !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 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 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 let test_path = |name| cx.path(ret_ty_sp, vec![test_id, Ident::from_str_and_span(name, sp)]);
169
170 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 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 let field = |name, expr| cx.field_imm(sp, Ident::from_str_and_span(name, sp), expr);
196
197 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 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(cx.lambda1(
218 sp,
219 cx.expr_call(
220 sp,
221 cx.expr_path(test_path("assert_test_result")),
222 thin_vec![
223 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 )), ],
234 )
235 } else {
236 cx.expr_call(
237 sp,
238 cx.expr_path(test_path("StaticTestFn")),
239 thin_vec![
240 coverage_off(cx.lambda0(
243 sp,
244 cx.expr_call(
246 sp,
247 cx.expr_path(test_path("assert_test_result")),
248 thin_vec![
249 cx.expr_call(
251 ret_ty_sp,
252 cx.expr_path(cx.path(sp, vec![item.ident])),
253 ThinVec::new(),
254 ), ],
256 ), )), ],
259 )
260 };
261
262 let test_path_symbol = Symbol::intern(&item_path(
263 &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 cx.attr_nested_word(sym::cfg, sym::test, attr_sp),
277 cx.attr_name_value_str(sym::rustc_test_marker, test_path_symbol, attr_sp),
279 cx.attr_nested_word(sym::doc, sym::hidden, attr_sp),
281 ],
282 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 define_opaque: None,
289 expr: Some(
291 cx.expr_struct(
292 sp,
293 test_path("TestDescAndFn"),
294 thin_vec![
295 field(
297 "desc",
298 cx.expr_struct(sp, test_path("TestDesc"), thin_vec![
299 field(
301 "name",
302 cx.expr_call(
303 sp,
304 cx.expr_path(test_path("StaticTestName")),
305 thin_vec![cx.expr_str(sp, test_path_symbol)],
306 ),
307 ),
308 field("ignore", cx.expr_bool(sp, should_ignore(&item)),),
310 field(
312 "ignore_message",
313 if let Some(msg) = should_ignore_message(&item) {
314 cx.expr_some(sp, cx.expr_str(sp, msg))
315 } else {
316 cx.expr_none(sp)
317 },
318 ),
319 field("source_file", cx.expr_str(sp, location_info.0)),
321 field("start_line", cx.expr_usize(sp, location_info.1)),
323 field("start_col", cx.expr_usize(sp, location_info.2)),
325 field("end_line", cx.expr_usize(sp, location_info.3)),
327 field("end_col", cx.expr_usize(sp, location_info.4)),
329 field("compile_fail", cx.expr_bool(sp, false)),
331 field("no_run", cx.expr_bool(sp, false)),
333 field("should_panic", match should_panic(cx, &item) {
335 ShouldPanic::No => {
337 cx.expr_path(should_panic_path("No"))
338 }
339 ShouldPanic::Yes(None) => {
341 cx.expr_path(should_panic_path("Yes"))
342 }
343 ShouldPanic::Yes(Some(sym)) => cx.expr_call(
345 sp,
346 cx.expr_path(should_panic_path("YesWithMessage")),
347 thin_vec![cx.expr_str(sp, sym)],
348 ),
349 },),
350 field("test_type", match test_type(cx) {
352 TestType::UnitTest => {
354 cx.expr_path(test_type_path("UnitTest"))
355 }
356 TestType::IntegrationTest => {
358 cx.expr_path(test_type_path("IntegrationTest"))
359 }
360 TestType::Unknown => {
362 cx.expr_path(test_type_path("Unknown"))
363 }
364 },),
365 ],),
367 ),
368 field("testfn", test_fn), ],
371 ), ),
373 }
374 .into(),
375 ),
376 );
377 test_const = test_const.map(|mut tc| {
378 tc.vis.kind = ast::VisibilityKind::Public;
379 tc
380 });
381
382 let test_extern = cx.item(sp, test_id, ast::AttrVec::new(), ast::ItemKind::ExternCrate(None));
384
385 debug!("synthetic test item:\n{}\n", pprust::item_to_string(&test_const));
386
387 if is_stmt {
388 vec![
389 Annotatable::Stmt(P(cx.stmt_item(sp, test_extern))),
391 Annotatable::Stmt(P(cx.stmt_item(sp, test_const))),
393 Annotatable::Stmt(P(cx.stmt_item(sp, item))),
395 ]
396 } else {
397 vec![
398 Annotatable::Item(test_extern),
400 Annotatable::Item(test_const),
402 Annotatable::Item(item),
404 ]
405 }
406}
407
408fn not_testable_error(cx: &ExtCtxt<'_>, attr_sp: Span, item: Option<&ast::Item>) {
409 let dcx = cx.dcx();
410 let msg = "the `#[test]` attribute may only be used on a non-associated function";
411 let level = match item.map(|i| &i.kind) {
412 Some(ast::ItemKind::MacCall(_)) => Level::Warning,
415 _ => Level::Error,
416 };
417 let mut err = Diag::<()>::new(dcx, level, msg);
418 err.span(attr_sp);
419 if let Some(item) = item {
420 err.span_label(
421 item.span,
422 format!(
423 "expected a non-associated function, found {} {}",
424 item.kind.article(),
425 item.kind.descr()
426 ),
427 );
428 }
429 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")
430 .with_span_suggestion(attr_sp,
431 "replace with conditional compilation to make the item only exist when tests are being run",
432 "#[cfg(test)]",
433 Applicability::MaybeIncorrect)
434 .emit();
435}
436
437fn get_location_info(cx: &ExtCtxt<'_>, item: &ast::Item) -> (Symbol, usize, usize, usize, usize) {
438 let span = item.ident.span;
439 let (source_file, lo_line, lo_col, hi_line, hi_col) =
440 cx.sess.source_map().span_to_location_info(span);
441
442 let file_name = match source_file {
443 Some(sf) => sf.name.display(FileNameDisplayPreference::Remapped).to_string(),
444 None => "no-location".to_string(),
445 };
446
447 (Symbol::intern(&file_name), lo_line, lo_col, hi_line, hi_col)
448}
449
450fn item_path(mod_path: &[Ident], item_ident: &Ident) -> String {
451 mod_path
452 .iter()
453 .chain(iter::once(item_ident))
454 .map(|x| x.to_string())
455 .collect::<Vec<String>>()
456 .join("::")
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 Some(_) => None,
474 None => attr.value_str(),
476 }
477 }
478 None => None,
479 }
480}
481
482fn should_panic(cx: &ExtCtxt<'_>, i: &ast::Item) -> ShouldPanic {
483 match attr::find_by_name(&i.attrs, sym::should_panic) {
484 Some(attr) => {
485 match attr.meta_item_list() {
486 Some(list) => {
488 let msg = list
489 .iter()
490 .find(|mi| mi.has_name(sym::expected))
491 .and_then(|mi| mi.meta_item())
492 .and_then(|mi| mi.value_str());
493 if list.len() != 1 || msg.is_none() {
494 cx.dcx()
495 .struct_span_warn(
496 attr.span,
497 "argument must be of the form: \
498 `expected = \"error message\"`",
499 )
500 .with_note(
501 "errors in this attribute were erroneously \
502 allowed and will become a hard error in a \
503 future release",
504 )
505 .emit();
506 ShouldPanic::Yes(None)
507 } else {
508 ShouldPanic::Yes(msg)
509 }
510 }
511 None => ShouldPanic::Yes(attr.value_str()),
513 }
514 }
515 None => ShouldPanic::No,
516 }
517}
518
519enum TestType {
520 UnitTest,
521 IntegrationTest,
522 Unknown,
523}
524
525fn test_type(cx: &ExtCtxt<'_>) -> TestType {
529 let crate_path = cx.root_path.as_path();
534
535 if crate_path.ends_with("src") {
536 TestType::UnitTest
538 } else if crate_path.ends_with("tests") {
539 TestType::IntegrationTest
541 } else {
542 TestType::Unknown
544 }
545}
546
547fn check_test_signature(
548 cx: &ExtCtxt<'_>,
549 i: &ast::Item,
550 f: &ast::Fn,
551) -> Result<(), ErrorGuaranteed> {
552 let has_should_panic_attr = attr::contains_name(&i.attrs, sym::should_panic);
553 let dcx = cx.dcx();
554
555 if let ast::Safety::Unsafe(span) = f.sig.header.safety {
556 return Err(dcx.emit_err(errors::TestBadFn { span: i.span, cause: span, kind: "unsafe" }));
557 }
558
559 if let Some(coroutine_kind) = f.sig.header.coroutine_kind {
560 match coroutine_kind {
561 ast::CoroutineKind::Async { span, .. } => {
562 return Err(dcx.emit_err(errors::TestBadFn {
563 span: i.span,
564 cause: span,
565 kind: "async",
566 }));
567 }
568 ast::CoroutineKind::Gen { span, .. } => {
569 return Err(dcx.emit_err(errors::TestBadFn {
570 span: i.span,
571 cause: span,
572 kind: "gen",
573 }));
574 }
575 ast::CoroutineKind::AsyncGen { span, .. } => {
576 return Err(dcx.emit_err(errors::TestBadFn {
577 span: i.span,
578 cause: span,
579 kind: "async gen",
580 }));
581 }
582 }
583 }
584
585 let has_output = match &f.sig.decl.output {
588 ast::FnRetTy::Default(..) => false,
589 ast::FnRetTy::Ty(t) if t.kind.is_unit() => false,
590 _ => true,
591 };
592
593 if !f.sig.decl.inputs.is_empty() {
594 return Err(dcx.span_err(i.span, "functions used as tests can not have any arguments"));
595 }
596
597 if has_should_panic_attr && has_output {
598 return Err(dcx.span_err(i.span, "functions using `#[should_panic]` must return `()`"));
599 }
600
601 if f.generics.params.iter().any(|param| !matches!(param.kind, GenericParamKind::Lifetime)) {
602 return Err(dcx.span_err(
603 i.span,
604 "functions used as tests can not have any non-lifetime generic parameters",
605 ));
606 }
607
608 Ok(())
609}
610
611fn check_bench_signature(
612 cx: &ExtCtxt<'_>,
613 i: &ast::Item,
614 f: &ast::Fn,
615) -> Result<(), ErrorGuaranteed> {
616 if f.sig.decl.inputs.len() != 1 {
619 return Err(cx.dcx().emit_err(errors::BenchSig { span: i.span }));
620 }
621 Ok(())
622}