test-strategy 0.3.0

Procedural macro to easily write higher-order strategies in proptest.
Documentation
use crate::syn_utils::{Arg, Args};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{
    parse2, parse_quote, parse_str, spanned::Spanned, token, Field, FnArg, Ident, ItemFn, Pat,
    Result, Visibility,
};

pub fn build_proptest(attr: TokenStream, mut item_fn: ItemFn) -> Result<TokenStream> {
    let mut attr_args = None;
    if !attr.is_empty() {
        attr_args = Some(parse2::<Args>(attr)?);
    }
    let mut dump = false;
    item_fn.attrs.retain(|attr| {
        if attr.path.is_ident("proptest_dump") {
            dump = true;
            false
        } else {
            true
        }
    });
    let args_type_str = format!("_{}Args", to_camel_case(&item_fn.sig.ident.to_string()));
    let args_type_ident: Ident = parse_str(&args_type_str).unwrap();
    let args = item_fn
        .sig
        .inputs
        .iter()
        .map(TestFnArg::from)
        .collect::<Result<Vec<_>>>()?;
    let args_pats = args.iter().map(|arg| arg.pat());
    let block = &item_fn.block;
    let block = quote! {
        {
            let #args_type_ident { #(#args_pats,)* } = input;
            #block
        }
    };
    item_fn.sig.inputs = parse_quote! { input: #args_type_ident };
    item_fn.block = Box::new(parse2(block)?);
    let args_fields = args.iter().map(|arg| &arg.field);
    let config = to_proptest_config(attr_args);
    let ts = quote! {
        #[derive(test_strategy::Arbitrary, Debug)]
        struct #args_type_ident {
            #(#args_fields,)*
        }
        proptest::proptest! {
            #config
            #[test]
            #item_fn
        }
    };
    if dump {
        panic!("{}", ts);
    }
    Ok(ts)
}

fn to_proptest_config(args: Option<Args>) -> TokenStream {
    if let Some(args) = args {
        let mut base_expr = None;
        let mut inits = Vec::new();
        for arg in args {
            match arg {
                Arg::Value(value) => base_expr = Some(value),
                Arg::NameValue { name, value, .. } => inits.push(quote!(#name : #value)),
            }
        }
        let base_expr = base_expr.unwrap_or_else(|| {
            parse_quote!(<proptest::test_runner::Config as std::default::Default>::default())
        });
        quote! {
            #![proptest_config(proptest::test_runner::Config {
                #(#inits,)*
                .. #base_expr
              })]
        }
    } else {
        quote! {}
    }
}
struct TestFnArg {
    field: Field,
    mutability: Option<token::Mut>,
}
impl TestFnArg {
    fn from(arg: &FnArg) -> Result<Self> {
        if let FnArg::Typed(arg) = arg {
            if let Pat::Ident(ident) = arg.pat.as_ref() {
                if ident.attrs.is_empty() && ident.by_ref.is_none() && ident.subpat.is_none() {
                    return Ok(Self {
                        field: Field {
                            attrs: arg.attrs.clone(),
                            vis: Visibility::Inherited,
                            ident: Some(ident.ident.clone()),
                            colon_token: Some(arg.colon_token),
                            ty: arg.ty.as_ref().clone(),
                        },
                        mutability: ident.mutability,
                    });
                }
            } else {
                bail!(arg.pat.span(), "argument pattern not supported.");
            }
        }
        bail!(
            arg.span(),
            "argument {} is not supported.",
            arg.to_token_stream()
        );
    }
    fn pat(&self) -> TokenStream {
        let mutability = &self.mutability;
        let ident = &self.field.ident;
        quote!(#mutability #ident)
    }
}

fn to_camel_case(s: &str) -> String {
    let mut upper = true;
    let mut r = String::new();
    for c in s.chars() {
        if c == '_' {
            upper = true;
        } else if upper {
            r.push_str(&c.to_uppercase().to_string());
            upper = false;
        } else {
            r.push(c);
        }
    }
    r
}