1use darling::ast::NestedMeta;
2use darling::{Error, FromMeta};
3use proc_macro::TokenStream;
4use proc_macro2::{Ident, Span};
5use quote::{quote, ToTokens};
6use syn::parse::{Parse, ParseStream};
7use syn::punctuated::Punctuated;
8use syn::{
9 parse2, parse_macro_input, Attribute, Expr, ExprClosure, FnArg, GenericArgument, ItemFn,
10 ItemMod, LitStr, Pat, PatType, PathArguments, PathSegment, ReturnType, Token, Type,
11 TypeParamBound, TypePath,
12};
13use test_r_core::internal::ShouldPanic;
14
15#[derive(Debug, Clone)]
16enum DependencyTag {
17 None,
18 Tagged(String),
19 Matrix(Ident),
20}
21
22impl DependencyTag {
23 fn into_iter(self) -> impl Iterator<Item = String> {
24 match self {
25 DependencyTag::Tagged(tag) => Some(tag),
26 _ => None,
27 }
28 .into_iter()
29 }
30}
31
32impl From<Option<String>> for DependencyTag {
33 fn from(value: Option<String>) -> Self {
34 match value {
35 Some(tag) => DependencyTag::Tagged(tag),
36 None => DependencyTag::None,
37 }
38 }
39}
40
41#[proc_macro_attribute]
42pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
43 test_impl(attr, item, false)
44}
45
46#[proc_macro_attribute]
47pub fn bench(attr: TokenStream, item: TokenStream) -> TokenStream {
48 test_impl(attr, item, true)
49}
50
51fn test_impl(_attr: TokenStream, item: TokenStream, is_bench: bool) -> TokenStream {
52 let mut ast: ItemFn = syn::parse(item).expect("test ast");
53 let test_name = ast.sig.ident.clone();
54 let test_name_str = test_name.to_string();
55
56 let is_ignored = ast.attrs.iter().any(|attr| attr.path().is_ident("ignore"));
57 let should_panic = ast
58 .attrs
59 .iter()
60 .find(|attr| attr.path().is_ident("should_panic"))
61 .map(should_panic_message)
62 .unwrap_or(ShouldPanic::No);
63
64 let should_panic = match should_panic {
65 ShouldPanic::No => quote! { test_r::core::ShouldPanic::No },
66 ShouldPanic::Yes => quote! { test_r::core::ShouldPanic::Yes },
67 ShouldPanic::WithMessage(message) => {
68 quote! { test_r::core::ShouldPanic::WithMessage(#message.to_string()) }
69 }
70 };
71
72 let timeout_attr = ast
73 .attrs
74 .iter()
75 .find(|attr| attr.path().is_ident("timeout"));
76 let timeout = timeout_attr
77 .map(|attr| {
78 let timeout = attr
79 .parse_args::<syn::LitInt>()
80 .expect("timeout attribute's parameter must be an integer (timeout milliseconds)");
81 let timeout = timeout
82 .base10_parse::<u64>()
83 .expect("timeout attribute's parameter must be an integer (timeout milliseconds)");
84 quote! { Some(std::time::Duration::from_millis(#timeout)) }
85 })
86 .unwrap_or(quote! { None });
87 let has_timeout = timeout_attr.is_some();
88
89 let flaky_attr = ast.attrs.iter().find(|attr| attr.path().is_ident("flaky"));
90 let non_flaky_attr = ast
91 .attrs
92 .iter()
93 .find(|attr| attr.path().is_ident("non_flaky"));
94 let flakiness_control = match (flaky_attr, non_flaky_attr) {
95 (None, None) => quote! { test_r::core::FlakinessControl::None },
96 (Some(_), Some(_)) => {
97 panic!("Cannot have both #[flaky] and #[non_flaky] attributes")
98 }
99 (Some(attr), None) => {
100 let n = attr
101 .parse_args::<syn::LitInt>()
102 .expect("flaky attribute's parameter must be an integer (max number of retries)");
103 let n = n
104 .base10_parse::<usize>()
105 .expect("flaky attribute's parameter must be an integer (max number of retries)");
106 quote! { test_r::core::FlakinessControl::RetryKnownFlaky(#n) }
107 }
108 (None, Some(attr)) => {
109 let n = attr
110 .parse_args::<syn::LitInt>()
111 .expect("non_flaky attribute's parameter must be an integer (number of tries)");
112 let n = n
113 .base10_parse::<usize>()
114 .expect("non_flaky attribute's parameter must be an integer (number of tries)");
115 quote! { test_r::core::FlakinessControl::ProveNonFlaky(#n) }
116 }
117 };
118
119 let capture_control = from_three_state_attrs(
120 &ast,
121 quote! { test_r::core::CaptureControl::Default },
122 "always_capture",
123 quote! { test_r::core::CaptureControl::AlwaysCapture },
124 "never_capture",
125 quote! { test_r::core::CaptureControl::NeverCapture },
126 );
127 let report_time_control = from_three_state_attrs(
128 &ast,
129 quote! { test_r::core::ReportTimeControl::Default },
130 "always_report_time",
131 quote! { test_r::core::ReportTimeControl::Enabled },
132 "never_report_time",
133 quote! { test_r::core::ReportTimeControl::Disabled },
134 );
135 let ensure_time_control = from_three_state_attrs(
136 &ast,
137 quote! { test_r::core::ReportTimeControl::Default },
138 "always_ensure_time",
139 quote! { test_r::core::ReportTimeControl::Enabled },
140 "never_ensure_time",
141 quote! { test_r::core::ReportTimeControl::Disabled },
142 );
143
144 let tag_attrs = ast
145 .attrs
146 .iter()
147 .filter(|attr| attr.path().is_ident("tag"))
148 .map(|attr| {
149 let tag = attr
150 .parse_args::<Ident>()
151 .expect("tag attribute's parameter must be a identifier");
152 let tag_str = tag.to_string();
153 quote! { #tag_str.to_string() }
154 });
155 let tags = quote! { vec![#(#tag_attrs),*] };
156
157 let register_ident = Ident::new(
158 &format!("test_r_register_{}", test_name_str),
159 test_name.span(),
160 );
161
162 let is_async = ast.sig.asyncness.is_some();
163 let (dep_getters, _dep_names, dep_dimensions) = get_dependency_params(&ast, is_bench);
164
165 if dep_dimensions.is_empty() {
166 let register_call = if is_bench {
167 if has_timeout {
168 panic!("Benchmarks cannot have a timeout attribute")
169 }
170
171 if is_async {
172 quote! {
173 test_r::core::register_test(
174 #test_name_str,
175 module_path!(),
176 #is_ignored,
177 #should_panic,
178 test_r::core::TestType::from_path(file!()),
179 None,
180 test_r::core::FlakinessControl::None,
181 #capture_control,
182 #tags,
183 #report_time_control,
184 #ensure_time_control,
185 test_r::core::TestFunction::AsyncBench(std::sync::Arc::new(|__test_r_bencher_arg, __test_r_deps_arg| Box::pin(async move { #test_name(__test_r_bencher_arg, #(#dep_getters),*).await })))
186 );
187 }
188 } else {
189 quote! {
190 test_r::core::register_test(
191 #test_name_str,
192 module_path!(),
193 #is_ignored,
194 #should_panic,
195 test_r::core::TestType::from_path(file!()),
196 None,
197 test_r::core::FlakinessControl::None,
198 #capture_control,
199 #tags,
200 #report_time_control,
201 #ensure_time_control,
202 test_r::core::TestFunction::SyncBench(std::sync::Arc::new(|__test_r_bencher_arg, __test_r_deps_arg| #test_name(__test_r_bencher_arg, #(#dep_getters),*)))
203 );
204 }
205 }
206 } else if is_async {
207 quote! {
208 test_r::core::register_test(
209 #test_name_str,
210 module_path!(),
211 #is_ignored,
212 #should_panic,
213 test_r::core::TestType::from_path(file!()),
214 #timeout,
215 #flakiness_control,
216 #capture_control,
217 #tags,
218 #report_time_control,
219 #ensure_time_control,
220 test_r::core::TestFunction::Async(std::sync::Arc::new(
221 move |__test_r_deps_arg| {
222 Box::pin(async move {
223 let result = #test_name(#(#dep_getters),*).await;
224 Box::new(result) as Box<dyn test_r::core::TestReturnValue>
225 })
226 }
227 ))
228 );
229 }
230 } else {
231 if has_timeout {
232 panic!("The #[timeout()] attribute is only supported for async tests");
233 }
234
235 quote! {
236 test_r::core::register_test(
237 #test_name_str,
238 module_path!(),
239 #is_ignored,
240 #should_panic,
241 test_r::core::TestType::from_path(file!()),
242 None,
243 #flakiness_control,
244 #capture_control,
245 #tags,
246 #report_time_control,
247 #ensure_time_control,
248 test_r::core::TestFunction::Sync(std::sync::Arc::new(|__test_r_deps_arg| Box::new(#test_name(#(#dep_getters),*))))
249 );
250 }
251 };
252
253 filter_custom_parameter_attributes(&mut ast);
254 let result = quote! {
255 #[cfg(test)]
256 #[test_r::ctor::ctor]
257 fn #register_ident() {
258 #register_call
259 }
260
261 #ast
262 };
263
264 result.into()
265 } else {
266 let test_name_impl = Ident::new(&format!("{}_impl", test_name), Span::call_site());
268 ast.sig.ident = test_name_impl.clone();
269
270 let mut overridden_dep_getters = dep_getters.clone();
271 let mut clones = Vec::new();
272
273 for (idx, _dim) in &dep_dimensions {
274 let dep_var = Ident::new(&format!("dep_{}", idx), Span::call_site());
275 overridden_dep_getters[*idx] = quote! { &#dep_var(__test_r_deps_arg.clone()) };
276 clones.push(quote! {
277 let #dep_var = #dep_var.clone();
278 });
279 }
280 let mut loops = if is_async {
281 quote! {
282 let mut tags_as_string = String::new();
283 for name in &name_stack {
284 tags_as_string.push_str("_");
285 tags_as_string.push_str(name);
286 }
287 #(#clones)*
288 r.add_async_test(
289 format!("{}{}", #test_name_str, tags_as_string),
290 test_r::core::TestProperties { test_type: test_r::core::TestType::from_path(file!()), ..Default::default() },
291 move |__test_r_deps_arg| {
292 #(#clones)*
293 Box::pin(async move {
294 #test_name_impl(#(#overridden_dep_getters),*).await
295 })
296 },
297 );
298 }
299 } else {
300 quote! {
301 let mut tags_as_string = String::new();
302 for name in &name_stack {
303 tags_as_string.push_str("_");
304 tags_as_string.push_str(name);
305 }
306 #(#clones)*
307 r.add_sync_test(
308 format!("{}{}", #test_name_str, tags_as_string),
309 test_r::core::TestProperties { test_type: test_r::core::TestType::from_path(file!()), ..Default::default() },
310 move |__test_r_deps_arg| {
311 #test_name_impl(#(#overridden_dep_getters),*)
312 },
313 );
314 }
315 };
316
317 for (idx, dim) in dep_dimensions {
318 let dep_name_var = Ident::new(&format!("tag_{}", idx), Span::call_site());
319 let dep_var = Ident::new(&format!("dep_{}", idx), Span::call_site());
320 let get_dep_tags_fn =
321 Ident::new(&format!("test_r_get_dep_tags_{}", dim), Span::call_site());
322 loops = quote! {
323 for (#dep_name_var, #dep_var) in #get_dep_tags_fn() {
324 name_stack.push(#dep_name_var);
325 #loops
326 name_stack.pop();
327 }
328 };
329 }
330
331 filter_custom_parameter_attributes(&mut ast);
332 let result = quote! {
333 #[test_r::test_gen]
334 fn #test_name(r: &mut test_r::core::DynamicTestRegistration) {
335 let mut name_stack = Vec::new();
336 #loops
337 }
338
339 #ast
340 };
341 result.into()
342 }
343}
344
345fn from_three_state_attrs(
346 ast: &ItemFn,
347 default: proc_macro2::TokenStream,
348 on_name: &str,
349 on_value: proc_macro2::TokenStream,
350 off_name: &str,
351 off_value: proc_macro2::TokenStream,
352) -> proc_macro2::TokenStream {
353 let on_attr = ast.attrs.iter().find(|attr| attr.path().is_ident(on_name));
354 let off_attr = ast.attrs.iter().find(|attr| attr.path().is_ident(off_name));
355 match (on_attr, off_attr) {
356 (None, None) => default,
357 (Some(_), Some(_)) => {
358 panic!("Cannot have both #[{on_name}] and #[{off_name}] attributes")
359 }
360 (Some(_), None) => on_value,
361 (None, Some(_)) => off_value,
362 }
363}
364
365struct ShouldPanicArgs {
366 pub expected: Option<LitStr>,
367}
368
369impl Parse for ShouldPanicArgs {
370 fn parse(input: ParseStream) -> syn::Result<Self> {
371 fn try_parse(input: ParseStream) -> syn::Result<Option<LitStr>> {
372 let key: Ident = input.parse()?;
373 if key != "expected" {
374 return Err(syn::Error::new(key.span(), "Expected `expected`"));
375 }
376 input.parse::<Token![=]>()?;
377 let message: LitStr = input.parse()?;
378 Ok(Some(message))
379 }
380
381 let expected = try_parse(input).ok().flatten();
382 Ok(ShouldPanicArgs { expected })
383 }
384}
385
386fn should_panic_message(attr: &Attribute) -> ShouldPanic {
387 let args: ShouldPanicArgs = attr
388 .parse_args()
389 .unwrap_or(ShouldPanicArgs { expected: None });
390 match args.expected {
391 Some(message) => ShouldPanic::WithMessage(message.value()),
392 None => ShouldPanic::Yes,
393 }
394}
395
396#[proc_macro]
397pub fn uses_test_r(_item: TokenStream) -> TokenStream {
398 r#"
399 #[cfg(test)]
400 pub fn main() -> std::process::ExitCode {
401 test_r::core::test_runner()
402 }
403 "#
404 .parse()
405 .unwrap()
406}
407
408struct InheritTestDep {
409 attr: Option<Attribute>,
410 typ: Type,
411}
412
413impl Parse for InheritTestDep {
414 fn parse(input: ParseStream) -> syn::Result<Self> {
415 if input.peek(Token![#]) {
416 let mut attrs = Attribute::parse_outer(input)?;
417 if attrs.len() != 1 {
418 Err(syn::Error::new(
419 input.span(),
420 "Expected zero or one attribute",
421 ))
422 } else {
423 Ok(InheritTestDep {
424 attr: Some(attrs.pop().unwrap()),
425 typ: input.parse()?,
426 })
427 }
428 } else {
429 Ok(InheritTestDep {
430 attr: None,
431 typ: input.parse()?,
432 })
433 }
434 }
435}
436
437#[proc_macro]
438pub fn inherit_test_dep(item: TokenStream) -> TokenStream {
439 let def: InheritTestDep = parse_macro_input!(item as InheritTestDep);
440 let dep_type = match &def.typ {
441 Type::Path(path) => path.clone(),
442 _ => {
443 panic!("Dependency constructor must return a single concrete type")
444 }
445 };
446
447 let tag_str = def.attr.and_then(|a| get_lit_str_attr(&[a], "tagged_as"));
448 let tag = match tag_str {
449 Some(tag) => DependencyTag::Tagged(tag),
450 None => DependencyTag::None,
451 };
452
453 let dep_name_str = type_path_to_string(&dep_type, tag);
454 let getter_ident = Ident::new(
455 &format!("test_r_get_dep_{}", dep_name_str),
456 Span::call_site(),
457 );
458
459 let result = quote! {
460 fn #getter_ident<'a>(dependency_view: &'a impl test_r::core::DependencyView) -> std::sync::Arc<#dep_type> {
461 super::#getter_ident(dependency_view)
462 }
463 };
464
465 result.into()
466}
467
468struct DefineMatrixDimension {
469 dim: Ident,
470 _colon: Token![:],
471 typ: Type,
472 _arrow: Token![->],
473 tags: Punctuated<LitStr, Token![,]>,
474}
475
476impl Parse for DefineMatrixDimension {
477 fn parse(input: ParseStream) -> syn::Result<Self> {
478 Ok(DefineMatrixDimension {
479 dim: input.parse()?,
480 _colon: input.parse()?,
481 typ: input.parse()?,
482 _arrow: input.parse()?,
483 tags: input.parse_terminated(|i| i.parse::<LitStr>(), Token![,])?,
484 })
485 }
486}
487
488#[proc_macro]
489pub fn define_matrix_dimension(item: TokenStream) -> TokenStream {
490 let def = parse_macro_input!(item as DefineMatrixDimension);
491 let get_dep_tags_fn = Ident::new(
492 &format!("test_r_get_dep_tags_{}", def.dim),
493 Span::call_site(),
494 );
495 let typ = def.typ;
496
497 let typ_path = match &typ {
498 Type::Path(path) => path,
499 _ => {
500 panic!("Must use a single concrete type in define_matrix_dimension")
501 }
502 };
503
504 let mut pushes = Vec::new();
505
506 for tag in def.tags {
507 let dep_tag = DependencyTag::Tagged(tag.value());
508 let dep_name_str = type_path_to_string(typ_path, dep_tag);
509 let getter_ident = Ident::new(
510 &format!("test_r_get_dep_{}", dep_name_str),
511 Span::call_site(),
512 );
513
514 let name = tag.value();
515 pushes.push(quote! {
516 result.push((#name.to_string(), std::sync::Arc::new(|dependency_view: std::sync::Arc<dyn test_r::core::DependencyView + Send + Sync>| #getter_ident(&dependency_view))));
517 });
518 }
519
520 let ast = quote! {
521 fn #get_dep_tags_fn() -> Vec<(String, std::sync::Arc<dyn (Fn(std::sync::Arc<dyn test_r::core::DependencyView + Send + Sync>) -> std::sync::Arc<#typ>) + Send + Sync + 'static>)> {
522 let mut result: Vec<(String, std::sync::Arc<dyn (Fn(std::sync::Arc<dyn test_r::core::DependencyView + Send + Sync>) -> std::sync::Arc<#typ>) + Send + Sync + 'static>)> = Vec::new();
523 #(#pushes)*
524 result
525 }
526 };
527 ast.into()
528}
529
530#[derive(Debug, darling::FromMeta)]
531struct TestDepArgs {
532 #[darling(default)]
533 tagged_as: Option<String>,
534}
535
536#[proc_macro_attribute]
537pub fn test_dep(attr: TokenStream, item: TokenStream) -> TokenStream {
538 let attr_args = match NestedMeta::parse_meta_list(attr.into()) {
539 Ok(v) => v,
540 Err(e) => {
541 return TokenStream::from(Error::from(e).write_errors());
542 }
543 };
544
545 let args = match TestDepArgs::from_list(&attr_args) {
546 Ok(v) => v,
547 Err(e) => {
548 return TokenStream::from(e.write_errors());
549 }
550 };
551
552 let ast: ItemFn = syn::parse(item).expect("test ast");
553 let ctor_name = ast.sig.ident.clone();
554
555 let dep_type = match &ast.sig.output {
556 ReturnType::Default => {
557 panic!("Dependency constructor must have a return type")
558 }
559 ReturnType::Type(_, typ) => match &**typ {
560 Type::Path(path) => path.clone(),
561 _ => {
562 panic!("Dependency constructor must return a single concrete type")
563 }
564 },
565 };
566 let dep_name_str = type_path_to_string(&dep_type, args.tagged_as.into());
567 let register_ident = Ident::new(
568 &format!("test_r_register_dep_{}", dep_name_str),
569 Span::call_site(),
570 );
571
572 let is_async = ast.sig.asyncness.is_some();
573 let (dep_getters, dep_names, _dep_dimensions) = get_dependency_params(&ast, false);
574
575 let register_call = if is_async {
576 quote! {
577 test_r::core::register_dependency_constructor(
578 #dep_name_str,
579 module_path!(),
580 test_r::core::DependencyConstructor::Async(std::sync::Arc::new(|__test_r_deps_arg| Box::pin(async move {
581 let result: std::sync::Arc<dyn std::any::Any + Send + Sync> = std::sync::Arc::new(#ctor_name(#(#dep_getters),*).await);
582 result
583 }))),
584 vec![#(#dep_names),*]
585 );
586 }
587 } else {
588 quote! {
589 test_r::core::register_dependency_constructor(
590 #dep_name_str,
591 module_path!(),
592 test_r::core::DependencyConstructor::Sync(std::sync::Arc::new(|__test_r_deps_arg| std::sync::Arc::new(#ctor_name(#(#dep_getters),*)))),
593 vec![#(#dep_names),*]
594 );
595 }
596 };
597
598 let getter_ident = Ident::new(
599 &format!("test_r_get_dep_{}", dep_name_str),
600 Span::call_site(),
601 );
602
603 let getter_body = quote! {
604 dependency_view
605 .get(#dep_name_str)
606 .expect("Dependency not found")
607 .downcast::<#dep_type>()
608 .expect("Dependency type mismatch")
609 };
610
611 let result = quote! {
612 #[cfg(test)]
613 #[test_r::ctor::ctor]
614 fn #register_ident() {
615 #register_call
616 }
617
618 #[cfg(test)]
619 fn #getter_ident<'a>(dependency_view: &'a impl test_r::core::DependencyView) -> std::sync::Arc<#dep_type> {
620 #getter_body
621 }
622
623 #ast
624 };
625
626 result.into()
627}
628
629#[proc_macro_attribute]
630pub fn test_gen(_attr: TokenStream, item: TokenStream) -> TokenStream {
631 let ast: ItemFn = syn::parse(item).expect("test generator ast");
632 let generator_name = ast.sig.ident.clone();
633 let generator_name_str = generator_name.to_string();
634
635 let is_ignored = ast.attrs.iter().any(|attr| attr.path().is_ident("ignore"));
636
637 let register_ident = Ident::new(
638 &format!("test_r_register_generator_{}", generator_name_str),
639 generator_name.span(),
640 );
641
642 let is_async = ast.sig.asyncness.is_some();
643
644 let register_call = if is_async {
645 quote! {
646 test_r::core::register_test_generator(
647 #generator_name_str,
648 module_path!(),
649 #is_ignored,
650 test_r::core::TestGeneratorFunction::Async(std::sync::Arc::new(|| Box::pin(async move { #generator_name().await })))
651 );
652 }
653 } else {
654 quote! {
655 test_r::core::register_test_generator(
656 #generator_name_str,
657 module_path!(),
658 #is_ignored,
659 test_r::core::TestGeneratorFunction::Sync(std::sync::Arc::new(|| #generator_name()))
660 );
661 }
662 };
663
664 let wrapped_ast = if is_async {
665 quote! {
666 async fn #generator_name() -> Vec<test_r::core::GeneratedTest> {
667 let mut tests = test_r::core::DynamicTestRegistration::new();
668 #ast
669 #generator_name(&mut tests).await;
670 tests.to_vec()
671 }
672 }
673 } else {
674 quote! {
675 fn #generator_name() -> Vec<test_r::core::GeneratedTest> {
676 let mut tests = test_r::core::DynamicTestRegistration::new();
677 #ast
678 #generator_name(&mut tests);
679 tests.to_vec()
680 }
681 }
682 };
683
684 let result = quote! {
685 #[cfg(test)]
686 #[test_r::ctor::ctor]
687 fn #register_ident() {
688 #register_call
689 }
690
691 #wrapped_ast
692 };
693
694 result.into()
695}
696
697#[proc_macro_attribute]
698pub fn sequential(_attr: TokenStream, item: TokenStream) -> TokenStream {
699 let ast: ItemMod = syn::parse(item).expect("#[sequential] must be applied to a module");
700
701 let register_ident = Ident::new(
702 &format!("test_r_register_mod_{}_sequential", ast.ident),
703 Span::call_site(),
704 );
705
706 let mod_name_str = ast.ident.to_string();
707 let register_call = quote! {
708 test_r::core::register_suite_sequential(
709 #mod_name_str,
710 module_path!(),
711 );
712 };
713
714 let result = quote! {
715 #[cfg(test)]
716 #[test_r::ctor::ctor]
717 fn #register_ident() {
718 #register_call
719 }
720
721 #ast
722 };
723
724 result.into()
725}
726
727#[proc_macro]
728pub fn add_test(input: TokenStream) -> TokenStream {
729 let params = parse_macro_input!(input with Punctuated::<Expr, Token![,]>::parse_terminated);
730
731 if params.len() != 4 {
732 panic!("add_test! expects exactly 4 parameters");
733 }
734
735 let dtr_expr = ¶ms[0];
736 let name_expr = ¶ms[1];
737 let test_props_expr = ¶ms[2];
738
739 let function_expr = ¶ms[3];
740
741 let function_closure: ExprClosure = parse2(function_expr.to_token_stream())
742 .expect("the third parameter of add_test! must be a closure");
743
744 let (dep_getters, _dep_names, bindings) =
745 get_dependency_params_for_closure(function_closure.inputs.iter());
746 let is_async = matches!(&*function_closure.body, Expr::Async(_));
747
748 let result = if is_async {
749 let mut lets = Vec::new();
750 for (getter, ident) in dep_getters.iter().zip(bindings) {
751 lets.push(quote! {
752 let #ident = #getter;
753 });
754 }
755 let body = match &*function_closure.body {
756 Expr::Async(inner) => inner.block.clone(),
757 _ => panic!("Expected async block"),
758 };
759 quote! {
760 #dtr_expr.add_async_test(#name_expr, #test_props_expr, move |__test_r_deps_arg| {
761 Box::pin(async move {
762 #(#lets)*
763 #body
764 })
765 });
766 }
767 } else {
768 quote! {
769 #dtr_expr.add_sync_test(#name_expr, #test_props_expr, move |__test_r_deps_arg| {
770 let gen = #function_closure;
771 gen(#(#dep_getters),*)
772 });
773 }
774 };
775
776 result.into()
777}
778
779#[proc_macro_attribute]
780pub fn timeout(_attr: TokenStream, item: TokenStream) -> TokenStream {
781 item
782}
783
784#[proc_macro_attribute]
785pub fn flaky(_attr: TokenStream, item: TokenStream) -> TokenStream {
786 item
787}
788
789#[proc_macro_attribute]
790pub fn non_flaky(_attr: TokenStream, item: TokenStream) -> TokenStream {
791 item
792}
793
794#[proc_macro_attribute]
795pub fn always_capture(_attr: TokenStream, item: TokenStream) -> TokenStream {
796 item
797}
798
799#[proc_macro_attribute]
800pub fn never_capture(_attr: TokenStream, item: TokenStream) -> TokenStream {
801 item
802}
803
804#[proc_macro_attribute]
805pub fn always_report_time(_attr: TokenStream, item: TokenStream) -> TokenStream {
806 item
807}
808
809#[proc_macro_attribute]
810pub fn never_report_time(_attr: TokenStream, item: TokenStream) -> TokenStream {
811 item
812}
813
814#[proc_macro_attribute]
815pub fn always_ensure_time(_attr: TokenStream, item: TokenStream) -> TokenStream {
816 item
817}
818
819#[proc_macro_attribute]
820pub fn never_ensure_time(_attr: TokenStream, item: TokenStream) -> TokenStream {
821 item
822}
823
824#[proc_macro_attribute]
825pub fn tag(attr: TokenStream, item: TokenStream) -> TokenStream {
826 if let Ok(ast) = syn::parse::<ItemMod>(item.clone()) {
827 let random = rand::random::<u64>();
828 let register_ident = Ident::new(
829 &format!("test_r_register_mod_{}_tag_{}", ast.ident, random),
830 Span::call_site(),
831 );
832
833 let mod_name_str = ast.ident.to_string();
834
835 let tag = parse_macro_input!(attr as Ident);
836 let tag_str = tag.to_string();
837 let tag = quote! { #tag_str.to_string() };
838
839 let register_call = quote! {
840 test_r::core::register_suite_tag(
841 #mod_name_str,
842 module_path!(),
843 #tag
844 );
845 };
846
847 let result = quote! {
848 #[cfg(test)]
849 #[test_r::ctor::ctor]
850 fn #register_ident() {
851 #register_call
852 }
853
854 #ast
855 };
856
857 result.into()
858 } else {
859 item
861 }
862}
863
864#[proc_macro]
865pub fn tag_suite(input: TokenStream) -> TokenStream {
866 let params = parse_macro_input!(input with Punctuated::<Ident, Token![,]>::parse_terminated);
867
868 if params.len() != 2 {
869 panic!("tag_suite! expects exactly 2 identifiers as parameters: the name of the suite module and the tag");
870 }
871
872 let mod_name_str = params[0].to_string();
873 let tag_str = params[1].to_string();
874
875 let random = rand::random::<u64>();
876 let register_ident = Ident::new(
877 &format!("test_r_register_mod_{}_tag_{}", mod_name_str, random),
878 Span::call_site(),
879 );
880
881 let tag = quote! { #tag_str.to_string() };
882
883 let register_call = quote! {
884 test_r::core::register_suite_tag(
885 #mod_name_str,
886 module_path!(),
887 #tag
888 );
889 };
890
891 let result = quote! {
892 #[cfg(test)]
893 #[test_r::ctor::ctor]
894 fn #register_ident() {
895 #register_call
896 }
897 };
898
899 result.into()
900}
901
902#[proc_macro]
903pub fn sequential_suite(input: TokenStream) -> TokenStream {
904 let params = parse_macro_input!(input with Punctuated::<Ident, Token![,]>::parse_terminated);
905
906 if params.len() != 1 {
907 panic!("sequential_suite! expects exactly 1 identifier as parameter: the name of the suite module");
908 }
909
910 let mod_name_str = params[0].to_string();
911
912 let register_ident = Ident::new(
913 &format!("test_r_register_mod_{}_sequential", mod_name_str),
914 Span::call_site(),
915 );
916
917 let register_call = quote! {
918 test_r::core::register_suite_sequential(
919 #mod_name_str,
920 module_path!(),
921 );
922 };
923
924 let result = quote! {
925 #[cfg(test)]
926 #[test_r::ctor::ctor]
927 fn #register_ident() {
928 #register_call
929 }
930 };
931
932 result.into()
933}
934
935fn type_to_string(typ: &Type, optional_tag: DependencyTag) -> String {
936 match typ {
937 Type::Array(array) => {
938 let inner_type = type_to_string(&array.elem, optional_tag);
939 format!("array_{}", inner_type)
940 }
941 Type::BareFn(_) => {
942 panic!("Function pointers are not supported in dependency injection")
943 }
944 Type::Group(group) => type_to_string(&group.elem, optional_tag),
945 Type::ImplTrait(impltrait) => {
946 let mut result = "impl".to_string();
947 for bound in &impltrait.bounds {
948 if let TypeParamBound::Trait(trait_bound) = bound {
949 result.push('_');
950 result.push_str(
951 &trait_bound
952 .path
953 .segments
954 .iter()
955 .map(|s| segment_to_string(s, DependencyTag::None))
956 .collect::<Vec<_>>()
957 .join("_"),
958 );
959 }
960 }
961 result
962 }
963 Type::Infer(_) => {
964 panic!("Type inference is not supported in dependency injection type signatures")
965 }
966 Type::Macro(_) => {
967 panic!("Macro invocations are not supported in dependency injection type signatures")
968 }
969 Type::Never(_) => "never".to_string(),
970 Type::Paren(inner) => type_to_string(&inner.elem, optional_tag),
971 Type::Path(path) => type_path_to_string(path, optional_tag),
972 Type::Ptr(inner) => {
973 let inner_type = type_to_string(&inner.elem, optional_tag);
974 format!("ptr_{}", inner_type)
975 }
976 Type::Reference(inner) => {
977 let inner_type = type_to_string(&inner.elem, optional_tag);
978 format!("ref_{}", inner_type)
979 }
980 Type::Slice(inner) => {
981 let inner_type = type_to_string(&inner.elem, optional_tag);
982 format!("slice_{}", inner_type)
983 }
984 Type::TraitObject(to) => {
985 let mut result = "dyn".to_string();
986 for bound in &to.bounds {
987 if let TypeParamBound::Trait(trait_bound) = bound {
988 result.push('_');
989 result.push_str(
990 &trait_bound
991 .path
992 .segments
993 .iter()
994 .map(|s| segment_to_string(s, DependencyTag::None))
995 .collect::<Vec<_>>()
996 .join("_"),
997 );
998 }
999 }
1000 if let DependencyTag::Tagged(tag) = optional_tag {
1001 result.push('_');
1002 result.push_str(&tag);
1003 }
1004 result
1005 }
1006 Type::Tuple(tuple) => {
1007 let inner_types = tuple
1008 .elems
1009 .iter()
1010 .map(|t| type_to_string(t, DependencyTag::None))
1011 .chain(optional_tag.into_iter())
1012 .collect::<Vec<_>>()
1013 .join("_");
1014 format!("tuple_{}", inner_types)
1015 }
1016 _ => "".to_string(),
1017 }
1018}
1019
1020fn type_path_to_string(dep_type: &TypePath, optional_tag: DependencyTag) -> String {
1021 let merged_ident = dep_type
1022 .path
1023 .segments
1024 .iter()
1025 .map(|s| segment_to_string(s, DependencyTag::None))
1026 .chain(optional_tag.into_iter())
1027 .collect::<Vec<_>>()
1028 .join("_");
1029 let dep_name = Ident::new(&merged_ident, Span::call_site());
1030 dep_name.to_string().to_lowercase()
1031}
1032
1033fn segment_to_string(segment: &PathSegment, optional_tag: DependencyTag) -> String {
1034 let mut result = segment.ident.to_string();
1035 match &segment.arguments {
1036 PathArguments::None => {}
1037 PathArguments::AngleBracketed(args) => {
1038 for arg in &args.args {
1039 result.push('_');
1040 result.push_str(&generic_argument_to_string(arg, optional_tag.clone()));
1041 }
1042 }
1043 PathArguments::Parenthesized(_args) => {
1044 panic!("Parenthesized type arguments are not supported - wrap the type in a newtype")
1045 }
1046 }
1047 result
1048}
1049
1050fn generic_argument_to_string(arg: &GenericArgument, optional_tag: DependencyTag) -> String {
1051 match arg {
1052 GenericArgument::Type(typ) => type_to_string(typ, optional_tag),
1053 GenericArgument::Const(_) => {
1054 panic!("Const generics are not supported in dependency injection")
1055 }
1056 GenericArgument::AssocType(_) => {
1057 panic!("Associated types are not supported in dependency injection")
1058 }
1059 GenericArgument::AssocConst(_) => {
1060 panic!("Associated constants are not supported in dependency injection")
1061 }
1062 GenericArgument::Constraint(_) => {
1063 panic!("Constraints are not supported in dependency injection; introduce a newtype")
1064 }
1065 _ => "".to_string(),
1066 }
1067}
1068
1069fn filter_custom_parameter_attributes(ast: &mut ItemFn) {
1071 ast.sig.inputs.iter_mut().for_each(|param| {
1072 if let FnArg::Typed(typed) = param {
1073 typed.attrs.retain(|attr| {
1074 !attr.path().is_ident("tagged_as") && !attr.path().is_ident("dimension")
1075 });
1076 }
1077 });
1078}
1079
1080fn get_lit_str_attr(attrs: &[Attribute], ident: &str) -> Option<String> {
1081 attrs
1082 .iter()
1083 .find(|attr| attr.path().is_ident(ident))
1084 .map(|attr| {
1085 let tag = attr
1086 .parse_args::<LitStr>()
1087 .unwrap_or_else(|_| panic!("{ident} attribute's parameter must be a string"));
1088 tag.value()
1089 })
1090}
1091
1092fn get_ident_attr(attrs: &[Attribute], ident: &str) -> Option<Ident> {
1093 attrs
1094 .iter()
1095 .find(|attr| attr.path().is_ident(ident))
1096 .map(|attr| {
1097 attr.parse_args::<Ident>()
1098 .unwrap_or_else(|_| panic!("{ident} attribute's parameter must be an identifier"))
1099 })
1100}
1101
1102fn get_dependency_params(
1103 ast: &ItemFn,
1104 is_bench: bool,
1105) -> (
1106 Vec<proc_macro2::TokenStream>,
1107 Vec<proc_macro2::TokenStream>,
1108 Vec<(usize, Ident)>,
1109) {
1110 let mut dep_getters = Vec::new();
1111 let mut dep_names = Vec::new();
1112 let mut dep_dimensions = Vec::new();
1113
1114 for (idx, param) in ast.sig.inputs.iter().enumerate() {
1115 if !is_bench || idx > 0 {
1116 let (dep_type, tag) = match param {
1118 FnArg::Receiver(_) => {
1119 panic!("Test functions cannot have a self parameter")
1120 }
1121 FnArg::Typed(typ) => {
1122 let tag_str = get_lit_str_attr(&typ.attrs, "tagged_as");
1123 let dim_str = get_ident_attr(&typ.attrs, "dimension");
1124
1125 let dep_tag = match (tag_str, dim_str) {
1126 (Some(tag), None) => DependencyTag::Tagged(tag),
1127 (None, Some(dim)) => DependencyTag::Matrix(dim),
1128 (Some(_), Some(_)) => panic!("Cannot have both a tag and a dimension attribute on the same test parameter"),
1129 (None, None) => DependencyTag::None,
1130 };
1131
1132 if let DependencyTag::Matrix(dim) = &dep_tag {
1133 dep_dimensions.push((idx, dim.clone()));
1134 }
1135
1136 let typ = get_dependency_param_from_pat_type(typ);
1137 (typ, dep_tag)
1138 }
1139 };
1140
1141 let dep_name_str = type_path_to_string(&dep_type, tag);
1142 let getter_ident = Ident::new(
1143 &format!("test_r_get_dep_{}", dep_name_str),
1144 Span::call_site(),
1145 );
1146
1147 dep_getters.push(quote! {
1148 &#getter_ident(&__test_r_deps_arg)
1149 });
1150 dep_names.push(quote! {
1151 #dep_name_str.to_string()
1152 });
1153 }
1154 }
1155 (dep_getters, dep_names, dep_dimensions)
1156}
1157
1158fn get_dependency_params_for_closure<'a>(
1159 ast: impl Iterator<Item = &'a Pat>,
1160) -> (
1161 Vec<proc_macro2::TokenStream>,
1162 Vec<proc_macro2::TokenStream>,
1163 Vec<Ident>,
1164) {
1165 let mut dep_getters = Vec::new();
1166 let mut dep_names = Vec::new();
1167 let mut bindings = Vec::new();
1168 for pat in ast {
1169 let (dep_type, tag) = match pat {
1170 Pat::Type(typ) => {
1171 let optional_tag = match get_lit_str_attr(&typ.attrs, "tagged_as") {
1172 Some(tag) => DependencyTag::Tagged(tag),
1173 None => DependencyTag::None,
1174 };
1175 (get_dependency_param_from_pat_type(typ), optional_tag)
1176 }
1177 _ => {
1178 panic!("Test functions can only have parameters which are immutable references to concrete types, but got {:?}", pat.to_token_stream())
1179 }
1181 };
1182 let dep_name_str = type_path_to_string(&dep_type, tag);
1183 let binding = match pat {
1184 Pat::Type(typ) => match &*typ.pat {
1185 Pat::Ident(ident) => ident.ident.clone(),
1186 _ => {
1187 panic!("Test functions can only have parameters which are immutable references to concrete types, but got {:?}", typ.pat.to_token_stream())
1188 }
1190 },
1191 _ => {
1192 panic!("Test functions can only have parameters which are immutable references to concrete types, but got {:?}", pat.to_token_stream())
1193 }
1195 };
1196
1197 let getter_ident = Ident::new(
1198 &format!("test_r_get_dep_{}", dep_name_str),
1199 Span::call_site(),
1200 );
1201
1202 dep_getters.push(quote! {
1203 &#getter_ident(&__test_r_deps_arg)
1204 });
1205 dep_names.push(quote! {
1206 #dep_name_str.to_string()
1207 });
1208 bindings.push(binding);
1209 }
1210 (dep_getters, dep_names, bindings)
1211}
1212
1213fn get_dependency_param_from_pat_type(typ: &PatType) -> TypePath {
1214 match &*typ.ty {
1215 Type::Reference(reference) => {
1216 match &*reference.elem {
1217 Type::Path(path) => path.clone(),
1218 _ => {
1219 panic!("Test functions can only have parameters which are immutable references to concrete types, but got {:?}", reference.elem.to_token_stream())
1220 }
1222 }
1223 }
1224 _ => {
1225 panic!("Test functions can only have parameters which are immutable references to concrete types, but got {:?}", typ.ty.to_token_stream())
1226 }
1228 }
1229}