fickle-macros 0.2.1

Tools for handling fickle (flaky) tests in rust.
Documentation
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()
}