// Copyright 2024 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! Internal crate for use by [`derive_fuzztest`](../derive_fuzztest/index.html). See the //! documentation there for usage information. use proc_macro::TokenStream; use proc_macro2::TokenStream as TokenStream2; use quote::quote; use syn::{parse::Nothing, spanned::Spanned, ItemFn, Pat, PatType, Type}; /// Define a fuzz test. /// /// All input parameters of the given function must implement `arbitrary::Arbitrary`. /// /// This macro derives new items based on the given function. /// 1. A `fuzz_target!` is generated that can be used with `cargo fuzz`. /// 2. Property tests (`quickcheck` or `proptest`, based on which features are enabled) are /// generated that can be tested using `cargo test`. /// /// See the crate documentation [`derive_fuzztest`](../derive_fuzztest/index.html) for details. #[proc_macro_attribute] pub fn fuzztest(attr: TokenStream, item: TokenStream) -> TokenStream { fuzztest_impl(attr.into(), item.into()) .unwrap_or_else(|e| e.into_compile_error()) .into() } fn fuzztest_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result { syn::parse2::(attr)?; let func = syn::parse2::(item)?; let fn_def = FunctionDefinition::parse(func)?; let original_fn = &fn_def.func; let fuzz_target = derive_fuzz_target(&fn_def); let proptest_target = proptest::derive_proptest(&fn_def); let quickcheck_target = quickcheck::derive_quickcheck(&fn_def); Ok(quote! { #[allow(unused)] #original_fn #fuzz_target #proptest_target #quickcheck_target }) } /// Define a fuzz target only without corresponding test. /// /// All input parameters of the given function must implement `arbitrary::Arbitrary`. /// /// This macro derives a `fuzz_target!` that can be used with `cargo fuzz`. If you wish to generate /// property tests that can be used with `cargo test` as well, use [`fuzztest`][macro@fuzztest]. /// /// See the crate documentation [`derive_fuzztest`](../derive_fuzztest/index.html) for details. #[proc_macro_attribute] pub fn fuzz(attr: TokenStream, item: TokenStream) -> TokenStream { fuzz_impl(attr.into(), item.into()) .unwrap_or_else(|e| e.into_compile_error()) .into() } fn fuzz_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result { syn::parse2::(attr)?; let func = syn::parse2::(item)?; let fn_def = FunctionDefinition::parse(func)?; let original_fn = &fn_def.func; let fuzz_target = derive_fuzz_target(&fn_def); Ok(quote! { #[allow(unused)] #original_fn #fuzz_target }) } /// Define a property test. /// /// This is similar to using `quickcheck!` or `proptest::proptest!` directly. /// /// All input parameters of the given function must implement `arbitrary::Arbitrary`. /// /// Unlike [`fuzztest`][macro@fuzztest], this macro does not have to be placed in a `[[bin]]` target /// and a single file can contain multiple of these tests. The generated tests can be run with /// `cargo test` as usual. #[proc_macro_attribute] pub fn proptest(attr: TokenStream, item: TokenStream) -> TokenStream { proptest_impl(attr.into(), item.into()) .unwrap_or_else(|e| e.into_compile_error()) .into() } fn proptest_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result { syn::parse2::(attr)?; let func = syn::parse2::(item)?; let fn_def = FunctionDefinition::parse(func)?; let original_fn = &fn_def.func; let proptest_target = proptest::derive_proptest(&fn_def); Ok(quote! { #[allow(unused)] #original_fn #proptest_target }) } fn derive_fuzz_target(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream { let FunctionDefinition { func, args, types } = fn_def; let func_ident = &func.sig.ident; quote! { #[automatically_derived] #[cfg(fuzzing)] ::libfuzzer_sys::fuzz_target!(|args: ( #(#types),* )| { let ( #(#args),* ) = args; // https://github.com/rust-fuzz/libfuzzer/issues/77 #func_ident ( #(#args),* ) }); #[cfg(not(any(fuzzing, rust_analyzer)))] fn main() { ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead"); } } } #[cfg(any(feature = "quickcheck", test))] mod quickcheck { use crate::FunctionDefinition; use quote::quote; pub(crate) fn derive_quickcheck(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream { let FunctionDefinition { func, args, types } = fn_def; let func_ident = &func.sig.ident; let adapted_types: Vec<_> = types .iter() .map(|ty| quote! { ArbitraryAdapter<#ty> }) .collect(); let arg_pattern: Vec<_> = args .iter() .map(|arg| quote! { ArbitraryAdapter(::core::result::Result::Ok(#arg)) }) .collect(); let test_name = quote::format_ident!("quickcheck_{func_ident}"); quote! { #[automatically_derived] #[test] fn #test_name() { use ::derive_fuzztest::reexport::quickcheck::TestResult; use ::derive_fuzztest::arbitrary_bridge::ArbitraryAdapter; fn inner(args: (#(#adapted_types),*)) -> TestResult { let (#(#arg_pattern),*) = args else { return TestResult::discard() }; match ::std::panic::catch_unwind(move || { #func_ident ( #(#args),* ); }) { ::core::result::Result::Ok(()) => TestResult::passed(), ::core::result::Result::Err(e) => TestResult::error(::std::format!("{e:?}")), } } ::derive_fuzztest::reexport::quickcheck::QuickCheck::new().tests(1024) .quickcheck(inner as fn(_) -> TestResult); } } } } #[cfg(not(any(feature = "quickcheck", test)))] mod quickcheck { use crate::FunctionDefinition; pub(crate) fn derive_quickcheck(_fn_def: &FunctionDefinition) -> proc_macro2::TokenStream { proc_macro2::TokenStream::default() } } #[cfg(any(feature = "proptest", test))] mod proptest { use crate::FunctionDefinition; use quote::quote; use syn::{Ident, Signature}; pub(crate) fn derive_proptest(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream { let FunctionDefinition { func, args, types } = fn_def; let func_attrs = &func.attrs; let Signature { constness, asyncness, unsafety, abi, fn_token, ident, generics, paren_token: _, inputs: _, variadic: _, output, } = &func.sig; let proptest_ident = Ident::new(&format!("proptest_{ident}"), ident.span()); quote! { #[automatically_derived] #[cfg(test)] mod #proptest_ident { use super::*; use ::derive_fuzztest::reexport::proptest; use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb; proptest::proptest! { #![proptest_config(proptest::prelude::ProptestConfig { cases: 1024, failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))), ..Default::default() })] #[test] #(#func_attrs)* #constness #asyncness #unsafety #abi #fn_token #proptest_ident #generics ( args in arb::<(#(#types),*)>() ) #output { let (#(#args),*) = args; #ident ( #(#args),* ); } } } } } } #[cfg(not(any(feature = "proptest", test)))] mod proptest { use crate::FunctionDefinition; pub(crate) fn derive_proptest(_fn_def: &FunctionDefinition) -> proc_macro2::TokenStream { proc_macro2::TokenStream::default() } } /// Representation of a function definition annotated with one of the attribute macros in this /// crate. struct FunctionDefinition { func: ItemFn, args: Vec, types: Vec, } impl FunctionDefinition { pub fn parse(func: ItemFn) -> syn::Result { let (args, types) = func .sig .inputs .clone() .into_iter() .map(|arg| match arg { syn::FnArg::Receiver(arg_receiver) => Err(syn::Error::new( arg_receiver.span(), "Receiver not supported", )), syn::FnArg::Typed(PatType { attrs: _, pat, colon_token: _, ty, }) => Ok((*pat, *ty)), }) .try_fold((Vec::new(), Vec::new()), |(mut args, mut types), result| { result.map(|(arg, type_)| { args.push(arg); types.push(type_); (args, types) }) })?; Ok(Self { func, args, types }) } } #[cfg(test)] mod tests { use crate::{fuzz_impl, fuzztest_impl, proptest_impl}; use quote::quote; use syn::parse_quote; /// Assert that a token stream for a `syn::File` is the same as expected. /// /// Usage is similar to `assert_eq!`: /// ```no_run /// assert_syn_file!( /// macro_impl(quote! { /// fn foobar() {} /// }), /// quote! { /// fn macro_rewritten_foobar() {} /// } /// ); /// ``` macro_rules! assert_syn_file { ($actual:expr, $expected:expr) => { let actual = syn::parse2::($actual).unwrap(); let expected: syn::File = $expected; assert!( actual == expected, "{}", pretty_assertions::StrComparison::new( &prettyplease::unparse(&expected), &prettyplease::unparse(&actual), ) ) }; } #[test] fn test_fuzztest_expansion() { assert_syn_file!( fuzztest_impl( quote! {}, quote! { fn foobar(input: &[u8]) { panic!("I am just a test") } } ) .unwrap(), parse_quote! { #[allow(unused)] fn foobar(input: &[u8]) { panic!("I am just a test") } #[automatically_derived] #[cfg(fuzzing)] ::libfuzzer_sys::fuzz_target!(|args: (&[u8])| { let (input) = args; foobar(input) }); #[cfg(not(any(fuzzing, rust_analyzer)))] fn main() { ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead"); } #[automatically_derived] #[cfg(test)] mod proptest_foobar { use super::*; use ::derive_fuzztest::reexport::proptest; use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb; proptest::proptest! { #![proptest_config(proptest::prelude::ProptestConfig { cases: 1024, failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))), ..Default::default() })] #[test] fn proptest_foobar(args in arb::<(&[u8])>()) { let (input) = args; foobar(input); } } } #[automatically_derived] #[test] fn quickcheck_foobar() { use ::derive_fuzztest::reexport::quickcheck::TestResult; use ::derive_fuzztest::arbitrary_bridge::ArbitraryAdapter; fn inner(args: (ArbitraryAdapter<&[u8]>)) -> TestResult { let (ArbitraryAdapter(::core::result::Result::Ok(input))) = args else { return TestResult::discard() }; match ::std::panic::catch_unwind(move || { foobar(input); }) { ::core::result::Result::Ok(()) => TestResult::passed(), ::core::result::Result::Err(e) => TestResult::error(::std::format!("{e:?}")), } } ::derive_fuzztest::reexport::quickcheck::QuickCheck::new() .tests(1024) .quickcheck(inner as fn(_) -> TestResult); } } ); } #[test] fn test_fuzz_expansion() { assert_syn_file!( fuzz_impl( quote! {}, quote! { fn foobar(input: &[u8]) { panic!("I am just a test") } } ) .unwrap(), parse_quote! { #[allow(unused)] fn foobar(input: &[u8]) { panic!("I am just a test") } #[automatically_derived] #[cfg(fuzzing)] ::libfuzzer_sys::fuzz_target!(|args: (&[u8])| { let (input) = args; foobar(input) }); #[cfg(not(any(fuzzing, rust_analyzer)))] fn main() { ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead"); } } ); } #[test] fn test_proptest_expansion() { assert_syn_file!( proptest_impl( quote! {}, quote! { fn foobar(input: &[u8]) { panic!("I am just a test") } } ) .unwrap(), parse_quote! { #[allow(unused)] fn foobar(input: &[u8]) { panic!("I am just a test") } #[automatically_derived] #[cfg(test)] mod proptest_foobar { use super::*; use ::derive_fuzztest::reexport::proptest; use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb; proptest::proptest! { #![proptest_config(proptest::prelude::ProptestConfig { cases: 1024, failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))), ..Default::default() })] #[test] fn proptest_foobar(args in arb::<(&[u8])>()) { let (input) = args; foobar(input); } } } } ); } }