propfuzz-macro/src/config.rs (194 lines of code) (raw):

// Copyright (c) The propfuzz Contributors // SPDX-License-Identifier: MIT OR Apache-2.0 //! Configuration for propfuzz macros. use crate::errors::{Error, ErrorList, Result}; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens}; use syn::punctuated::Punctuated; use syn::{spanned::Spanned, Attribute, Expr, Lit, Meta, MetaNameValue, NestedMeta, Token, Type}; // --- // Config builders // --- pub(crate) trait ConfigBuilder { fn apply_meta(&mut self, meta: &Meta, errors: &mut ErrorList); /// Applies the given #[propfuzz] attributes. fn apply_attrs<'a>( &mut self, attrs: impl IntoIterator<Item = &'a Attribute>, errors: &mut ErrorList, ) { for attr in attrs { if let Some(args) = errors.combine_opt(|| { attr.parse_args_with(Punctuated::<NestedMeta, Token![,]>::parse_terminated) }) { self.apply_args(&args, errors); } } } /// Applies the given arguments. fn apply_args<'a>( &mut self, args: impl IntoIterator<Item = &'a NestedMeta>, errors: &mut ErrorList, ) { for arg in args { self.apply_arg(arg, errors) } } /// Applies a single argument. fn apply_arg(&mut self, arg: &NestedMeta, errors: &mut ErrorList) { match arg { NestedMeta::Meta(meta) => { self.apply_meta(meta, errors); } NestedMeta::Lit(meta) => { errors.combine(Error::new_spanned(meta, "expected key = value format")); } } } } // --- // Config for a function // --- /// Configuration for a propfuzz target. #[derive(Debug, Default)] pub(crate) struct PropfuzzConfigBuilder { fuzz_default: Option<bool>, proptest: ProptestConfig, } impl PropfuzzConfigBuilder { /// Completes building args and returns a `PropfuzzConfig`. pub(crate) fn finish(self) -> PropfuzzConfig { PropfuzzConfig { fuzz_default: self.fuzz_default.unwrap_or(false), proptest: self.proptest, } } } impl ConfigBuilder for PropfuzzConfigBuilder { fn apply_meta(&mut self, meta: &Meta, errors: &mut ErrorList) { let path = meta.path(); if path.is_ident("fuzz_default") { errors.combine_fn(|| { replace_empty(meta.span(), &mut self.fuzz_default, read_bool(meta)?) }); } else if path.is_ident("cases") { errors.combine_fn(|| { replace_empty(meta.span(), &mut self.proptest.cases, read_u32(meta)?) }); } else if path.is_ident("max_local_rejects") { errors.combine_fn(|| { replace_empty( meta.span(), &mut self.proptest.max_local_rejects, read_u32(meta)?, ) }); } else if path.is_ident("max_global_rejects") { errors.combine_fn(|| { replace_empty( meta.span(), &mut self.proptest.max_global_rejects, read_u32(meta)?, ) }); } else if path.is_ident("max_flat_map_regens") { errors.combine_fn(|| { replace_empty( meta.span(), &mut self.proptest.max_flat_map_regens, read_u32(meta)?, ) }); } else if path.is_ident("fork") { errors.combine_fn(|| { replace_empty(meta.span(), &mut self.proptest.fork, read_bool(meta)?) }); } else if path.is_ident("timeout") { errors.combine_fn(|| { replace_empty(meta.span(), &mut self.proptest.timeout, read_u32(meta)?) }); } else if path.is_ident("max_shrink_time") { errors.combine_fn(|| { replace_empty( meta.span(), &mut self.proptest.max_shrink_time, read_u32(meta)?, ) }); } else if path.is_ident("max_shrink_iters") { errors.combine_fn(|| { replace_empty( meta.span(), &mut self.proptest.max_shrink_iters, read_u32(meta)?, ) }); } else if path.is_ident("verbose") { errors.combine_fn(|| { replace_empty(meta.span(), &mut self.proptest.verbose, read_u32(meta)?) }); } else { errors.combine(Error::new_spanned(path, "argument not recognized")); } } } /// Overall config for a single propfuzz function, fully built. #[derive(Debug)] pub(crate) struct PropfuzzConfig { // fuzz_default is currently unused. fuzz_default: bool, pub(crate) proptest: ProptestConfig, } /// Proptest config for a single propfuzz function. /// /// This contains most of the settings in proptest's config. #[derive(Debug, Default)] pub(crate) struct ProptestConfig { cases: Option<u32>, max_local_rejects: Option<u32>, max_global_rejects: Option<u32>, max_flat_map_regens: Option<u32>, fork: Option<bool>, timeout: Option<u32>, max_shrink_time: Option<u32>, max_shrink_iters: Option<u32>, verbose: Option<u32>, } macro_rules! extend_config { ($tokens:ident, $var:ident) => { if let Some($var) = $var { $tokens.extend(quote! { config.$var = #$var; }) } }; } /// Generates a ProptestConfig for this function. impl ToTokens for ProptestConfig { fn to_tokens(&self, tokens: &mut TokenStream) { let Self { cases, max_local_rejects, max_global_rejects, max_flat_map_regens, fork, timeout, max_shrink_time, max_shrink_iters, verbose, } = self; tokens.extend(quote! { let mut config = ::propfuzz::proptest::test_runner::Config::default(); config.source_file = Some(file!()); }); extend_config!(tokens, cases); extend_config!(tokens, max_local_rejects); extend_config!(tokens, max_global_rejects); extend_config!(tokens, max_flat_map_regens); extend_config!(tokens, fork); extend_config!(tokens, timeout); extend_config!(tokens, max_shrink_time); extend_config!(tokens, max_shrink_iters); extend_config!(tokens, verbose); tokens.extend(quote! { config }) } } // --- // Configuration for arguments // --- pub(crate) struct ParamConfigBuilder<'a> { ty: &'a Type, strategy: Option<Expr>, } impl<'a> ParamConfigBuilder<'a> { pub(crate) fn new(ty: &'a Type) -> Self { Self { ty, strategy: None } } pub(crate) fn finish(self) -> ParamConfig { let ty = self.ty; let strategy = match self.strategy { Some(expr) => quote! { #expr }, None => quote! { ::propfuzz::proptest::arbitrary::any::<#ty>() }, }; ParamConfig { strategy } } } impl<'a> ConfigBuilder for ParamConfigBuilder<'a> { fn apply_meta(&mut self, meta: &Meta, errors: &mut ErrorList) { let path = meta.path(); if path.is_ident("strategy") { errors.combine_fn(|| replace_empty(meta.span(), &mut self.strategy, read_expr(meta)?)); } else { errors.combine(Error::new_spanned(path, "argument not recognized")); } } } #[derive(Debug)] pub(crate) struct ParamConfig { strategy: TokenStream, } impl ParamConfig { pub(crate) fn strategy(&self) -> &TokenStream { &self.strategy } } // --- // Helper functions // --- fn read_bool(meta: &Meta) -> Result<bool> { let name_value = name_value(meta)?; match &name_value.lit { Lit::Bool(lit) => Ok(lit.value), _ => Err(Error::new_spanned(&name_value.lit, "expected bool")), } } fn read_u32(meta: &Meta) -> Result<u32> { let name_value = name_value(meta)?; match &name_value.lit { Lit::Int(lit) => Ok(lit.base10_parse::<u32>()?), _ => Err(Error::new_spanned(&name_value.lit, "expected integer")), } } fn read_expr(meta: &Meta) -> Result<Expr> { let name_value = name_value(meta)?; match &name_value.lit { Lit::Str(lit) => Ok(lit.parse::<Expr>()?), _ => Err(Error::new_spanned( &name_value.lit, "expected expression string", )), } } fn name_value(meta: &Meta) -> Result<&MetaNameValue> { match meta { Meta::NameValue(meta) => Ok(meta), _ => Err(Error::new_spanned(meta, "expected key = value format")), } } fn replace_empty<T>(span: Span, dest: &mut Option<T>, val: T) -> Result<()> { if dest.replace(val).is_some() { Err(Error::new(span, "key specified more than once")) } else { Ok(()) } }