quickcheck_async/
lib.rs

1// Copyright 2020 nytopop (Eric Izoita)
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// http://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7//! Runtime-agnostic attribute macros to use quickcheck with async tests.
8#![warn(rust_2018_idioms, missing_docs)]
9
10use proc_macro::TokenStream;
11use quote::{format_ident, quote};
12use syn::{
13    parse_macro_input, punctuated::Punctuated, token::Comma, AttributeArgs, Error, FnArg, ItemFn,
14    NestedMeta, Pat, Type,
15};
16
17struct Arguments {
18    ids: Punctuated<Pat, Comma>,
19    tys: Punctuated<Type, Comma>,
20}
21
22fn parse_args(fn_item: &ItemFn) -> Result<Arguments, TokenStream> {
23    let mut args = Arguments {
24        ids: Punctuated::new(),
25        tys: Punctuated::new(),
26    };
27
28    for pt in fn_item.sig.inputs.iter() {
29        match pt {
30            FnArg::Receiver(_) => {
31                return Err(
32                    Error::new_spanned(&fn_item, "test fn cannot take a receiver")
33                        .to_compile_error()
34                        .into(),
35                )
36            }
37
38            FnArg::Typed(pt) => {
39                args.ids.push(*pt.pat.clone());
40                args.tys.push(*pt.ty.clone());
41            }
42        }
43    }
44
45    Ok(args)
46}
47
48/// Mark an async function to be fuzz-tested using [quickcheck][qc], within a tokio
49/// executor.
50///
51/// # Usage
52///
53/// ```
54/// #[quickcheck_async::tokio]
55/// async fn fuzz_me(fuzz_arg: String) -> bool {
56///     fuzz_arg != "fuzzed".to_owned()
57/// }
58/// ```
59///
60/// # Attribute arguments
61///
62/// Arguments to this attribute are passed through to [tokio::test][tt].
63///
64/// ```
65/// #[quickcheck_async::tokio(core_threads = 3)]
66/// async fn fuzz_me(fuzz_arg: String) -> bool {
67///     fuzz_arg != "fuzzed".to_owned()
68/// }
69/// ```
70/// [qc]: https://docs.rs/quickcheck/latest/quickcheck/fn.quickcheck.html
71/// [tt]: https://docs.rs/tokio/latest/tokio/attr.test.html
72#[proc_macro_attribute]
73pub fn tokio(args: TokenStream, item: TokenStream) -> TokenStream {
74    let fn_item = parse_macro_input!(item as ItemFn);
75
76    for attr in &fn_item.attrs {
77        if attr.path.is_ident("test") {
78            return Error::new_spanned(&fn_item, "multiple #[test] attributes were supplied")
79                .to_compile_error()
80                .into();
81        }
82    }
83
84    if fn_item.sig.asyncness.is_none() {
85        return Error::new_spanned(&fn_item, "test fn must be async")
86            .to_compile_error()
87            .into();
88    }
89
90    let p_args = parse_macro_input!(args as AttributeArgs);
91    let attrib: Punctuated<NestedMeta, Comma> = p_args.into_iter().collect();
92
93    let call_by = format_ident!("{}", fn_item.sig.ident);
94
95    let Arguments { ids, tys } = match parse_args(&fn_item) {
96        Err(e) => return e,
97        Ok(ts) => ts,
98    };
99
100    let ret = &fn_item.sig.output;
101
102    quote! (
103        #[::tokio::test(#attrib)]
104        async fn #call_by() {
105            #fn_item
106
107            let test_fn: fn(#tys) #ret = |#ids| {
108                ::futures::executor::block_on(#call_by(#ids))
109            };
110
111            ::tokio::task::spawn_blocking(move || {
112                ::quickcheck::quickcheck(test_fn)
113            })
114            .await
115            .unwrap()
116        }
117    )
118    .into()
119}
120
121/// Mark an async function to be fuzz-tested using [quickcheck][qc], within an async_std
122/// executor.
123///
124/// # Usage
125///
126/// ```
127/// #[quickcheck_async::async_std]
128/// async fn fuzz_me(fuzz_arg: String) -> bool {
129///     fuzz_arg != "fuzzed".to_owned()
130/// }
131/// ```
132/// [qc]: https://docs.rs/quickcheck/latest/quickcheck/fn.quickcheck.html
133#[proc_macro_attribute]
134pub fn async_std(args: TokenStream, item: TokenStream) -> TokenStream {
135    let fn_item = parse_macro_input!(item as ItemFn);
136
137    for attr in &fn_item.attrs {
138        if attr.path.is_ident("test") {
139            return Error::new_spanned(&fn_item, "multiple #[test] attributes were supplied")
140                .to_compile_error()
141                .into();
142        }
143    }
144
145    if fn_item.sig.asyncness.is_none() {
146        return Error::new_spanned(&fn_item, "test fn must be async")
147            .to_compile_error()
148            .into();
149    }
150
151    let p_args = parse_macro_input!(args as AttributeArgs);
152    let attrib: Punctuated<NestedMeta, Comma> = p_args.into_iter().collect();
153
154    let call_by = format_ident!("{}", fn_item.sig.ident);
155
156    let Arguments { ids, tys } = match parse_args(&fn_item) {
157        Err(e) => return e,
158        Ok(ts) => ts,
159    };
160
161    let ret = &fn_item.sig.output;
162
163    quote! (
164        #[::async_std::test(#attrib)]
165        async fn #call_by() {
166            #fn_item
167
168            let test_fn: fn(#tys) #ret = |#ids| {
169                ::futures::executor::block_on(#call_by(#ids))
170            };
171
172            ::quickcheck::quickcheck(test_fn);
173        }
174    )
175    .into()
176}