diff --git a/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/src/lib.rs b/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/src/lib.rs index 09954100f..6aea48a3e 100644 --- a/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/src/lib.rs +++ b/rust/cargo_fuzztest/test_crates/another_sample_fuzz_crate/src/lib.rs @@ -11,7 +11,7 @@ fn another_sample_fuzztest_target(data: i32) { let _ = data; } -#[fuzztest(data = Arbitrary::::default())] +#[fuzztest(_data = Arbitrary::::default())] fn jobs_fuzztest_target(_data: i32) { println!("JOBS_TEST_PID: {}", std::process::id()); } diff --git a/rust/e2e_tests/testdata/fuzz_tests.rs b/rust/e2e_tests/testdata/fuzz_tests.rs index e725ff975..cfedd9640 100644 --- a/rust/e2e_tests/testdata/fuzz_tests.rs +++ b/rust/e2e_tests/testdata/fuzz_tests.rs @@ -181,5 +181,5 @@ impl Domain for FallibleDomain { } } -#[fuzztest(a = FallibleDomain::new())] +#[fuzztest(_a = FallibleDomain::new())] fn fallible_fuzz_test(_a: u32) {} diff --git a/rust/fuzztest_macro/src/helpers/test_registration.rs b/rust/fuzztest_macro/src/helpers/test_registration.rs index a17af8ecc..7fe486d8b 100644 --- a/rust/fuzztest_macro/src/helpers/test_registration.rs +++ b/rust/fuzztest_macro/src/helpers/test_registration.rs @@ -17,6 +17,8 @@ use convert_case::Casing; use proc_macro2::Span; use proc_macro2::TokenStream; use quote::quote; +use syn::parse::Parse; +use syn::parse::ParseStream; use syn::parse_quote; use syn::Expr; use syn::Generics; @@ -32,6 +34,19 @@ use super::TestFnArgument; const USER_VALUE_LIFETIME_GENERIC_NAME: &str = "'user"; +#[derive(Clone, Debug)] +pub struct FuzzTestArg { + pub ident: Ident, + pub _eq_token: syn::Token![=], + pub domain_ty: Expr, +} + +impl Parse for FuzzTestArg { + fn parse(input: ParseStream) -> syn::Result { + Ok(Self { ident: input.parse()?, _eq_token: input.parse()?, domain_ty: input.parse()? }) + } +} + #[derive(Clone, Debug)] pub struct FuzzTestObjectDefinitionAndFactory { pub fuzztest_object_factory_name: Ident, @@ -68,7 +83,7 @@ impl<'a> FuzzTestRegistrationCtx<'a> { /// pre-computes the tokenstream for the fuzz test struct instance. pub fn new( test_fn_signature: &'a Signature, - domain_ctors: impl IntoIterator, + fuzztest_args: impl IntoIterator, ) -> syn::Result { let user_value_lifetime_generic = Lifetime::new(USER_VALUE_LIFETIME_GENERIC_NAME, Span::call_site()); @@ -91,9 +106,33 @@ impl<'a> FuzzTestRegistrationCtx<'a> { )); } + let fuzztest_args = fuzztest_args.into_iter().collect::>(); + if fuzztest_args.len() != test_fn_args.len() { + return Err(syn::Error::new_spanned( + test_fn_signature, + format!( + "number of domain arguments in #[fuzztest] ({}) does not match number of property function parameters ({})", + fuzztest_args.len(), + test_fn_args.len() + ), + )); + } + + for (fuzztest_arg, test_fn_arg) in fuzztest_args.iter().zip(&test_fn_args) { + if fuzztest_arg.ident != *test_fn_arg.name { + return Err(syn::Error::new_spanned( + &fuzztest_arg.ident, + format!( + "domain argument name `{}` does not match property function parameter name `{}`", + fuzztest_arg.ident, test_fn_arg.name + ), + )); + } + } + let test_fn_ident = &test_fn_signature.ident; let prop_fn_ident = quote::format_ident!("__property_fn__{}", test_fn_ident); - let domain_ctors = domain_ctors.into_iter().collect::>(); + let domain_ctors = fuzztest_args.iter().map(|arg| &arg.domain_ty).collect::>(); let fuzz_test_struct_name = quote::format_ident!("__FuzzTest{}", test_fn_ident.to_string().to_case(Case::Pascal)); let crate_name = import_fuzztest_crate(); @@ -283,11 +322,11 @@ mod tests { #[googletest::test] fn test_generate_test_registration() { let sig: Signature = parse_quote! { fn test_fuzz(a: i32, b: std::string::String) }; - let expr1: Expr = - parse_quote! { ::fuzztest::domains::arbitrary::Arbitrary::::default() }; - let expr2: Expr = - parse_quote! { ::fuzztest::domains::arbitrary::Arbitrary::::default() }; - let context = FuzzTestRegistrationCtx::new(&sig, [&expr1, &expr2]).unwrap(); + let arg1: FuzzTestArg = + parse_quote! { a = ::fuzztest::domains::arbitrary::Arbitrary::::default() }; + let arg2: FuzzTestArg = + parse_quote! { b = ::fuzztest::domains::arbitrary::Arbitrary::::default() }; + let context = FuzzTestRegistrationCtx::new(&sig, [&arg1, &arg2]).unwrap(); let FuzzTestObjectDefinitionAndFactory { fuzztest_object_factory_name, fuzztest_object_tokenstream, @@ -360,11 +399,11 @@ mod tests { #[googletest::test] fn test_generate_gtest() { let sig: Signature = parse_quote! { fn test_fuzz(a: i32, b: std::string::String) }; - let expr1: Expr = - parse_quote! { ::fuzztest::domains::arbitrary::Arbitrary::::default() }; - let expr2: Expr = - parse_quote! { ::fuzztest::domains::arbitrary::Arbitrary::::default() }; - let context = FuzzTestRegistrationCtx::new(&sig, [&expr1, &expr2]).unwrap(); + let arg1: FuzzTestArg = + parse_quote! { a = ::fuzztest::domains::arbitrary::Arbitrary::::default() }; + let arg2: FuzzTestArg = + parse_quote! { b = ::fuzztest::domains::arbitrary::Arbitrary::::default() }; + let context = FuzzTestRegistrationCtx::new(&sig, [&arg1, &arg2]).unwrap(); let GTestDefinition { gtest_tokens } = context.generate_gtest().unwrap(); // test that the gtest was generated @@ -390,4 +429,36 @@ mod tests { ) ) } + + #[googletest::test] + fn test_mismatched_argument_count_fails() { + let sig: Signature = parse_quote! { fn test_fuzz(a: i32, b: std::string::String) }; + let arg1: FuzzTestArg = + parse_quote! { a = ::fuzztest::domains::arbitrary::Arbitrary::::default() }; + + let err = FuzzTestRegistrationCtx::new(&sig, [&arg1]).err().unwrap(); + expect_that!( + err.to_string(), + matchers::contains_substring( + "number of domain arguments in #[fuzztest] (1) does not match number of property function parameters (2)" + ) + ); + } + + #[googletest::test] + fn test_mismatched_argument_name_fails() { + let sig: Signature = parse_quote! { fn test_fuzz(a: i32, b: std::string::String) }; + let arg1: FuzzTestArg = + parse_quote! { a = ::fuzztest::domains::arbitrary::Arbitrary::::default() }; + let arg2: FuzzTestArg = + parse_quote! { wrong_name = ::fuzztest::domains::arbitrary::Arbitrary::::default() }; + + let err = FuzzTestRegistrationCtx::new(&sig, [&arg1, &arg2]).err().unwrap(); + expect_that!( + err.to_string(), + matchers::contains_substring( + "domain argument name `wrong_name` does not match property function parameter name `b`" + ) + ); + } } diff --git a/rust/fuzztest_macro/src/lib.rs b/rust/fuzztest_macro/src/lib.rs index d51935a85..a8b5f8994 100644 --- a/rust/fuzztest_macro/src/lib.rs +++ b/rust/fuzztest_macro/src/lib.rs @@ -20,26 +20,14 @@ mod helpers; use proc_macro::TokenStream; use quote::quote; -use syn::parse::{Parse, ParseStream, Parser}; +use syn::parse::Parser; use syn::{parse_macro_input, Item}; +use helpers::test_registration::FuzzTestArg; 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 { - 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. /// @@ -81,10 +69,8 @@ pub fn fuzztest(args: TokenStream, input: TokenStream) -> TokenStream { 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) { + match FuzzTestRegistrationCtx::new(&test_fn.sig, &args) { Ok(fuzztest_registration_context) => fuzztest_registration_context, Err(e) => return e.into_compile_error().into(), };