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