test_r_macro/
lib.rs

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        // Dependency matrix, generating a test generator
267        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 = &params[0];
736    let name_expr = &params[1];
737    let test_props_expr = &params[2];
738
739    let function_expr = &params[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        // applied to a test function
860        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
1069/// Removes custom attributes from parameters that are only interpreted by the #[test] macro
1070fn 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            // TODO: verify that the first bench arg is a Bencher/AsyncBencher
1117            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                // TODO: nicer error report
1180            }
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                    // TODO: nicer error report
1189                }
1190            },
1191            _ => {
1192                panic!("Test functions can only have parameters which are immutable references to concrete types, but got {:?}", pat.to_token_stream())
1193                // TODO: nicer error report
1194            }
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                    // TODO: nicer error report
1221                }
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            // TODO: nicer error report
1227        }
1228    }
1229}