1 // Copyright 2024 Google LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 //     http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 //! Internal crate for use by [`derive_fuzztest`](../derive_fuzztest/index.html). See the
16 //! documentation there for usage information.
17 
18 use proc_macro::TokenStream;
19 use proc_macro2::TokenStream as TokenStream2;
20 use quote::quote;
21 use syn::{parse::Nothing, spanned::Spanned, ItemFn, Pat, PatType, Type};
22 
23 /// Define a fuzz test.
24 ///
25 /// All input parameters of the given function must implement `arbitrary::Arbitrary`.
26 ///
27 /// This macro derives new items based on the given function.
28 /// 1. A `fuzz_target!` is generated that can be used with `cargo fuzz`.
29 /// 2. Property tests (`quickcheck` or `proptest`, based on which features are enabled) are
30 ///    generated that can be tested using `cargo test`.
31 ///
32 /// See the crate documentation [`derive_fuzztest`](../derive_fuzztest/index.html) for details.
33 #[proc_macro_attribute]
fuzztest(attr: TokenStream, item: TokenStream) -> TokenStream34 pub fn fuzztest(attr: TokenStream, item: TokenStream) -> TokenStream {
35     fuzztest_impl(attr.into(), item.into())
36         .unwrap_or_else(|e| e.into_compile_error())
37         .into()
38 }
39 
fuzztest_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2>40 fn fuzztest_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
41     syn::parse2::<Nothing>(attr)?;
42     let func = syn::parse2::<ItemFn>(item)?;
43     let fn_def = FunctionDefinition::parse(func)?;
44     let original_fn = &fn_def.func;
45     let fuzz_target = derive_fuzz_target(&fn_def);
46     let proptest_target = proptest::derive_proptest(&fn_def);
47     let quickcheck_target = quickcheck::derive_quickcheck(&fn_def);
48 
49     Ok(quote! {
50         #[allow(unused)]
51         #original_fn
52         #fuzz_target
53         #proptest_target
54         #quickcheck_target
55     })
56 }
57 
58 /// Define a fuzz target only without corresponding test.
59 ///
60 /// All input parameters of the given function must implement `arbitrary::Arbitrary`.
61 ///
62 /// This macro derives a `fuzz_target!` that can be used with `cargo fuzz`. If you wish to generate
63 /// property tests that can be used with `cargo test` as well, use [`fuzztest`][macro@fuzztest].
64 ///
65 /// See the crate documentation [`derive_fuzztest`](../derive_fuzztest/index.html) for details.
66 #[proc_macro_attribute]
fuzz(attr: TokenStream, item: TokenStream) -> TokenStream67 pub fn fuzz(attr: TokenStream, item: TokenStream) -> TokenStream {
68     fuzz_impl(attr.into(), item.into())
69         .unwrap_or_else(|e| e.into_compile_error())
70         .into()
71 }
72 
fuzz_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2>73 fn fuzz_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
74     syn::parse2::<Nothing>(attr)?;
75     let func = syn::parse2::<ItemFn>(item)?;
76     let fn_def = FunctionDefinition::parse(func)?;
77     let original_fn = &fn_def.func;
78     let fuzz_target = derive_fuzz_target(&fn_def);
79 
80     Ok(quote! {
81         #[allow(unused)]
82         #original_fn
83         #fuzz_target
84     })
85 }
86 
87 /// Define a property test.
88 ///
89 /// This is similar to using `quickcheck!` or `proptest::proptest!` directly.
90 ///
91 /// All input parameters of the given function must implement `arbitrary::Arbitrary`.
92 ///
93 /// Unlike [`fuzztest`][macro@fuzztest], this macro does not have to be placed in a `[[bin]]` target
94 /// and a single file can contain multiple of these tests. The generated tests can be run with
95 /// `cargo test` as usual.
96 #[proc_macro_attribute]
proptest(attr: TokenStream, item: TokenStream) -> TokenStream97 pub fn proptest(attr: TokenStream, item: TokenStream) -> TokenStream {
98     proptest_impl(attr.into(), item.into())
99         .unwrap_or_else(|e| e.into_compile_error())
100         .into()
101 }
102 
proptest_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2>103 fn proptest_impl(attr: TokenStream2, item: TokenStream2) -> syn::Result<TokenStream2> {
104     syn::parse2::<Nothing>(attr)?;
105     let func = syn::parse2::<ItemFn>(item)?;
106     let fn_def = FunctionDefinition::parse(func)?;
107     let original_fn = &fn_def.func;
108     let proptest_target = proptest::derive_proptest(&fn_def);
109 
110     Ok(quote! {
111         #[allow(unused)]
112         #original_fn
113         #proptest_target
114     })
115 }
116 
derive_fuzz_target(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream117 fn derive_fuzz_target(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
118     let FunctionDefinition { func, args, types } = fn_def;
119     let func_ident = &func.sig.ident;
120     quote! {
121         #[automatically_derived]
122         #[cfg(fuzzing)]
123         ::libfuzzer_sys::fuzz_target!(|args: ( #(#types),* )| {
124             let ( #(#args),* ) = args;  // https://github.com/rust-fuzz/libfuzzer/issues/77
125             #func_ident ( #(#args),* )
126         });
127 
128         #[cfg(not(any(fuzzing, rust_analyzer)))]
129         fn main() {
130             ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead");
131         }
132     }
133 }
134 
135 #[cfg(any(feature = "quickcheck", test))]
136 mod quickcheck {
137     use crate::FunctionDefinition;
138     use quote::quote;
139 
derive_quickcheck(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream140     pub(crate) fn derive_quickcheck(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
141         let FunctionDefinition { func, args, types } = fn_def;
142         let func_ident = &func.sig.ident;
143         let adapted_types: Vec<_> = types
144             .iter()
145             .map(|ty| quote! { ArbitraryAdapter<#ty> })
146             .collect();
147         let arg_pattern: Vec<_> = args
148             .iter()
149             .map(|arg| quote! { ArbitraryAdapter(::core::result::Result::Ok(#arg)) })
150             .collect();
151         let test_name = quote::format_ident!("quickcheck_{func_ident}");
152         quote! {
153             #[automatically_derived]
154             #[test]
155             fn #test_name() {
156                 use ::derive_fuzztest::reexport::quickcheck::TestResult;
157                 use ::derive_fuzztest::arbitrary_bridge::ArbitraryAdapter;
158 
159                 fn inner(args: (#(#adapted_types),*)) -> TestResult {
160                     let (#(#arg_pattern),*) = args else { return TestResult::discard() };
161                     match ::std::panic::catch_unwind(move || {
162                         #func_ident ( #(#args),* );
163                     }) {
164                         ::core::result::Result::Ok(()) => TestResult::passed(),
165                         ::core::result::Result::Err(e) => TestResult::error(::std::format!("{e:?}")),
166                     }
167                 }
168 
169                 ::derive_fuzztest::reexport::quickcheck::QuickCheck::new().tests(1024)
170                     .quickcheck(inner as fn(_) -> TestResult);
171             }
172         }
173     }
174 }
175 
176 #[cfg(not(any(feature = "quickcheck", test)))]
177 mod quickcheck {
178     use crate::FunctionDefinition;
179 
derive_quickcheck(_fn_def: &FunctionDefinition) -> proc_macro2::TokenStream180     pub(crate) fn derive_quickcheck(_fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
181         proc_macro2::TokenStream::default()
182     }
183 }
184 
185 #[cfg(any(feature = "proptest", test))]
186 mod proptest {
187     use crate::FunctionDefinition;
188     use quote::quote;
189     use syn::{Ident, Signature};
190 
derive_proptest(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream191     pub(crate) fn derive_proptest(fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
192         let FunctionDefinition { func, args, types } = fn_def;
193         let func_attrs = &func.attrs;
194         let Signature {
195             constness,
196             asyncness,
197             unsafety,
198             abi,
199             fn_token,
200             ident,
201             generics,
202             paren_token: _,
203             inputs: _,
204             variadic: _,
205             output,
206         } = &func.sig;
207         let proptest_ident = Ident::new(&format!("proptest_{ident}"), ident.span());
208         quote! {
209             #[automatically_derived]
210             #[cfg(test)]
211             mod #proptest_ident {
212                 use super::*;
213                 use ::derive_fuzztest::reexport::proptest;
214                 use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb;
215 
216                 proptest::proptest! {
217                     #![proptest_config(proptest::prelude::ProptestConfig {
218                         cases: 1024,
219                         failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))),
220                         ..Default::default()
221                     })]
222                     #[test]
223                     #(#func_attrs)*
224                     #constness #asyncness #unsafety #abi #fn_token #proptest_ident #generics ( args in arb::<(#(#types),*)>() ) #output {
225                         let (#(#args),*) = args;
226                         #ident ( #(#args),* );
227                     }
228                 }
229             }
230         }
231     }
232 }
233 
234 #[cfg(not(any(feature = "proptest", test)))]
235 mod proptest {
236     use crate::FunctionDefinition;
237 
derive_proptest(_fn_def: &FunctionDefinition) -> proc_macro2::TokenStream238     pub(crate) fn derive_proptest(_fn_def: &FunctionDefinition) -> proc_macro2::TokenStream {
239         proc_macro2::TokenStream::default()
240     }
241 }
242 
243 /// Representation of a function definition annotated with one of the attribute macros in this
244 /// crate.
245 struct FunctionDefinition {
246     func: ItemFn,
247     args: Vec<Pat>,
248     types: Vec<Type>,
249 }
250 
251 impl FunctionDefinition {
parse(func: ItemFn) -> syn::Result<Self>252     pub fn parse(func: ItemFn) -> syn::Result<Self> {
253         let (args, types) = func
254             .sig
255             .inputs
256             .clone()
257             .into_iter()
258             .map(|arg| match arg {
259                 syn::FnArg::Receiver(arg_receiver) => Err(syn::Error::new(
260                     arg_receiver.span(),
261                     "Receiver not supported",
262                 )),
263                 syn::FnArg::Typed(PatType {
264                     attrs: _,
265                     pat,
266                     colon_token: _,
267                     ty,
268                 }) => Ok((*pat, *ty)),
269             })
270             .try_fold((Vec::new(), Vec::new()), |(mut args, mut types), result| {
271                 result.map(|(arg, type_)| {
272                     args.push(arg);
273                     types.push(type_);
274                     (args, types)
275                 })
276             })?;
277         Ok(Self { func, args, types })
278     }
279 }
280 
281 #[cfg(test)]
282 mod tests {
283     use crate::{fuzz_impl, fuzztest_impl, proptest_impl};
284     use quote::quote;
285     use syn::parse_quote;
286 
287     /// Assert that a token stream for a `syn::File` is the same as expected.
288     ///
289     /// Usage is similar to `assert_eq!`:
290     /// ```no_run
291     /// assert_syn_file!(
292     ///     macro_impl(quote! {
293     ///         fn foobar() {}
294     ///     }),
295     ///     quote! {
296     ///         fn macro_rewritten_foobar() {}
297     ///     }
298     /// );
299     /// ```
300     macro_rules! assert_syn_file {
301         ($actual:expr, $expected:expr) => {
302             let actual = syn::parse2::<syn::File>($actual).unwrap();
303             let expected: syn::File = $expected;
304             assert!(
305                 actual == expected,
306                 "{}",
307                 pretty_assertions::StrComparison::new(
308                     &prettyplease::unparse(&expected),
309                     &prettyplease::unparse(&actual),
310                 )
311             )
312         };
313     }
314 
315     #[test]
test_fuzztest_expansion()316     fn test_fuzztest_expansion() {
317         assert_syn_file!(
318             fuzztest_impl(
319                 quote! {},
320                 quote! {
321                     fn foobar(input: &[u8]) {
322                         panic!("I am just a test")
323                     }
324                 }
325             )
326             .unwrap(),
327             parse_quote! {
328                 #[allow(unused)]
329                 fn foobar(input: &[u8]) {
330                     panic!("I am just a test")
331                 }
332 
333                 #[automatically_derived]
334                 #[cfg(fuzzing)]
335                 ::libfuzzer_sys::fuzz_target!(|args: (&[u8])| {
336                     let (input) = args;
337                     foobar(input)
338                 });
339 
340                 #[cfg(not(any(fuzzing, rust_analyzer)))]
341                 fn main() {
342                     ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead");
343                 }
344 
345                 #[automatically_derived]
346                 #[cfg(test)]
347                 mod proptest_foobar {
348                     use super::*;
349                     use ::derive_fuzztest::reexport::proptest;
350                     use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb;
351                     proptest::proptest! {
352                         #![proptest_config(proptest::prelude::ProptestConfig {
353                             cases: 1024,
354                             failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))),
355                             ..Default::default()
356                         })]
357                         #[test]
358                         fn proptest_foobar(args in arb::<(&[u8])>()) {
359                             let (input) = args;
360                             foobar(input);
361                         }
362                     }
363                 }
364 
365                 #[automatically_derived]
366                 #[test]
367                 fn quickcheck_foobar() {
368                     use ::derive_fuzztest::reexport::quickcheck::TestResult;
369                     use ::derive_fuzztest::arbitrary_bridge::ArbitraryAdapter;
370 
371                     fn inner(args: (ArbitraryAdapter<&[u8]>)) -> TestResult {
372                         let (ArbitraryAdapter(::core::result::Result::Ok(input))) = args else {
373                             return TestResult::discard()
374                         };
375                         match ::std::panic::catch_unwind(move || {
376                             foobar(input);
377                         }) {
378                             ::core::result::Result::Ok(()) => TestResult::passed(),
379                             ::core::result::Result::Err(e) => TestResult::error(::std::format!("{e:?}")),
380                         }
381                     }
382                     ::derive_fuzztest::reexport::quickcheck::QuickCheck::new()
383                         .tests(1024)
384                         .quickcheck(inner as fn(_) -> TestResult);
385                 }
386             }
387         );
388     }
389 
390     #[test]
test_fuzz_expansion()391     fn test_fuzz_expansion() {
392         assert_syn_file!(
393             fuzz_impl(
394                 quote! {},
395                 quote! {
396                     fn foobar(input: &[u8]) {
397                         panic!("I am just a test")
398                     }
399                 }
400             )
401             .unwrap(),
402             parse_quote! {
403                 #[allow(unused)]
404                 fn foobar(input: &[u8]) {
405                     panic!("I am just a test")
406                 }
407 
408                 #[automatically_derived]
409                 #[cfg(fuzzing)]
410                 ::libfuzzer_sys::fuzz_target!(|args: (&[u8])| {
411                     let (input) = args;
412                     foobar(input)
413                 });
414 
415                 #[cfg(not(any(fuzzing, rust_analyzer)))]
416                 fn main() {
417                     ::std::unreachable!("Run this target with `cargo fuzz` or `cargo test` instead");
418                 }
419             }
420         );
421     }
422 
423     #[test]
test_proptest_expansion()424     fn test_proptest_expansion() {
425         assert_syn_file!(
426             proptest_impl(
427                 quote! {},
428                 quote! {
429                     fn foobar(input: &[u8]) {
430                         panic!("I am just a test")
431                     }
432                 }
433             )
434             .unwrap(),
435             parse_quote! {
436                 #[allow(unused)]
437                 fn foobar(input: &[u8]) {
438                     panic!("I am just a test")
439                 }
440 
441                 #[automatically_derived]
442                 #[cfg(test)]
443                 mod proptest_foobar {
444                     use super::*;
445                     use ::derive_fuzztest::reexport::proptest;
446                     use ::derive_fuzztest::reexport::proptest_arbitrary_interop::arb;
447                     proptest::proptest! {
448                         #![proptest_config(proptest::prelude::ProptestConfig {
449                             cases: 1024,
450                             failure_persistence: Some(Box::new(proptest::test_runner::FileFailurePersistence::WithSource("regression"))),
451                             ..Default::default()
452                         })]
453                         #[test]
454                         fn proptest_foobar(args in arb::<(&[u8])>()) {
455                             let (input) = args;
456                             foobar(input);
457                         }
458                     }
459                 }
460             }
461         );
462     }
463 }
464