diff options
| author | HampusM <hampus@hampusmat.com> | 2023-03-25 11:58:16 +0100 | 
|---|---|---|
| committer | HampusM <hampus@hampusmat.com> | 2023-03-25 11:58:16 +0100 | 
| commit | aef63bfecb7731c0307cc65eab0b9a06b8a7363d (patch) | |
| tree | 8455d54be4fd4008ac593fae5e9e9608198a61b5 | |
| parent | 96a13690f8552386bb183ebc01418774047ebbfc (diff) | |
feat: add argument matching
| -rw-r--r-- | Cargo.toml | 1 | ||||
| -rw-r--r-- | examples/basic.rs | 20 | ||||
| -rw-r--r-- | examples/generic_method.rs | 25 | ||||
| -rw-r--r-- | macros/src/expectation.rs | 164 | ||||
| -rw-r--r-- | macros/src/mock.rs | 18 | ||||
| -rw-r--r-- | src/lib.rs | 3 | 
6 files changed, 220 insertions, 11 deletions
@@ -10,4 +10,5 @@ repository = "https://git.hampusmat.com/ridicule"  members = ["macros"]  [dependencies] +predicates = "3.0.1"  ridicule-macros = { path = "./macros" } diff --git a/examples/basic.rs b/examples/basic.rs index a157413..01688f5 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,8 +1,9 @@  use ridicule::mock; +use ridicule::predicate::{always, eq}; -trait Foo: Sized +trait Foo  { -    fn bar(&self, num: u128) -> &Self; +    fn bar(&self, num: u128, text: &str);  }  mock! { @@ -10,7 +11,7 @@ mock! {      impl Foo for MockFoo      { -        fn bar(&self, num: u128) -> &Self; +        fn bar(&self, num: u128, text: &str);      }  } @@ -18,11 +19,12 @@ fn main()  {      let mut mock_foo = MockFoo::new(); -    mock_foo.expect_bar().returning(|me, num| { -        println!("bar was called with {num}"); +    mock_foo +        .expect_bar() +        .with(eq(1234), always()) +        .returning(|_, num, text| { +            println!("bar was called with {num} and '{text}'"); +        }); -        me -    }); - -    mock_foo.bar(123); +    mock_foo.bar(1234, "Hello");  } diff --git a/examples/generic_method.rs b/examples/generic_method.rs index 4262ee2..7283810 100644 --- a/examples/generic_method.rs +++ b/examples/generic_method.rs @@ -1,12 +1,14 @@  use std::fmt::Display; +use predicates::float::is_close;  use ridicule::mock; +use ridicule::predicate::function;  trait Foo  {      fn bar<Baz: Display>(&self, num: u128) -> Baz; -    fn abc<Baz: Display>(&mut self, baz: Baz); +    fn abc<ThingA, ThingB>(&mut self, thing_a: ThingA, thing_b: ThingB);  }  mock! { @@ -16,7 +18,7 @@ mock! {      {          fn bar<Baz: Display>(&self, num: u128) -> Baz; -        fn abc<Baz: Display>(&mut self, baz_baz: Baz); +        fn abc<ThingA, ThingB>(&mut self, thing_a: ThingA, thing_b: ThingB);      }  } @@ -39,6 +41,17 @@ fn main()          128u8      }); +    mock_foo +        .expect_abc::<&str, f64>() +        .with( +            function(|thing_a: &&str| thing_a.starts_with("Good morning")), +            is_close(7.13081), +        ) +        .returning(|_me, _thing_a, _thing_b| { +            println!("abc was called"); +        }) +        .times(1); +      assert_eq!(mock_foo.bar::<String>(123), "Hello".to_string());      assert_eq!(mock_foo.bar::<String>(123), "Hello".to_string());      assert_eq!(mock_foo.bar::<String>(123), "Hello".to_string()); @@ -47,4 +60,12 @@ fn main()      // mock_foo.bar::<String>(123);      assert_eq!(mock_foo.bar::<u8>(456), 128); + +    mock_foo.abc( +        concat!( +            "Good morning, and in case I don't see ya, good afternoon,", +            " good evening, and good night!" +        ), +        7.13081f64, +    );  } diff --git a/macros/src/expectation.rs b/macros/src/expectation.rs index af604ae..8120cfd 100644 --- a/macros/src/expectation.rs +++ b/macros/src/expectation.rs @@ -16,13 +16,20 @@ use syn::{      ImplItemMethod,      ItemStruct,      Lifetime, +    Pat, +    PatIdent, +    PatType,      Path,      PathSegment,      Receiver,      ReturnType,      Token, +    TraitBound, +    TraitBoundModifier,      Type,      TypeBareFn, +    TypeImplTrait, +    TypeParamBound,      TypePath,      TypeReference,      Visibility, @@ -174,6 +181,7 @@ impl Expectation          generics: Generics,          phantom_fields: &[PhantomField],          returning_fn: &Type, +        boxed_predicate_types: &[Type],      ) -> ItemStruct      {          ItemStruct { @@ -225,6 +233,24 @@ impl Expectation                      },                  ]                  .into_iter() +                .chain(boxed_predicate_types.iter().enumerate().map( +                    |(index, boxed_predicate_type)| Field { +                        attrs: vec![], +                        vis: Visibility::Inherited, +                        ident: Some(format_ident!("predicate_{index}")), +                        colon_token: Some(<Token![:]>::default()), +                        ty: Type::Path(TypePath::new(Path::new( +                            WithLeadingColons::No, +                            [PathSegment::new( +                                format_ident!("Option"), +                                Some(AngleBracketedGenericArguments::new( +                                    WithColons::No, +                                    [GenericArgument::Type(boxed_predicate_type.clone())], +                                )), +                            )], +                        ))), +                    }, +                ))                  .chain(phantom_fields.iter().cloned().map(Field::from))                  .collect(),              }), @@ -267,11 +293,60 @@ impl ToTokens for Expectation          let method_ident = &self.method_ident; +        let arg_types_no_refs = self +            .arg_types +            .iter() +            .map(|arg_type| match arg_type { +                Type::Reference(type_ref) => &*type_ref.elem, +                ty => ty, +            }) +            .collect::<Vec<_>>(); + +        let predicate_paths = arg_types_no_refs +            .iter() +            .map(|arg_type| { +                Path::new( +                    WithLeadingColons::Yes, +                    [ +                        PathSegment::new(format_ident!("ridicule"), None), +                        PathSegment::new( +                            format_ident!("Predicate"), +                            Some(AngleBracketedGenericArguments::new( +                                WithColons::No, +                                [GenericArgument::Type((*arg_type).clone())], +                            )), +                        ), +                    ], +                ) +            }) +            .collect::<Vec<_>>(); + +        let boxed_predicate_types = arg_types_no_refs +            .iter() +            .map(|arg_type| { +                Type::Path(TypePath::new(Path::new( +                    WithLeadingColons::Yes, +                    [ +                        PathSegment::new(format_ident!("ridicule"), None), +                        PathSegment::new(format_ident!("__private"), None), +                        PathSegment::new( +                            format_ident!("BoxPredicate"), +                            Some(AngleBracketedGenericArguments::new( +                                WithColons::No, +                                [GenericArgument::Type((*arg_type).clone())], +                            )), +                        ), +                    ], +                ))) +            }) +            .collect::<Vec<_>>(); +          let expectation_struct = Self::create_struct(              self.ident.clone(),              generics.clone(),              phantom_fields,              &returning_fn, +            &boxed_predicate_types,          );          let boundless_generics = generics.clone().strip_where_clause_and_bounds(); @@ -284,6 +359,70 @@ impl ToTokens for Expectation              quote! { unsafe { std::mem::transmute(self) } }          }; +        let with_arg_names = (0..self.arg_types.len()) +            .map(|index| format_ident!("predicate_{index}")) +            .collect::<Vec<_>>(); + +        let with_args = +            predicate_paths +                .iter() +                .enumerate() +                .map(|(index, predicate_path)| { +                    FnArg::Typed(PatType { +                        attrs: vec![], +                        pat: Box::new(Pat::Ident(PatIdent { +                            attrs: vec![], +                            by_ref: None, +                            mutability: None, +                            ident: format_ident!("predicate_{index}"), +                            subpat: None, +                        })), +                        colon_token: <Token![:]>::default(), +                        ty: Box::new(Type::ImplTrait(TypeImplTrait { +                            impl_token: <Token![impl]>::default(), +                            bounds: [ +                                TypeParamBound::Trait(TraitBound { +                                    paren_token: None, +                                    modifier: TraitBoundModifier::None, +                                    lifetimes: None, +                                    path: predicate_path.clone(), +                                }), +                                TypeParamBound::Trait(TraitBound { +                                    paren_token: None, +                                    modifier: TraitBoundModifier::None, +                                    lifetimes: None, +                                    path: create_path!(Send), +                                }), +                                TypeParamBound::Trait(TraitBound { +                                    paren_token: None, +                                    modifier: TraitBoundModifier::None, +                                    lifetimes: None, +                                    path: create_path!(Sync), +                                }), +                                TypeParamBound::Lifetime(Lifetime::create( +                                    format_ident!("static"), +                                )), +                            ] +                            .into_iter() +                            .collect(), +                        })), +                    }) +                }); + +        let check_predicates_arg_names = (0..self.arg_types.len()) +            .map(|index| format_ident!("arg_{index}")) +            .collect::<Vec<_>>(); + +        let arg_types = &self.arg_types; + +        let predicate_field_inits = (0..boxed_predicate_types.len()) +            .map(|index| { +                let ident = format_ident!("predicate_{index}"); + +                quote! { #ident: None } +            }) +            .collect::<Vec<_>>(); +          quote! {              #expectation_struct @@ -295,6 +434,7 @@ impl ToTokens for Expectation                          call_cnt: ::std::sync::atomic::AtomicU32::new(0),                          call_cnt_expectation:                              ::ridicule::__private::CallCountExpectation::Unlimited, +                        #(#predicate_field_inits,)*                          #(#phantom_fields),*                      }                  } @@ -324,6 +464,30 @@ impl ToTokens for Expectation                      self                  } +                pub fn with(&mut self, #(#with_args),*) -> &mut Self +                { +                    #( +                        self.#with_arg_names = Some( +                            ::ridicule::__private::BoxPredicate::new(#with_arg_names) +                        ); +                    )* + +                    self +                } + +                fn check_predicates(&self, #(#check_predicates_arg_names: &#arg_types),*) +                { +                    use ::ridicule::Predicate; + +                    #( +                        if let Some(predicate) = &self.#with_arg_names { +                            if !predicate.eval(&#check_predicates_arg_names) { +                                panic!("Predicate '{}' evaluated to false", predicate); +                            } +                        } +                    )* +                } +                  #[allow(unused)]                  fn strip_generic_params(                      self, diff --git a/macros/src/mock.rs b/macros/src/mock.rs index 9aa03f4..4d040a9 100644 --- a/macros/src/mock.rs +++ b/macros/src/mock.rs @@ -226,6 +226,22 @@ fn create_mock_function(          })          .collect::<Vec<_>>(); +    let typed_args = item_method +        .sig +        .inputs +        .iter() +        .filter_map(|fn_arg| match fn_arg { +            FnArg::Typed(arg) => { +                let Pat::Ident(pat_ident) = arg.pat.as_ref() else { +                    return None; +                }; + +                Some(pat_ident.ident.clone()) +            } +            FnArg::Receiver(_) => None, +        }) +        .collect::<Vec<_>>(); +      let expectations_field = format_ident!("{func_ident}_expectations");      ImplItemMethod { @@ -257,6 +273,8 @@ fn create_mock_function(                      ))                      .with_generic_params::<#(#type_param_idents,)*>(); +                expectation.check_predicates(#(&#typed_args),*); +                  (expectation.get_returning())(#(#args),*)              }          }) @@ -1,11 +1,14 @@  //! Mocking library supporting non-static generics.  #![deny(clippy::all, clippy::pedantic, missing_docs)] +pub use predicates::prelude::*;  pub use ridicule_macros::mock;  #[doc(hidden)]  pub mod __private  { +    pub use predicates::BoxPredicate; +      pub mod type_id      {          #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]  | 
