Skip to content

Commit b844446

Browse files
authored
Merge pull request #523 from matthew-russo/478-custom-strategies-attr-macro
[proptest-macro] add the ability to specify custom strategies
2 parents e5b6c65 + 779a6ac commit b844446

14 files changed

+492
-118
lines changed

.github/workflows/rust.yml

+2
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ jobs:
4646
run: cd proptest && cargo build --verbose
4747
- name: Run tests
4848
run: cd proptest && cargo test --verbose
49+
- name: Run macro tests
50+
run: cd proptest-macro && cargo test --verbose
4951
- name: Build coverage no-default-features
5052
if: ${{ matrix.build == 'stable' }}
5153
env:

proptest-macro/src/lib.rs

+21-3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ mod property_test;
1111
/// Using the `property_test` macro:
1212
///
1313
/// ```
14+
/// # use proptest_macro::property_test;
1415
/// #[property_test]
1516
/// fn foo(x: i32) {
1617
/// assert_eq!(x, x);
@@ -19,7 +20,7 @@ mod property_test;
1920
///
2021
/// is roughly equivalent to:
2122
///
22-
/// ```
23+
/// ```ignore
2324
/// proptest! {
2425
/// #[test]
2526
/// fn foo(x in any::<i32>()) {
@@ -35,19 +36,36 @@ mod property_test;
3536
/// of setting up the test harness and generating input values, allowing the user to focus
3637
/// on writing the test logic.
3738
///
38-
/// # Attributes
39+
/// ## Attributes
3940
///
4041
/// The `property_test` macro can take an optional `config` attribute, which allows you to
4142
/// customize the configuration of the `proptest` runner.
4243
///
4344
/// E.g. running 100 cases:
4445
///
45-
/// ```rust
46+
/// ```rust,ignore
4647
/// #[property_test(config = "ProptestConfig { cases: 100, .. ProptestConfig::default() }")]
4748
/// fn foo(x: i32) {
4849
/// assert_eq!(x, x);
4950
/// }
5051
/// ```
52+
///
53+
/// ## Custom strategies
54+
///
55+
/// By default, [`property_test`] will use the `Arbitrary` impl for parameters. However, you can
56+
/// provide a custom `Strategy` with `#[strategy = <expr>]` on an argument:
57+
///
58+
/// ```
59+
/// # use proptest_macro::property_test;
60+
/// #[property_test]
61+
/// fn foo(#[strategy = "[0-9]*"] s: String) {
62+
/// for c in s.chars() {
63+
/// assert!(c.is_numeric());
64+
/// }
65+
/// }
66+
/// ```
67+
/// Multiple `#[strategy = <expr>]` attributes on an argument are not allowed.
68+
///
5169
#[proc_macro_attribute]
5270
pub fn property_test(attr: TokenStream, item: TokenStream) -> TokenStream {
5371
property_test::property_test(attr.into(), item.into()).into()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use super::*;
2+
use quote::quote_spanned;
3+
4+
/// Generate the arbitrary impl for the struct
5+
pub(super) fn gen_arbitrary_impl(
6+
fn_name: &Ident,
7+
args: &[Argument],
8+
) -> TokenStream {
9+
if args.iter().all(|arg| arg.strategy.is_none()) {
10+
no_custom_strategies(fn_name, args)
11+
} else {
12+
custom_strategies(fn_name, args)
13+
}
14+
}
15+
16+
// we can avoid boxing strategies if there are no custom strategies, since we have types written
17+
// out in function args
18+
//
19+
// If there are custom strategies, we can't write the type, because we're only provided the
20+
// expression for the strategy (e.g. `#[strategy = my_custom_strategy()]` doesn't tell us the
21+
// return type of `my_custom_strategy`). In these cases, we just use `BoxedStrategy<Self>`
22+
fn no_custom_strategies(fn_name: &Ident, args: &[Argument]) -> TokenStream {
23+
let arg_types = args.iter().map(|arg| {
24+
let ty = &arg.pat_ty.ty;
25+
quote!(#ty,)
26+
});
27+
28+
let arg_types = quote! { #(#arg_types)* };
29+
30+
let arg_names = args.iter().enumerate().map(|(index, arg)| {
31+
let name = nth_field_name(arg.pat_ty.span(), index);
32+
quote!(#name,)
33+
});
34+
35+
let arg_names = quote! { #(#arg_names)* };
36+
37+
let strategy_type = quote! {
38+
::proptest::strategy::Map<::proptest::arbitrary::StrategyFor<(#arg_types)>, fn((#arg_types)) -> Self>
39+
};
40+
41+
let strategy_expr = quote! {
42+
use ::proptest::strategy::Strategy;
43+
::proptest::prelude::any::<(#arg_types)>().prop_map(|(#arg_names)| Self { #arg_names })
44+
};
45+
46+
arbitrary_shared(fn_name, strategy_type, strategy_expr)
47+
}
48+
49+
// if we have `fn foo(#[strategy = x] a: i32, b: i32) {}`, we want to generate something like this:
50+
// ```ignore
51+
// impl Arbitrary for FooArgs {
52+
// type Parameters = ();
53+
// type Strategy = BoxedStrategy<Self>;
54+
//
55+
// fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
56+
// (x, any::<i32>()).prop_map(|(a, b)| Self { a, b }).boxed()
57+
// }
58+
// }
59+
// ```
60+
fn custom_strategies(fn_name: &Ident, args: &[Argument]) -> TokenStream {
61+
let arg_strategies: TokenStream =
62+
args.iter()
63+
.map(|arg| {
64+
arg.strategy.as_ref().map(|s| quote! {#s,}).unwrap_or_else(
65+
|| {
66+
let ty = &arg.pat_ty.ty;
67+
quote_spanned! {
68+
ty.span() => ::proptest::prelude::any::<#ty>(),
69+
}
70+
},
71+
)
72+
})
73+
.collect();
74+
75+
let arg_names: TokenStream = args
76+
.iter()
77+
.enumerate()
78+
.map(|(index, arg)| {
79+
let name = nth_field_name(arg.pat_ty.span(), index);
80+
quote!(#name,)
81+
})
82+
.collect();
83+
let arg_names = &arg_names;
84+
85+
let strategy_expr = quote! {
86+
use ::proptest::strategy::Strategy;
87+
(#arg_strategies).prop_map(|(#arg_names)| Self { #arg_names }).boxed()
88+
};
89+
90+
let strategy_type = quote! {
91+
::proptest::strategy::BoxedStrategy<Self>
92+
};
93+
arbitrary_shared(fn_name, strategy_type, strategy_expr)
94+
}
95+
96+
/// shared code between both boxed and unboxed paths
97+
fn arbitrary_shared(
98+
fn_name: &Ident,
99+
strategy_type: TokenStream,
100+
strategy_expr: TokenStream,
101+
) -> TokenStream {
102+
let struct_name = struct_name(fn_name);
103+
104+
quote! {
105+
impl ::proptest::prelude::Arbitrary for #struct_name {
106+
type Parameters = ();
107+
type Strategy = #strategy_type;
108+
109+
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
110+
#strategy_expr
111+
}
112+
}
113+
}
114+
}

proptest-macro/src/property_test/codegen/mod.rs

+19-63
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
use proc_macro2::TokenStream;
22
use quote::{quote, ToTokens};
3-
use syn::{parse_str, spanned::Spanned, Attribute, Ident, ItemFn, PatType};
3+
use syn::{parse_quote, spanned::Spanned, Attribute, Ident, ItemFn};
44

5-
use super::{options::Options, utils::strip_args};
5+
use super::{
6+
options::Options,
7+
utils::{strip_args, Argument},
8+
};
69

10+
mod arbitrary;
711
mod test_body;
812

913
/// Generate the modified test function
@@ -20,17 +24,18 @@ pub(super) fn generate(item_fn: ItemFn, options: Options) -> TokenStream {
2024
let (mut argless_fn, args) = strip_args(item_fn);
2125

2226
let struct_tokens = generate_struct(&argless_fn.sig.ident, &args);
23-
let arb_tokens = generate_arbitrary_impl(&argless_fn.sig.ident, &args);
27+
let arb_tokens =
28+
arbitrary::gen_arbitrary_impl(&argless_fn.sig.ident, &args);
2429

25-
let struct_and_tokens = quote! {
30+
let struct_and_arb = quote! {
2631
#struct_tokens
2732
#arb_tokens
2833
};
2934

3035
let new_body = test_body::body(
3136
*argless_fn.block,
3237
&args,
33-
struct_and_tokens,
38+
struct_and_arb,
3439
&argless_fn.sig.ident,
3540
&argless_fn.sig.output,
3641
&options,
@@ -43,12 +48,12 @@ pub(super) fn generate(item_fn: ItemFn, options: Options) -> TokenStream {
4348
}
4449

4550
/// Generate the inner struct that represents the arguments of the function
46-
fn generate_struct(fn_name: &Ident, args: &[PatType]) -> TokenStream {
51+
fn generate_struct(fn_name: &Ident, args: &[Argument]) -> TokenStream {
4752
let struct_name = struct_name(fn_name);
4853

4954
let fields = args.iter().enumerate().map(|(index, arg)| {
50-
let field_name = nth_field_name(&arg.pat, index);
51-
let ty = &arg.ty;
55+
let field_name = nth_field_name(&arg.pat_ty.pat, index);
56+
let ty = &arg.pat_ty.ty;
5257

5358
quote! { #field_name: #ty, }
5459
});
@@ -61,37 +66,6 @@ fn generate_struct(fn_name: &Ident, args: &[PatType]) -> TokenStream {
6166
}
6267
}
6368

64-
/// Generate the arbitrary impl for the struct
65-
fn generate_arbitrary_impl(fn_name: &Ident, args: &[PatType]) -> TokenStream {
66-
let struct_name = struct_name(fn_name);
67-
68-
let arg_types = args.iter().map(|arg| {
69-
let ty = &arg.ty;
70-
quote!(#ty,)
71-
});
72-
73-
let arg_types = quote! { #(#arg_types)* };
74-
75-
let arg_names = args.iter().enumerate().map(|(index, ty)| {
76-
let name = nth_field_name(ty.span(), index);
77-
quote!(#name,)
78-
});
79-
80-
let arg_names = quote! { #(#arg_names)* };
81-
82-
quote! {
83-
impl ::proptest::prelude::Arbitrary for #struct_name {
84-
type Parameters = ();
85-
type Strategy = ::proptest::strategy::Map<::proptest::arbitrary::StrategyFor<(#arg_types)>, fn((#arg_types)) -> Self>;
86-
87-
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
88-
use ::proptest::strategy::Strategy;
89-
::proptest::prelude::any::<(#arg_types)>().prop_map(|(#arg_names)| Self { #arg_names })
90-
}
91-
}
92-
}
93-
}
94-
9569
/// Convert the name of a function to the name of a struct representing its args
9670
///
9771
/// E.g. `some_function` -> `SomeFunctionArgs`
@@ -111,19 +85,14 @@ fn nth_field_name(span: impl Spanned, index: usize) -> Ident {
11185
Ident::new(&format!("field{index}"), span.span())
11286
}
11387

114-
/// I couldn't find a better way to get just the `#[test]` attribute since [`syn::Attribute`]
115-
/// doesn't implement `Parse`
11688
fn test_attr() -> Attribute {
117-
let mut f: ItemFn = parse_str("#[test] fn foo() {}").unwrap();
118-
f.attrs.pop().unwrap()
89+
parse_quote! { #[test] }
11990
}
12091

12192
#[cfg(test)]
12293
mod tests {
123-
use quote::ToTokens;
124-
use syn::{parse2, parse_str, ItemStruct};
125-
12694
use super::*;
95+
use syn::{parse2, parse_quote, parse_str, ItemStruct};
12796

12897
/// Simple helper that parses a function, and validates that the struct name and fields are
12998
/// correct
@@ -180,31 +149,18 @@ mod tests {
180149

181150
#[test]
182151
fn generates_arbitrary_impl() {
183-
let f: ItemFn = parse_str("fn foo(x: i32, y: u8) {}").unwrap();
152+
let f: ItemFn = parse_quote! { fn foo(x: i32, y: u8) {} };
184153
let (f, args) = strip_args(f);
185-
let arb = generate_arbitrary_impl(&f.sig.ident, &args);
186-
187-
let expected = quote! {
188-
impl ::proptest::prelude::Arbitrary for FooArgs {
189-
type Parameters = ();
190-
type Strategy = ::proptest::strategy::Map<::proptest::arbitrary::StrategyFor<(i32, u8,)>, fn((i32, u8,)) -> Self>;
191-
192-
fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
193-
use ::proptest::strategy::Strategy;
194-
195-
::proptest::prelude::any::<(i32, u8,)>().prop_map(|(field0, field1,)| Self { field0, field1, })
196-
}
197-
198-
}
199-
};
154+
let arb = arbitrary::gen_arbitrary_impl(&f.sig.ident, &args);
200155

201-
assert_eq!(arb.to_string(), expected.to_string());
156+
insta::assert_snapshot!(arb.to_string());
202157
}
203158
}
204159

205160
#[cfg(test)]
206161
mod snapshot_tests {
207162
use super::*;
163+
use syn::parse_str;
208164

209165
macro_rules! snapshot_test {
210166
($name:ident) => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: proptest-macro/src/property_test/codegen/mod.rs
3+
expression: arb.to_string()
4+
---
5+
impl :: proptest :: prelude :: Arbitrary for FooArgs { type Parameters = () ; type Strategy = :: proptest :: strategy :: Map < :: proptest :: arbitrary :: StrategyFor < (i32 , u8 ,) > , fn ((i32 , u8 ,)) -> Self > ; fn arbitrary_with (() : Self :: Parameters) -> Self :: Strategy { use :: proptest :: strategy :: Strategy ; :: proptest :: prelude :: any :: < (i32 , u8 ,) > () . prop_map (| (field0 , field1 ,) | Self { field0 , field1 , }) } }

proptest-macro/src/property_test/codegen/test_body.rs

+7-8
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
11
use proc_macro2::TokenStream;
22
use quote::{quote, ToTokens};
33
use syn::{
4-
parse2, spanned::Spanned, Block, Expr, Ident, PatType, ReturnType, Type,
5-
TypeTuple,
4+
parse2, spanned::Spanned, Block, Expr, Ident, ReturnType, Type, TypeTuple,
65
};
76

8-
use crate::property_test::options::Options;
7+
use crate::property_test::{options::Options, utils::Argument};
98

109
use super::{nth_field_name, struct_name};
1110

1211
/// Generate the new test body by putting the struct and arbitrary impl at the start, then adding
1312
/// the usual glue that `proptest!` adds
1413
pub(super) fn body(
1514
block: Block,
16-
args: &[PatType],
15+
args: &[Argument],
1716
struct_and_impl: TokenStream,
1817
fn_name: &Ident,
1918
ret_ty: &ReturnType,
20-
options: &Options,
19+
options: &Options,
2120
) -> Block {
2221
let struct_name = struct_name(fn_name);
2322

2423
let errors = &options.errors;
2524

2625
// convert each arg to `field0: x`
2726
let struct_fields = args.iter().enumerate().map(|(index, arg)| {
28-
let pat = &arg.pat;
29-
let field_name = nth_field_name(arg.pat.span(), index);
27+
let pat = &arg.pat_ty.pat;
28+
let field_name = nth_field_name(arg.pat_ty.pat.span(), index);
3029
quote!(#field_name: #pat,)
3130
});
3231

@@ -66,6 +65,7 @@ pub(super) fn body(
6665
} );
6766

6867
// unwrap here is fine because the double braces create a block
68+
// std::fs::write("/tmp/pt-debug", tokens.to_string());
6969
parse2(tokens).unwrap()
7070
}
7171

@@ -104,4 +104,3 @@ fn make_config(config: Option<&Expr>) -> TokenStream {
104104
};
105105
}
106106
}
107-

0 commit comments

Comments
 (0)