fickle_macros/
lib.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
use proc_macro::TokenStream;
use quote::quote;
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::{parse_macro_input, Expr, Item, Meta, Token};

#[allow(clippy::test_attr_in_doctest)]
/// Mark a test as fickle (AKA flaky).
///
/// This macro will run your test multiple times if it continues to fail. By default, it will only
/// retry once. To override use the `retries` argument.
///
/// ```
/// #[test]
/// #[fickle]
/// fn my_fickle_test() {}
///
/// #[test]
/// #[fickle(retries=2)]
/// fn more_fickle() {}
/// ```
///
/// It also works with tests that return [`Result`]s.
///
/// ```
/// #[test]
/// #[fickle]
/// fn my_fickle_test() -> Result<(), ()> {
///     Ok(())
/// }
/// ```
#[proc_macro_attribute]
pub fn fickle(attr: TokenStream, item: TokenStream) -> TokenStream {
    // parse the attr args
    let mut n_retries: Option<usize> = None;
    let attrinp = parse_macro_input!(attr with Punctuated<Meta, Token![,]>::parse_terminated);
    for a in attrinp {
        match a {
            Meta::NameValue(nv) => {
                if nv.path.is_ident("retries") {
                    match nv.value {
                        Expr::Lit(lit) => match lit.lit {
                            syn::Lit::Int(litint) => match litint.base10_parse() {
                                Ok(r) => n_retries = Some(r),
                                Err(_) => {
                                    return syn::Error::new(
                                        litint.span(),
                                        "Could not parse to a usize",
                                    )
                                    .into_compile_error()
                                    .into();
                                }
                            },
                            _ => {
                                return syn::Error::new(lit.lit.span(), "Not an integer")
                                    .into_compile_error()
                                    .into();
                            }
                        },
                        _ => {
                            return syn::Error::new(nv.value.span(), "Not an literal")
                                .into_compile_error()
                                .into();
                        }
                    }
                } else {
                    return syn::Error::new(nv.path.span(), "Unrecognized argument")
                        .into_compile_error()
                        .into();
                }
            }
            _ => {
                return syn::Error::new(a.span(), "Not a name value (`name = value`)")
                    .into_compile_error()
                    .into();
            }
        }
    }
    let fickle_obj = if let Some(retries) = n_retries {
        quote! {
            let fickle = fickle::Fickle::new(#retries);
        }
    } else {
        quote! {
            let fickle = fickle::Fickle::default();
        }
    };

    // parse the func
    let iteminput = parse_macro_input!(item as Item);
    let testfn = match iteminput {
        Item::Fn(func) => func,
        _ => {
            return syn::Error::new(
                iteminput.span(),
                "Expected #[fickle] to annotate a function",
            )
            .into_compile_error()
            .into();
        }
    };
    let attrs = testfn.attrs;
    let vis = testfn.vis;
    let sig = testfn.sig;
    let stmts = testfn.block.stmts;

    let func_body = match &sig.output {
        syn::ReturnType::Default => {
            // these types of tests panic...
            quote! {
                {
                    #fickle_obj
                    let block: fn() -> () = || {
                        #(#stmts)*
                    };
                    fickle.run(block).unwrap();
                }
            }
        }
        syn::ReturnType::Type(_rarrow, typ) => match **typ {
            syn::Type::Path(_) => {
                // this would be where tests that return Results end up
                quote! {
                    {
                        #fickle_obj
                        let block: fn() -> #typ = || {
                            #(#stmts)*
                        };
                        match fickle.run(block) {
                            Ok(res) => Ok(res),
                            Err(fails) => panic!("{}", fails)
                        }
                    }
                }
            }
            _ => {
                return syn::Error::new(typ.span(), "Not able to handle return type")
                    .into_compile_error()
                    .into();
            }
        },
    };

    let expanded = quote! {
        #(#attrs)*
        #vis #sig
        #func_body
    };
    expanded.into()
}