proptest_attr_macro/
lib.rs

1// -*- coding: utf-8 -*-
2// ------------------------------------------------------------------------------------------------
3// Copyright © 2019, Douglas Creager.
4//
5// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
6// in compliance with the License.  You may obtain a copy of the License at
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software distributed under the
11// License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
12// express or implied.  See the License for the specific language governing permissions and
13// limitations under the License.
14// ------------------------------------------------------------------------------------------------
15
16//! This crate provides a procedural attribute macro version of [proptest]'s `proptest!` macro.
17//!
18//! So instead of having to write:
19//!
20//! ```
21//! use proptest::proptest;
22//!
23//! proptest! {
24//!     fn test_excluded_middle(x: u32, y: u32) {
25//!         assert!(x == y || x != y);
26//!     }
27//! }
28//! ```
29//!
30//! you can write:
31//!
32//! ```
33//! use proptest_attr_macro::proptest;
34//!
35//! #[proptest]
36//! fn test_excluded_middle(x: u32, y: u32) {
37//!     assert!(x == y || x != y);
38//! }
39//! ```
40//! [proptest]: https://docs.rs/proptest/*/
41//!
42//! ## Limitations
43//!
44//! Procedural attribute macros can only be used with valid Rust syntax, which means that you can't
45//! use proptest's `in` operator (which allows you to draw values from a specific strategy
46//! function):
47//!
48//! ``` compile_fail
49//! // This won't compile!
50//! #[proptest]
51//! fn test_even_numbers(x in even(any::<u32>())) {
52//!     assert!((x % 2) == 0);
53//! }
54//! ```
55//!
56//! Instead you must provide an actual parameter list, just like you would with a real Rust
57//! function definition.  That, in turn, means that your function parameters can only draw values
58//! using the `any` strategy for their types.  If you want to use a custom strategy, you must
59//! create a separately named type, and have the new type's `Arbitrary` impl use that strategy:
60//!
61//! ```
62//! # #[derive(Clone, Debug)]
63//! struct Even { value: i32 }
64//!
65//! # use proptest::arbitrary::Arbitrary;
66//! # use proptest::strategy::BoxedStrategy;
67//! # use proptest::strategy::Strategy;
68//! impl Arbitrary for Even {
69//!     type Parameters = ();
70//!     type Strategy = BoxedStrategy<Even>;
71//!
72//!     fn arbitrary_with(_args: ()) -> Self::Strategy {
73//!         (0..100).prop_map(|x| Even { value: x * 2 }).boxed()
74//!     }
75//! }
76//!
77//! # use proptest_attr_macro::proptest;
78//! #[proptest]
79//! fn test_even_numbers(even: Even) {
80//!     assert!((even.value % 2) == 0);
81//! }
82//! ```
83//!
84//! ## Benefits
85//!
86//! The main one is purely aesthetic: since you're applying the `proptest` attribute macro to valid
87//! Rust functions, `rustfmt` works on them!
88
89extern crate proc_macro;
90
91use proc_macro::TokenStream;
92use proc_macro2::Span;
93use proc_macro2::TokenStream as TokenStream2;
94use quote::quote;
95use quote::ToTokens;
96use syn::parse_macro_input;
97use syn::parse_quote;
98use syn::punctuated::Punctuated;
99use syn::token::Comma;
100use syn::FnArg;
101use syn::Item;
102use syn::Pat;
103use syn::Stmt;
104use syn::Token;
105
106/// An attribute macro that marks a function as a test case, and uses proptest's [`any`][] strategy
107/// to produce random values for each of the function's parameters.
108///
109/// [`any`]: https://docs.rs/proptest/*/proptest/prelude/fn.any.html
110///
111/// ```
112/// # extern crate proptest_attr_macro;
113/// # use proptest_attr_macro::proptest;
114/// #[proptest]
115/// fn test_excluded_middle(x: u32, y: u32) {
116///     assert!(x == y || x != y);
117/// }
118/// ```
119#[proc_macro_attribute]
120pub fn proptest(_args: TokenStream, input: TokenStream) -> TokenStream {
121    let item = parse_macro_input!(input as Item);
122    match item {
123        Item::Fn(mut func) => {
124            let func_name = &func.sig.ident;
125            let mut func_body = func.block.clone();
126            if let Some(stmt) = func_body.stmts.last_mut() {
127                if let Stmt::Expr(expr) = stmt {
128                    // Function body has a return expression, but that's probably a typo.
129                    *stmt = Stmt::Semi(expr.clone(), Token![;](Span::call_site()));
130                }
131            }
132            func_body.stmts.push(parse_quote! { return Ok(()); });
133
134            let mut formal_params = TupleList::new();
135            let mut actual_params = Punctuated::<_, Comma>::new();
136            let mut names = TupleList::new();
137            let mut strategies = TupleList::new();
138            for arg in func.sig.inputs.iter() {
139                if let FnArg::Typed(typed) = arg {
140                    if let Pat::Ident(name) = &*typed.pat {
141                        let ty = &typed.ty;
142                        formal_params.push(name.ident.clone());
143                        actual_params.push(name.ident.clone());
144                        names.push(name.ident.to_string());
145                        strategies.push(quote! { ::proptest::arbitrary::any::<#ty>() });
146                    }
147                }
148            }
149
150            func.attrs.insert(0, parse_quote! { #[test] });
151            func.sig.inputs.clear();
152            func.block = parse_quote! {{
153                let mut config = ::proptest::test_runner::Config::default();
154                config.test_name = Some(concat!(module_path!(), "::", stringify!(#func_name)));
155                config.source_file = Some(file!());
156                let mut runner = ::proptest::test_runner::TestRunner::new(config);
157                let names = #names;
158                match runner.run(
159                    &::proptest::strategy::Strategy::prop_map(
160                        #strategies,
161                        |values| ::proptest::sugar::NamedArguments(names, values),
162                    ),
163                    |::proptest::sugar::NamedArguments(_, #formal_params)| {
164                        #func_body
165                    }
166                ) {
167                    Ok(_) => (),
168                    Err(e) => panic!("{}\n{}", e, runner),
169                }
170            }};
171
172            func.into_token_stream().into()
173        }
174        _ => {
175            let msg = "#[proptest] is only supported on functions";
176            syn::parse::Error::new_spanned(item, msg)
177                .to_compile_error()
178                .into()
179        }
180    }
181}
182
183#[derive(Debug)]
184struct TupleList<T>(Vec<T>);
185
186impl<T> TupleList<T> {
187    fn new() -> TupleList<T> {
188        TupleList(Vec::new())
189    }
190
191    fn push(&mut self, value: T) {
192        self.0.push(value);
193    }
194}
195
196impl<T> ToTokens for TupleList<T>
197where
198    T: ToTokens,
199{
200    fn to_tokens(&self, tokens: &mut TokenStream2) {
201        let mut result = TokenStream2::new();
202        for (idx, value) in self.0.iter().rev().enumerate() {
203            if idx == 0 {
204                value.to_tokens(&mut result);
205            } else {
206                result = quote! { (#value, #result) };
207            }
208        }
209        result.to_tokens(tokens);
210    }
211}