Skip to main content

fickle_macros/
lib.rs

1use std::fmt::Display;
2use std::str::FromStr;
3
4use proc_macro::TokenStream;
5use quote::quote;
6use syn::punctuated::Punctuated;
7use syn::spanned::Spanned;
8use syn::{parse_macro_input, Expr, Item, Meta, Token};
9
10#[allow(clippy::test_attr_in_doctest)]
11/// Mark a test as fickle (AKA flaky).
12///
13/// This macro will run your test multiple times if it continues to fail. By default, it will only
14/// retry once. To override use the `retries` argument.
15///
16/// ```
17/// #[test]
18/// #[fickle]
19/// fn my_fickle_test() {}
20///
21/// #[test]
22/// #[fickle(retries=2)]
23/// fn more_fickle() {}
24/// ```
25///
26/// There is also the option to set a minimum number of passes. For example, to make sure it passes
27/// at least 2 out of every 3 tries, you can use the `passes` argument like so
28///
29/// ```
30/// #[test]
31/// #[fickle(retries=2, passes=2)]
32/// fn another() {}
33/// ```
34///
35/// It also works with tests that return [`Result`]s.
36///
37/// ```
38/// #[test]
39/// #[fickle]
40/// fn my_fickle_test() -> Result<(), ()> {
41///     Ok(())
42/// }
43/// ```
44#[proc_macro_attribute]
45pub fn fickle(attr: TokenStream, item: TokenStream) -> TokenStream {
46    // parse the attr args
47    let mut n_retries: Option<usize> = None;
48    let mut n_passes: Option<usize> = None;
49    let attrinp = parse_macro_input!(attr with Punctuated<Meta, Token![,]>::parse_terminated);
50    let attrspan = attrinp.span();
51    for a in attrinp {
52        match a {
53            Meta::NameValue(nv) => {
54                if nv.path.is_ident("retries") {
55                    match get_int(nv.value) {
56                        Ok(r) => n_retries = Some(r),
57                        Err(err) => return err.into_compile_error().into(),
58                    }
59                } else if nv.path.is_ident("passes") {
60                    match get_int(nv.value) {
61                        Ok(r) => n_passes = Some(r),
62                        Err(err) => return err.into_compile_error().into(),
63                    }
64                } else {
65                    return syn::Error::new(nv.path.span(), "Unrecognized argument")
66                        .into_compile_error()
67                        .into();
68                }
69            }
70            _ => {
71                return syn::Error::new(a.span(), "Not a name value (`name = value`)")
72                    .into_compile_error()
73                    .into();
74            }
75        }
76    }
77
78    let fickle_obj = match (n_retries, n_passes) {
79        (Some(retries), Some(passes)) => {
80            if passes > retries + 1 {
81                return syn::Error::new(
82                    attrspan,
83                    format!(
84                        "Requested a minimum of {} passes with a maximum of {} runs",
85                        passes,
86                        retries + 1
87                    ),
88                )
89                .into_compile_error()
90                .into();
91            }
92            quote! {
93                let fickle = fickle::Fickle::new(#retries, #passes).unwrap();
94            }
95        }
96        (Some(retries), None) => quote! {
97            let fickle = fickle::Fickle::new_retries(#retries).unwrap();
98        },
99        (None, Some(passes)) => {
100            if passes > 2 {
101                return syn::Error::new(
102                    attrspan,
103                    format!(
104                        "Requested a minimum of {} passes with a maximum of {} runs",
105                        passes, 2
106                    ),
107                )
108                .into_compile_error()
109                .into();
110            }
111            quote! {
112                let fickle = fickle::Fickle::new_passes(#passes).unwrap();
113            }
114        }
115        (None, None) => quote! {
116            let fickle = fickle::Fickle::default();
117        },
118    };
119
120    // parse the func
121    let iteminput = parse_macro_input!(item as Item);
122    let testfn = match iteminput {
123        Item::Fn(func) => func,
124        _ => {
125            return syn::Error::new(
126                iteminput.span(),
127                "Expected #[fickle] to annotate a function",
128            )
129            .into_compile_error()
130            .into();
131        }
132    };
133    let attrs = testfn.attrs;
134    let vis = testfn.vis;
135    let sig = testfn.sig;
136    let stmts = testfn.block.stmts;
137
138    let func_body = match &sig.output {
139        syn::ReturnType::Default => {
140            // these types of tests panic...
141            quote! {
142                {
143                    #fickle_obj
144                    let block: fn() -> () = || {
145                        #(#stmts)*
146                    };
147                    fickle.run(block).unwrap();
148                }
149            }
150        }
151        syn::ReturnType::Type(_rarrow, typ) => match **typ {
152            syn::Type::Path(_) => {
153                // this would be where tests that return Results end up
154                quote! {
155                    {
156                        #fickle_obj
157                        let block: fn() -> #typ = || {
158                            #(#stmts)*
159                        };
160                        match fickle.run(block) {
161                            Ok(res) => Ok(res),
162                            Err(fails) => panic!("{}", fails)
163                        }
164                    }
165                }
166            }
167            _ => {
168                return syn::Error::new(typ.span(), "Not able to handle return type")
169                    .into_compile_error()
170                    .into();
171            }
172        },
173    };
174
175    let expanded = quote! {
176        #(#attrs)*
177        #vis #sig
178        #func_body
179    };
180    expanded.into()
181}
182
183fn get_int<T>(expr: Expr) -> syn::Result<T>
184where
185    T: FromStr,
186    <T as FromStr>::Err: Display,
187{
188    match expr {
189        Expr::Lit(lit) => match lit.lit {
190            syn::Lit::Int(litint) => match litint.base10_parse() {
191                Ok(r) => Ok(r),
192                Err(_) => Err(syn::Error::new(litint.span(), "Could not parse to a usize")),
193            },
194            _ => Err(syn::Error::new(lit.lit.span(), "Not an integer")),
195        },
196        _ => Err(syn::Error::new(expr.span(), "Not an literal")),
197    }
198}