diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..3550a30f --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 4f836501..377c06a5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,5 @@ persistence-test.txt # IntelliJ .idea + +/.direnv diff --git a/Cargo.toml b/Cargo.toml index 720756b6..97898599 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ members = [ "proptest", "proptest-derive", "proptest-state-machine", + "proptest-macro", ] exclude = ["proptest/test-persistence-location/*"] diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..118d0af0 --- /dev/null +++ b/flake.lock @@ -0,0 +1,129 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1709126324, + "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "d465f4819400de7c8d874d50b982301f28a84605", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1705309234, + "narHash": "sha256-uNRRNRKmJyCRC/8y1RqBkqWBLM034y4qN7EprSdmgyA=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "1ef2e671c3b0c19053962c07dbda38332dcebf26", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1710143616, + "narHash": "sha256-u52Nm+rJ1CyMoz4SpPQYy6CHpo8bauC0hm6CTNyrhjs=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c317ecd14f75eef9c446b59930d1858579ca9ccd", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs_2": { + "locked": { + "lastModified": 1706487304, + "narHash": "sha256-LE8lVX28MV2jWJsidW13D2qrHU/RUUONendL2Q/WlJg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "90f456026d284c22b3e3497be980b2e47d0b28ac", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": "nixpkgs_2" + }, + "locked": { + "lastModified": 1710123130, + "narHash": "sha256-EoGL/WSM1M2L099Q91mPKO/FRV2iu2ZLOEp3y5sLfiE=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "73aca260afe5d41d3ebce932c8d896399c9d5174", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..cd65a620 --- /dev/null +++ b/flake.nix @@ -0,0 +1,32 @@ +{ + description = "proptest development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + rust-overlay.url = "github:oxalica/rust-overlay"; + }; + + outputs = { self, nixpkgs, flake-utils, rust-overlay }: + flake-utils.lib.eachDefaultSystem + ( + system: + let + pkgs = import nixpkgs { + inherit system; + overlays = [ (import rust-overlay) ]; + }; + in + { + devShells.default = pkgs.mkShell { + nativeBuildInputs = with pkgs; [ + rust-bin.nightly.latest.default + + cargo-insta + ]; + + TRYBUILD_CARGO_CMD = "test"; + }; + } + ); +} diff --git a/proptest-derive/src/ast.rs b/proptest-derive/src/ast.rs index ff43b8b9..c29fdbb3 100644 --- a/proptest-derive/src/ast.rs +++ b/proptest-derive/src/ast.rs @@ -14,7 +14,6 @@ use std::ops::{Add, AddAssign}; use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, TokenStreamExt}; -use syn; use syn::spanned::Spanned; use crate::error::{Ctx, DeriveResult}; diff --git a/proptest-macro/Cargo.toml b/proptest-macro/Cargo.toml new file mode 100644 index 00000000..4b1dcc4a --- /dev/null +++ b/proptest-macro/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "proptest-macro" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +[dependencies] +syn = { version = "2", features = ["full"] } +quote = "1" +proc-macro2 = "1" +convert_case = "0.6" + +[dev-dependencies] +insta = "1" +prettyplease = "0.2" diff --git a/proptest-macro/src/lib.rs b/proptest-macro/src/lib.rs new file mode 100644 index 00000000..ef0329a9 --- /dev/null +++ b/proptest-macro/src/lib.rs @@ -0,0 +1,8 @@ +use proc_macro::TokenStream; + +mod property_test; + +#[proc_macro_attribute] +pub fn property_test(attr: TokenStream, item: TokenStream) -> TokenStream { + property_test::property_test(attr.into(), item.into()).into() +} diff --git a/proptest-macro/src/property_test/codegen/mod.rs b/proptest-macro/src/property_test/codegen/mod.rs new file mode 100644 index 00000000..77d6a113 --- /dev/null +++ b/proptest-macro/src/property_test/codegen/mod.rs @@ -0,0 +1,231 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{parse_str, spanned::Spanned, Attribute, Ident, ItemFn, PatType}; + +use super::{options::Options, utils::strip_args}; + +mod test_body; + +/// Generate the modified test function +/// +/// The rough process is: +/// - strip out the function args from the provided function +/// - turn them into a struct +/// - implement `Arbitrary` for that struct (simple field-wise impl) +/// - create a runner, do the rest +/// +/// Currently, any attributes on parameters are ignored - in the future, we probably want to read +/// these for things like customizing strategies +pub(super) fn generate(item_fn: ItemFn, options: Options) -> TokenStream { + let (mut argless_fn, args) = strip_args(item_fn); + + let struct_tokens = generate_struct(&argless_fn.sig.ident, &args); + let arb_tokens = generate_arbitrary_impl(&argless_fn.sig.ident, &args); + + let struct_and_tokens = quote! { + #struct_tokens + #arb_tokens + }; + + let new_body = test_body::body( + *argless_fn.block, + &args, + struct_and_tokens, + &argless_fn.sig.ident, + &argless_fn.sig.output, + &options, + ); + + *argless_fn.block = new_body; + argless_fn.attrs.push(test_attr()); + + argless_fn.to_token_stream() +} + +/// Generate the inner struct that represents the arguments of the function +fn generate_struct(fn_name: &Ident, args: &[PatType]) -> TokenStream { + let struct_name = struct_name(fn_name); + + let fields = args.iter().enumerate().map(|(index, arg)| { + let field_name = nth_field_name(&arg.pat, index); + let ty = &arg.ty; + + quote! { #field_name: #ty, } + }); + + quote! { + #[derive(Debug)] + struct #struct_name { + #(#fields)* + } + } +} + +/// Generate the arbitrary impl for the struct +fn generate_arbitrary_impl(fn_name: &Ident, args: &[PatType]) -> TokenStream { + let struct_name = struct_name(fn_name); + + let arg_types = args.iter().map(|arg| { + let ty = &arg.ty; + quote!(#ty,) + }); + + let arg_types = quote! { #(#arg_types)* }; + + let arg_names = args.iter().enumerate().map(|(index, ty)| { + let name = nth_field_name(ty.span(), index); + quote!(#name,) + }); + + let arg_names = quote! { #(#arg_names)* }; + + quote! { + impl ::proptest::prelude::Arbitrary for #struct_name { + type Parameters = (); + type Strategy = ::proptest::strategy::Map<::proptest::arbitrary::StrategyFor<(#arg_types)>, fn((#arg_types)) -> Self>; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + use ::proptest::strategy::Strategy; + ::proptest::prelude::any::<(#arg_types)>().prop_map(|(#arg_names)| Self { #arg_names }) + } + } + } +} + +/// Convert the name of a function to the name of a struct representing its args +/// +/// E.g. `some_function` -> `SomeFunctionArgs` +fn struct_name(fn_name: &Ident) -> Ident { + use convert_case::{Case, Casing}; + + let name = fn_name.to_string(); + let name = name.to_case(Case::Pascal); + let name = format!("{name}Args"); + Ident::new(&name, fn_name.span()) +} + +/// We convert all fields to `"field0"`, etc. to account for various different patterns that can +/// exist in function args. We restore the patterns/bindings when we destructure the struct in the +/// test body +fn nth_field_name(span: impl Spanned, index: usize) -> Ident { + Ident::new(&format!("field{index}"), span.span()) +} + +/// I couldn't find a better way to get just the `#[test]` attribute since [`syn::Attribute`] +/// doesn't implement `Parse` +fn test_attr() -> Attribute { + let mut f: ItemFn = parse_str("#[test] fn foo() {}").unwrap(); + f.attrs.pop().unwrap() +} + +#[cfg(test)] +mod tests { + use quote::ToTokens; + use syn::{parse2, parse_str, ItemStruct}; + + use super::*; + + /// Simple helper that parses a function, and validates that the struct name and fields are + /// correct + fn check_struct( + fn_def: &str, + expected_name: &'static str, + expected_fields: impl IntoIterator, + ) { + let f: ItemFn = parse_str(fn_def).unwrap(); + let (f, args) = strip_args(f); + let tokens = generate_struct(&f.sig.ident, &args); + let s: ItemStruct = parse2(tokens).unwrap(); + + let fields: Vec<_> = s + .fields + .into_iter() + .map(|field| { + ( + field.ident.unwrap().to_string(), + field.ty.to_token_stream().to_string(), + ) + }) + .collect(); + + assert_eq!(s.ident.to_string(), expected_name); + let expected_fields: Vec<_> = expected_fields + .into_iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + assert_eq!(fields, expected_fields); + } + + #[test] + fn derives_debug() { + let f: ItemFn = parse_str("fn foo(x: i32) {}").unwrap(); + let (f, args) = strip_args(f); + let string = generate_struct(&f.sig.ident, &args).to_string(); + + assert!(string.contains("derive")); + assert!(string.contains("Debug")); + } + + #[test] + fn generates_correct_struct() { + check_struct("fn foo() {}", "FooArgs", []); + check_struct("fn foo(x: i32) {}", "FooArgs", [("field0", "i32")]); + check_struct( + "fn foo(a: i32, b: String) {}", + "FooArgs", + [("field0", "i32"), ("field1", "String")], + ); + } + + #[test] + fn generates_arbitrary_impl() { + let f: ItemFn = parse_str("fn foo(x: i32, y: u8) {}").unwrap(); + let (f, args) = strip_args(f); + let arb = generate_arbitrary_impl(&f.sig.ident, &args); + + let expected = quote! { + 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, }) + } + + } + }; + + assert_eq!(arb.to_string(), expected.to_string()); + } +} + +#[cfg(test)] +mod snapshot_tests { + use super::*; + + macro_rules! snapshot_test { + ($name:ident) => { + #[test] + fn $name() { + const TEXT: &str = include_str!(concat!( + "test_data/", + stringify!($name), + ".rs" + )); + + let tokens = generate( + parse_str(TEXT).unwrap(), + $crate::property_test::options::Options::default(), + ); + insta::assert_debug_snapshot!(tokens); + } + }; + } + + snapshot_test!(simple); + snapshot_test!(many_params); + snapshot_test!(arg_pattern); +} diff --git a/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__arg_pattern.snap b/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__arg_pattern.snap new file mode 100644 index 00000000..12bd4f42 --- /dev/null +++ b/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__arg_pattern.snap @@ -0,0 +1,1290 @@ +--- +source: proptest-macro/src/property_test/codegen/mod.rs +expression: tokens +--- +TokenStream [ + Punct { + char: '#', + spacing: Alone, + }, + Group { + delimiter: Bracket, + stream: TokenStream [ + Ident { + sym: test, + }, + ], + }, + Ident { + sym: fn, + }, + Ident { + sym: foo, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Punct { + char: '#', + spacing: Alone, + }, + Group { + delimiter: Bracket, + stream: TokenStream [ + Ident { + sym: derive, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: Debug, + }, + ], + }, + ], + }, + Ident { + sym: struct, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ':', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: i32, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Ident { + sym: impl, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Arbitrary, + }, + Ident { + sym: for, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: type, + }, + Ident { + sym: Parameters, + }, + Punct { + char: '=', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: type, + }, + Ident { + sym: Strategy, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Map, + }, + Punct { + char: '<', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: arbitrary, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: StrategyFor, + }, + Punct { + char: '<', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: i32, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '>', + spacing: Alone, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: fn, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: i32, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + Punct { + char: '-', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: '>', + spacing: Alone, + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: fn, + }, + Ident { + sym: arbitrary_with, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Parameters, + }, + ], + }, + Punct { + char: '-', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: use, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Punct { + char: ';', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: any, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Punct { + char: '<', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: i32, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: '.', + spacing: Alone, + }, + Ident { + sym: prop_map, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '|', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + ], + }, + ], + }, + Ident { + sym: let, + }, + Ident { + sym: config, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Config, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: test_name, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Some, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: concat, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: module_path, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "::", + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: stringify, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '$', + spacing: Alone, + }, + Ident { + sym: test_name, + }, + ], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: source_file, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Some, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: file, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '.', + spacing: Joint, + }, + Punct { + char: '.', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Config, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: default, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: mut, + }, + Ident { + sym: runner, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: TestRunner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: new, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: config, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: result, + }, + Punct { + char: '=', + spacing: Alone, + }, + Ident { + sym: runner, + }, + Punct { + char: '.', + spacing: Alone, + }, + Ident { + sym: run, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '&', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prop_map, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: any, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Punct { + char: '<', + spacing: Alone, + }, + Ident { + sym: FooArgs, + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '|', + spacing: Alone, + }, + Ident { + sym: values, + }, + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: sugar, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: NamedArguments, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: stringify, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: FooArgs, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: values, + }, + ], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '|', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: sugar, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: NamedArguments, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: _, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ':', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: x, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: y, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: let, + }, + Ident { + sym: result, + }, + Punct { + char: '=', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: println, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Literal { + lit: "{x} and {y}", + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: _, + }, + Punct { + char: '=', + spacing: Alone, + }, + Ident { + sym: result, + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: Ok, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: match, + }, + Ident { + sym: result, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: Ok, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: '=', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [], + }, + Ident { + sym: Err, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: e, + }, + ], + }, + Punct { + char: '=', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: panic, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Literal { + lit: "{}", + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: e, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, +] diff --git a/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__many_params.snap b/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__many_params.snap new file mode 100644 index 00000000..5f5f0d4d --- /dev/null +++ b/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__many_params.snap @@ -0,0 +1,1293 @@ +--- +source: proptest-macro/src/property_test/codegen/mod.rs +expression: tokens +--- +TokenStream [ + Punct { + char: '#', + spacing: Alone, + }, + Group { + delimiter: Bracket, + stream: TokenStream [ + Ident { + sym: test, + }, + ], + }, + Ident { + sym: fn, + }, + Ident { + sym: foo, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Punct { + char: '#', + spacing: Alone, + }, + Group { + delimiter: Bracket, + stream: TokenStream [ + Ident { + sym: derive, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: Debug, + }, + ], + }, + ], + }, + Ident { + sym: struct, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: field1, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Ident { + sym: impl, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Arbitrary, + }, + Ident { + sym: for, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: type, + }, + Ident { + sym: Parameters, + }, + Punct { + char: '=', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: type, + }, + Ident { + sym: Strategy, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Map, + }, + Punct { + char: '<', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: arbitrary, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: StrategyFor, + }, + Punct { + char: '<', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '>', + spacing: Alone, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: fn, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + Punct { + char: '-', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: '>', + spacing: Alone, + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: fn, + }, + Ident { + sym: arbitrary_with, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Parameters, + }, + ], + }, + Punct { + char: '-', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: use, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Punct { + char: ';', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: any, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Punct { + char: '<', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: '.', + spacing: Alone, + }, + Ident { + sym: prop_map, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: field1, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '|', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: field1, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + ], + }, + ], + }, + Ident { + sym: let, + }, + Ident { + sym: config, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Config, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: test_name, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Some, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: concat, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: module_path, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "::", + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: stringify, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '$', + spacing: Alone, + }, + Ident { + sym: test_name, + }, + ], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: source_file, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Some, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: file, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '.', + spacing: Joint, + }, + Punct { + char: '.', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Config, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: default, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: mut, + }, + Ident { + sym: runner, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: TestRunner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: new, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: config, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: result, + }, + Punct { + char: '=', + spacing: Alone, + }, + Ident { + sym: runner, + }, + Punct { + char: '.', + spacing: Alone, + }, + Ident { + sym: run, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '&', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prop_map, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: any, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Punct { + char: '<', + spacing: Alone, + }, + Ident { + sym: FooArgs, + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '|', + spacing: Alone, + }, + Ident { + sym: values, + }, + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: sugar, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: NamedArguments, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: stringify, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: FooArgs, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: values, + }, + ], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '|', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: sugar, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: NamedArguments, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: _, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: x, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: field1, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: y, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: let, + }, + Ident { + sym: result, + }, + Punct { + char: '=', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: println, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Literal { + lit: "{x} and {y}", + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: _, + }, + Punct { + char: '=', + spacing: Alone, + }, + Ident { + sym: result, + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: Ok, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: match, + }, + Ident { + sym: result, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: Ok, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: '=', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [], + }, + Ident { + sym: Err, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: e, + }, + ], + }, + Punct { + char: '=', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: panic, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Literal { + lit: "{}", + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: e, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, +] diff --git a/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__simple.snap b/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__simple.snap new file mode 100644 index 00000000..b2e58110 --- /dev/null +++ b/proptest-macro/src/property_test/codegen/snapshots/proptest_macro__property_test__codegen__snapshot_tests__simple.snap @@ -0,0 +1,1230 @@ +--- +source: proptest-macro/src/property_test/codegen/mod.rs +expression: tokens +--- +TokenStream [ + Punct { + char: '#', + spacing: Alone, + }, + Group { + delimiter: Bracket, + stream: TokenStream [ + Ident { + sym: test, + }, + ], + }, + Ident { + sym: fn, + }, + Ident { + sym: foo, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Punct { + char: '#', + spacing: Alone, + }, + Group { + delimiter: Bracket, + stream: TokenStream [ + Ident { + sym: derive, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: Debug, + }, + ], + }, + ], + }, + Ident { + sym: struct, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Ident { + sym: impl, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Arbitrary, + }, + Ident { + sym: for, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: type, + }, + Ident { + sym: Parameters, + }, + Punct { + char: '=', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: type, + }, + Ident { + sym: Strategy, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Map, + }, + Punct { + char: '<', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: arbitrary, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: StrategyFor, + }, + Punct { + char: '<', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '>', + spacing: Alone, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: fn, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + Punct { + char: '-', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: '>', + spacing: Alone, + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: fn, + }, + Ident { + sym: arbitrary_with, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Parameters, + }, + ], + }, + Punct { + char: '-', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: use, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Punct { + char: ';', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: any, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Punct { + char: '<', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: i32, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: '.', + spacing: Alone, + }, + Ident { + sym: prop_map, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: '|', + spacing: Alone, + }, + Ident { + sym: Self, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + ], + }, + ], + }, + Ident { + sym: let, + }, + Ident { + sym: config, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Config, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: test_name, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Some, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: concat, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: module_path, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ',', + spacing: Alone, + }, + Literal { + lit: "::", + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: stringify, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '$', + spacing: Alone, + }, + Ident { + sym: test_name, + }, + ], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: source_file, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Some, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: file, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '.', + spacing: Joint, + }, + Punct { + char: '.', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Config, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: default, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: mut, + }, + Ident { + sym: runner, + }, + Punct { + char: '=', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: test_runner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: TestRunner, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: new, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: config, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: result, + }, + Punct { + char: '=', + spacing: Alone, + }, + Ident { + sym: runner, + }, + Punct { + char: '.', + spacing: Alone, + }, + Ident { + sym: run, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: '&', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: Strategy, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prop_map, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: prelude, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: any, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Punct { + char: '<', + spacing: Alone, + }, + Ident { + sym: FooArgs, + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '|', + spacing: Alone, + }, + Ident { + sym: values, + }, + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: sugar, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: NamedArguments, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: stringify, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: FooArgs, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: values, + }, + ], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + Punct { + char: '|', + spacing: Alone, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: proptest, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: sugar, + }, + Punct { + char: ':', + spacing: Joint, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: NamedArguments, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: _, + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: FooArgs, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: field0, + }, + Punct { + char: ':', + spacing: Alone, + }, + Ident { + sym: x, + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, + Punct { + char: '|', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: let, + }, + Ident { + sym: result, + }, + Punct { + char: '=', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: println, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Literal { + lit: "{x}", + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: let, + }, + Ident { + sym: _, + }, + Punct { + char: '=', + spacing: Alone, + }, + Ident { + sym: result, + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: Ok, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + Punct { + char: ';', + spacing: Alone, + }, + Ident { + sym: match, + }, + Ident { + sym: result, + }, + Group { + delimiter: Brace, + stream: TokenStream [ + Ident { + sym: Ok, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Group { + delimiter: Parenthesis, + stream: TokenStream [], + }, + ], + }, + Punct { + char: '=', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Group { + delimiter: Brace, + stream: TokenStream [], + }, + Ident { + sym: Err, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Ident { + sym: e, + }, + ], + }, + Punct { + char: '=', + spacing: Joint, + }, + Punct { + char: '>', + spacing: Alone, + }, + Ident { + sym: panic, + }, + Punct { + char: '!', + spacing: Alone, + }, + Group { + delimiter: Parenthesis, + stream: TokenStream [ + Literal { + lit: "{}", + }, + Punct { + char: ',', + spacing: Alone, + }, + Ident { + sym: e, + }, + ], + }, + Punct { + char: ',', + spacing: Alone, + }, + ], + }, + ], + }, +] diff --git a/proptest-macro/src/property_test/codegen/test_body.rs b/proptest-macro/src/property_test/codegen/test_body.rs new file mode 100644 index 00000000..82beeec3 --- /dev/null +++ b/proptest-macro/src/property_test/codegen/test_body.rs @@ -0,0 +1,107 @@ +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{ + parse2, spanned::Spanned, Block, Expr, Ident, PatType, ReturnType, Type, + TypeTuple, +}; + +use crate::property_test::options::Options; + +use super::{nth_field_name, struct_name}; + +/// Generate the new test body by putting the struct and arbitrary impl at the start, then adding +/// the usual glue that `proptest!` adds +pub(super) fn body( + block: Block, + args: &[PatType], + struct_and_impl: TokenStream, + fn_name: &Ident, + ret_ty: &ReturnType, + options: &Options, +) -> Block { + let struct_name = struct_name(fn_name); + + let errors = &options.errors; + + // convert each arg to `field0: x` + let struct_fields = args.iter().enumerate().map(|(index, arg)| { + let pat = &arg.pat; + let field_name = nth_field_name(arg.pat.span(), index); + quote!(#field_name: #pat,) + }); + + // e.g. FooArgs { field0: x, field1: (y, z), } + let struct_pattern = quote! { + #struct_name { #(#struct_fields)* } + }; + + let handle_result = handle_result(ret_ty); + + let config = make_config(options.config.as_ref()); + + let tokens = quote! ( { + + #(#errors)* + + #struct_and_impl + + #config + + let mut runner = ::proptest::test_runner::TestRunner::new(config); + + let result = runner.run( + &::proptest::strategy::Strategy::prop_map(::proptest::prelude::any::<#struct_name>(), |values| { + ::proptest::sugar::NamedArguments(stringify!(#struct_name), values) + }), + |::proptest::sugar::NamedArguments(_, #struct_pattern)| { + let result = #block; + #handle_result + }, + ); + + match result { + Ok(()) => {} + Err(e) => panic!("{}", e), + } + } ); + + // unwrap here is fine because the double braces create a block + parse2(tokens).unwrap() +} + +/// rough heuristic for whether we should use result-style syntax - if the function returns either +/// nothing (i.e. `()`) or an empty tuple, it will be non-result handling, otherwise it uses +/// result-style handling +/// +/// Note, this won't catch cases like `type Foo = ();`, since type information isn't available yet, +/// it's just looking for the syntax `fn foo() {}` or `fn foo() -> () {}` +fn handle_result(ret_ty: &ReturnType) -> TokenStream { + let default_body = || quote! { let _ = result; Ok(()) }; + let result_body = || quote! { result }; + + match ret_ty { + ReturnType::Default => default_body(), + ReturnType::Type(_, ty) => match ty.as_ref() { + Type::Tuple(TypeTuple { elems, .. }) if elems.is_empty() => { + default_body() + } + _ => result_body(), + }, + } +} + +fn make_config(config: Option<&Expr>) -> TokenStream { + let trailing = match config { + None => quote! { ::proptest::test_runner::Config::default() }, + Some(config) => config.to_token_stream(), + }; + + quote! { + let config = ::proptest::test_runner::Config { + test_name: Some(concat!(module_path!(), "::", stringify!($test_name))), + source_file: Some(file!()), + ..#trailing + }; + } +} + diff --git a/proptest-macro/src/property_test/codegen/test_data/arg_pattern.rs b/proptest-macro/src/property_test/codegen/test_data/arg_pattern.rs new file mode 100644 index 00000000..be795dfb --- /dev/null +++ b/proptest-macro/src/property_test/codegen/test_data/arg_pattern.rs @@ -0,0 +1,3 @@ +fn foo((x, y): (i32, i32)) { + println!("{x} and {y}"); +} diff --git a/proptest-macro/src/property_test/codegen/test_data/many_params.rs b/proptest-macro/src/property_test/codegen/test_data/many_params.rs new file mode 100644 index 00000000..fb3c182f --- /dev/null +++ b/proptest-macro/src/property_test/codegen/test_data/many_params.rs @@ -0,0 +1,3 @@ +fn foo(x: i32, y: i32) { + println!("{x} and {y}"); +} diff --git a/proptest-macro/src/property_test/codegen/test_data/simple.rs b/proptest-macro/src/property_test/codegen/test_data/simple.rs new file mode 100644 index 00000000..b0cf614a --- /dev/null +++ b/proptest-macro/src/property_test/codegen/test_data/simple.rs @@ -0,0 +1,3 @@ +fn foo(x: i32) { + println!("{x}"); +} diff --git a/proptest-macro/src/property_test/mod.rs b/proptest-macro/src/property_test/mod.rs new file mode 100644 index 00000000..31d7eb6e --- /dev/null +++ b/proptest-macro/src/property_test/mod.rs @@ -0,0 +1,33 @@ +use proc_macro2::TokenStream; +use syn::parse2; + +use self::validate::validate; + +mod codegen; +mod options; +mod utils; +mod validate; + +#[cfg(test)] +mod tests; + +/// try to parse an item, or return the error as a token stream +macro_rules! parse { + ($e:expr) => { + match parse2($e) { + Ok(item) => item, + Err(e) => return e.into_compile_error(), + } + }; +} + +pub fn property_test(attr: TokenStream, item: TokenStream) -> TokenStream { + let item_fn = parse!(item); + let options = parse!(attr); + + if let Err(compile_error) = validate(&item_fn) { + return compile_error; + } + + codegen::generate(item_fn, options) +} diff --git a/proptest-macro/src/property_test/options.rs b/proptest-macro/src/property_test/options.rs new file mode 100644 index 00000000..ad3b91b0 --- /dev/null +++ b/proptest-macro/src/property_test/options.rs @@ -0,0 +1,60 @@ +use proc_macro2::TokenStream; +use quote::quote_spanned; +use syn::{ + parse::Parse, punctuated::Punctuated, spanned::Spanned, Expr, Ident, + LitStr, MetaNameValue, Token, +}; + +/// Options parsed from the attribute itself (e.g. the config from `#[property_test(config = ...)]`) +#[derive(Default)] +pub(super) struct Options { + /// Collect compiler errors and emit them later, since errors here are largely recoverable + pub errors: Vec, + pub config: Option, +} + +impl Parse for Options { + // note: this impl takes only the contents of the attr, not the attr itself + // e.g. it will get `foo = bar, baz = qux`, not `#[macro(foo = bar, baz = qux)]` + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let pairs = + Punctuated::::parse_terminated(input)?; + + let mut errors = Vec::new(); + + let mut config = None; + + for MetaNameValue { path, value, .. } in pairs { + let path_string = path.get_ident().map(Ident::to_string); + + match path_string.as_deref() { + None => errors.push(quote_spanned!(path.span() => compile_error!("unknown argument"))), + Some("config") => config = Some(value), + Some(other) => { + let error_message = format!("unknown argument: {other}"); + let error_message = LitStr::new(&error_message, other.span()); + let error = quote_spanned!(other.span() => compile_error!(#error_message)); + errors.push(error); + } + } + } + + Ok(Self { errors, config }) + } +} + +#[cfg(test)] +mod tests { + use syn::parse_str; + + use super::*; + + #[test] + fn simple_parse_example() { + let Options { errors, config } = + parse_str("config = (), random = 123").unwrap(); + + assert!(config.is_some()); + assert_eq!(errors.len(), 1); + } +} diff --git a/proptest-macro/src/property_test/tests/mod.rs b/proptest-macro/src/property_test/tests/mod.rs new file mode 100644 index 00000000..82e41366 --- /dev/null +++ b/proptest-macro/src/property_test/tests/mod.rs @@ -0,0 +1,3 @@ +/// Tests that make sure we're generating consistent syntax trees +mod snapshot_tests; + diff --git a/proptest-macro/src/property_test/tests/snapshot_tests.rs b/proptest-macro/src/property_test/tests/snapshot_tests.rs new file mode 100644 index 00000000..c90beec7 --- /dev/null +++ b/proptest-macro/src/property_test/tests/snapshot_tests.rs @@ -0,0 +1,14 @@ +use syn::{parse_str, ItemFn}; + +use crate::property_test::{codegen, options::Options}; + +#[test] +fn basic_derive_example() { + let f: ItemFn = + parse_str("fn foo(x: i32, y: String) { let x = 1; }").unwrap(); + let tokens = codegen::generate(f, Options::default()); + let file = syn::parse_file(&tokens.to_string()).unwrap(); + let formatted = prettyplease::unparse(&file); + + insta::assert_snapshot!(formatted); +} diff --git a/proptest-macro/src/property_test/tests/snapshots/proptest_macro__property_test__tests__snapshot_tests__basic_derive_example.snap b/proptest-macro/src/property_test/tests/snapshots/proptest_macro__property_test__tests__snapshot_tests__basic_derive_example.snap new file mode 100644 index 00000000..7a0a5a61 --- /dev/null +++ b/proptest-macro/src/property_test/tests/snapshots/proptest_macro__property_test__tests__snapshot_tests__basic_derive_example.snap @@ -0,0 +1,50 @@ +--- +source: proptest-macro/src/property_test/tests/snapshot_tests.rs +expression: formatted +--- +#[test] +fn foo() { + #[derive(Debug)] + struct FooArgs { + field0: i32, + field1: String, + } + impl ::proptest::prelude::Arbitrary for FooArgs { + type Parameters = (); + type Strategy = ::proptest::strategy::Map< + ::proptest::arbitrary::StrategyFor<(i32, String)>, + fn((i32, String)) -> Self, + >; + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + use ::proptest::strategy::Strategy; + ::proptest::prelude::any::<(i32, String)>() + .prop_map(|(field0, field1)| Self { field0, field1 }) + } + } + let config = ::proptest::test_runner::Config { + test_name: Some(concat!(module_path!(), "::", stringify!($test_name))), + source_file: Some(file!()), + ..::proptest::test_runner::Config::default() + }; + let mut runner = ::proptest::test_runner::TestRunner::new(config); + let result = runner + .run( + &::proptest::strategy::Strategy::prop_map( + ::proptest::prelude::any::(), + |values| { + ::proptest::sugar::NamedArguments(stringify!(FooArgs), values) + }, + ), + |::proptest::sugar::NamedArguments(_, FooArgs { field0: x, field1: y })| { + let result = { + let x = 1; + }; + let _ = result; + Ok(()) + }, + ); + match result { + Ok(()) => {} + Err(e) => panic!("{}", e), + } +} diff --git a/proptest-macro/src/property_test/utils.rs b/proptest-macro/src/property_test/utils.rs new file mode 100644 index 00000000..ef7a1ba5 --- /dev/null +++ b/proptest-macro/src/property_test/utils.rs @@ -0,0 +1,41 @@ +use core::mem::replace; + +use syn::{punctuated::Punctuated, FnArg, ItemFn, PatType}; + +/// Convert a function to a zero-arg function, and return the args +/// +/// Panics if any arg is a receiver (i.e. `self` or a variant) +pub fn strip_args(mut f: ItemFn) -> (ItemFn, Vec) { + let args = replace(&mut f.sig.inputs, Punctuated::new()); + (f, args.into_iter().map(|arg| match arg { + FnArg::Typed(arg) => arg, + FnArg::Receiver(_) => panic!("receivers aren't allowed - should be filtered by `validate`"), + }).collect()) +} + +#[cfg(test)] +mod tests { + use quote::ToTokens; + use syn::parse_str; + + use super::*; + + #[test] + fn strip_args_works() { + let f = parse_str("fn foo(i: i32) {}").unwrap(); + let (f, mut types) = strip_args(f); + + assert_eq!(f.to_token_stream().to_string(), "fn foo () { }"); + + assert_eq!(types.len(), 1); + let ty = types.pop().unwrap(); + assert_eq!(ty.to_token_stream().to_string(), "i : i32"); + } + + #[test] + #[should_panic] + fn strip_args_panics_with_self() { + let f = parse_str("fn foo(self) {}").unwrap(); + strip_args(f); + } +} diff --git a/proptest-macro/src/property_test/validate.rs b/proptest-macro/src/property_test/validate.rs new file mode 100644 index 00000000..5ce663fe --- /dev/null +++ b/proptest-macro/src/property_test/validate.rs @@ -0,0 +1,59 @@ +use proc_macro2::TokenStream; +use quote::quote_spanned; +use syn::{spanned::Spanned, FnArg, ItemFn}; + +/// Validate an `ItemFn` for some basic sanity checks +/// +/// Many checks are deferred to rustc (e.g. rustc already errors if you make a test function +/// unsafe, so we just transparently pass unsafe through to the generated function and let rustc +/// emit the error) +pub(super) fn validate(f: &ItemFn) -> Result<(), TokenStream> { + all_args_non_self(f)?; + + Ok(()) +} + +fn all_args_non_self(f: &ItemFn) -> Result<(), TokenStream> { + let first_self_arg = f + .sig + .inputs + .iter() + .find(|arg| matches!(arg, FnArg::Receiver(_))); + + match first_self_arg { + None => Ok(()), + Some(arg) => err(arg, "`self` parameters are forbidden"), + } +} + +/// Helper function to generate `compile_error!()` outputs +fn err(span: impl Spanned, s: &str) -> Result<(), TokenStream> { + Err(quote_spanned! { span.span() => compile_error!(#s) }) +} + +#[cfg(test)] +mod tests { + use syn::parse_str; + + use super::*; + + #[test] + fn validate_fails_with_self_arg() { + let invalids = [ + "fn foo(self) {}", + "fn foo(&self) {}", + "fn foo(&mut self) {}", + "fn foo(self: Self) {}", + "fn foo(self: &Self) {}", + "fn foo(self: &mut Self) {}", + "fn foo(self: Box) {}", + "fn foo(self: Rc) {}", + "fn foo(self: Arc) {}", + ]; + + for invalid in invalids { + let f: ItemFn = parse_str(invalid).unwrap(); + assert!(validate(&f).is_err()); + } + } +} diff --git a/proptest/Cargo.toml b/proptest/Cargo.toml index 0239898b..dd52fd25 100644 --- a/proptest/Cargo.toml +++ b/proptest/Cargo.toml @@ -23,6 +23,8 @@ default = ["std", "fork", "timeout", "bit-set"] # Everything in `default` that doesn't break code coverage builds default-code-coverage = ["std", "fork", "timeout", "bit-set"] +attr-macro = ["proptest-macro"] + # Enables unstable features of Rust. unstable = [] @@ -57,6 +59,7 @@ bit-set = [ "dep:bit-set", "dep:bit-vec" ] [dependencies] bitflags = "2" unarray = "0.1.4" +proptest-macro = { path = "../proptest-macro", optional = true } # [dependencies.hashmap_core] # version = "0.1.5" @@ -111,9 +114,10 @@ optional = true version = "0.52.0" optional = true -[dev-dependencies] -regex = "1" - [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] + +[dev-dependencies] +regex = "1.0" +trybuild = "=1.0.0" diff --git a/proptest/src/lib.rs b/proptest/src/lib.rs index da37075d..db48b6ee 100644 --- a/proptest/src/lib.rs +++ b/proptest/src/lib.rs @@ -96,3 +96,13 @@ pub mod sample; pub mod string; pub mod prelude; + +#[cfg(feature = "attr-macro")] +pub use proptest_macro::property_test; + +#[cfg(feature = "attr-macro")] +#[test] +fn compile_tests() { + let t = trybuild::TestCases::new(); + t.pass("tests/pass/*.rs"); +} diff --git a/proptest/tests/pass/hygiene.rs b/proptest/tests/pass/hygiene.rs new file mode 100644 index 00000000..b00e80c0 --- /dev/null +++ b/proptest/tests/pass/hygiene.rs @@ -0,0 +1,11 @@ + +fn main() {} + +struct MyTestArgs { + something_else: String, +} + +#[proptest::property_test] +fn my_test(x: i32) { + assert_eq!(x, x); +} diff --git a/proptest/tests/pass/simple_example.rs b/proptest/tests/pass/simple_example.rs new file mode 100644 index 00000000..e27cd171 --- /dev/null +++ b/proptest/tests/pass/simple_example.rs @@ -0,0 +1,6 @@ +fn main() {} + +#[proptest::property_test] +fn my_test(x: i32) { + assert_eq!(x, x); +} diff --git a/proptest/tests/pass/with_params.rs b/proptest/tests/pass/with_params.rs new file mode 100644 index 00000000..2589cb6c --- /dev/null +++ b/proptest/tests/pass/with_params.rs @@ -0,0 +1,21 @@ +fn main() {} + +#[proptest::property_test( + config = proptest::test_runner::Config { + cases: 10, + ..Default::default() + } +)] +fn no_trailing_comma(x: i32) { + assert_eq!(x, x); +} + +#[proptest::property_test( + config = proptest::test_runner::Config { + cases: 10, + ..Default::default() + } +)] +fn trailing_comma(x: i32,) { + assert_eq!(x, x); +}