blob: beb692aad4cd1c8d90c6159f4ee4e93d742ab53b [file] [log] [blame]
// 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<TokenStream2> {
syn::parse2::<Nothing>(attr)?;
let func = syn::parse2::<ItemFn>(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<TokenStream2> {
syn::parse2::<Nothing>(attr)?;
let func = syn::parse2::<ItemFn>(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<TokenStream2> {
syn::parse2::<Nothing>(attr)?;
let func = syn::parse2::<ItemFn>(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<Pat>,
types: Vec<Type>,
}
impl FunctionDefinition {
pub fn parse(func: ItemFn) -> syn::Result<Self> {
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::<syn::File>($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);
}
}
}
}
);
}
}