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)] |