blob: 19450ea66a63a721c19f8064ecb5e57e2ec88610 [file]
/// Modules here cannot be public (ie: `pub`) because a proc-macro crate cannot export anything else
/// other than a proc-macro. That is to say, a function tagged with `#[proc_macro]`,
/// `#[proc_macro_attribute]`, `#[proc_macro_derive]`
mod helpers;
use proc_macro::TokenStream;
use quote::quote;
use syn::parse::{Parse, ParseStream, Parser};
use syn::{parse_macro_input, Item};
use helpers::test_registration::FuzzTestObjectDefinitionAndFactory;
use helpers::test_registration::FuzzTestRegistrationCtx;
use helpers::test_registration::GTestDefinition;
#[derive(Debug)]
struct FuzzTestArg {
_ident: syn::Ident,
_eq_token: syn::Token![=],
domain_ty: syn::Expr,
}
impl Parse for FuzzTestArg {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self { _ident: input.parse()?, _eq_token: input.parse()?, domain_ty: input.parse()? })
}
}
/// The `fuzztest` macro is used to generate and register a fuzz test object to be used by a fuzz
/// testing engine.
///
/// The macro takes a list of arguments, where each argument is a domain object used to generate
/// values for the property function inputs.
///
/// Example usage:
/// ```markdown
/// ```
/// # use fuzztest::fuzztest;
/// # use fuzztest::domains::arbitrary::Arbitrary;
/// # use fuzztest::domains::range::InRange;
///
/// #[fuzztest(a = Arbitrary::<i32>::default(), b = InRange::new(0, 10))]
/// fn fuzz_test_function(a: i32, b: i32) {
/// assert_eq!(a, b);
/// }
/// ```
/// ```
///
#[proc_macro_attribute]
pub fn fuzztest(args: TokenStream, input: TokenStream) -> TokenStream {
let item = parse_macro_input!(input as Item);
let syn::Item::Fn(mut test_fn) = item else {
return syn::Error::new_spanned(item, "not a function").into_compile_error().into();
};
let original_ident = test_fn.sig.ident.clone();
let fuzztest_name = original_ident.to_string();
// TODO(mathuxny-73): Support the case where there are no arguments. Probably we should return an error
// in this case as it may not make sense to have a FuzzTest without input.
let args =
syn::punctuated::Punctuated::<FuzzTestArg, syn::Token![,]>::parse_terminated.parse(args);
let args = match args {
Ok(args) => args,
Err(e) => return e.into_compile_error().into(),
};
let domain_ctors = args.iter().map(|arg| &arg.domain_ty);
let fuzztest_registration_context =
match FuzzTestRegistrationCtx::new(&test_fn.sig, domain_ctors) {
Ok(fuzztest_registration_context) => fuzztest_registration_context,
Err(e) => return e.into_compile_error().into(),
};
let FuzzTestObjectDefinitionAndFactory {
fuzztest_object_factory_name,
fuzztest_object_tokenstream,
} = match fuzztest_registration_context.generate_test_registration() {
Ok(fuzztest_object) => fuzztest_object,
Err(e) => return e.into_compile_error().into(),
};
let GTestDefinition { gtest_tokens } = match fuzztest_registration_context.generate_gtest() {
Ok(gtest_tokens) => gtest_tokens,
Err(e) => return e.into_compile_error().into(),
};
test_fn.sig.ident = fuzztest_registration_context.prop_fn_ident().clone();
let fuzztest_mod_name = quote::format_ident!("__fuzztest_mod__{}", original_ident);
// TODO(mathuxny-73): Extract the current module path from the invocation of the macro
// (ie: from `item.attrs`)
let tokens = quote! {
#[allow(non_snake_case)]
mod #fuzztest_mod_name {
use super::*;
#test_fn
#fuzztest_object_tokenstream
const _: () = {
static FUZZTEST_INFO: ::fuzztest::internal::FuzzTestInfo =
::fuzztest::internal::FuzzTestInfo {
name: #fuzztest_name,
file: file!(),
line: line!(),
};
::fuzztest::reexports::inventory::submit! {
::fuzztest::internal::FuzzTestRegistration {
info: &FUZZTEST_INFO,
factory: #fuzztest_object_factory_name
}
}
};
#gtest_tokens
}
};
tokens.into()
}