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));
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}