summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.toml1
-rw-r--r--examples/basic.rs20
-rw-r--r--examples/generic_method.rs25
-rw-r--r--macros/src/expectation.rs164
-rw-r--r--macros/src/mock.rs18
-rw-r--r--src/lib.rs3
6 files changed, 220 insertions, 11 deletions
diff --git a/Cargo.toml b/Cargo.toml
index 8edf1f1..6eb5d1a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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),*)
}
})
diff --git a/src/lib.rs b/src/lib.rs
index 3a1d983..24800e1 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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)]