diff --git a/CHANGELOG.md b/CHANGELOG.md index 07a12a5..6d5d370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,11 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +- Provide expectations clearing for automock. + Ex: Useful to configure a general mock behavior through a helper function, + and then specialize a single method by clearing it's initial behavior. + Partially addresses issue [#283](https://github.com/asomers/mockall/issues/283) + ## [ 0.14.0 ] - 2025-11-22 ### Added diff --git a/mockall/tests/automock_struct.rs b/mockall/tests/automock_struct.rs index d344167..b59c179 100644 --- a/mockall/tests/automock_struct.rs +++ b/mockall/tests/automock_struct.rs @@ -2,6 +2,7 @@ //! automocking a struct #![deny(warnings)] +use std::panic; use mockall::*; pub struct SimpleStruct {} @@ -20,3 +21,47 @@ fn returning() { .returning(|x| i64::from(x) + 1); assert_eq!(5, mock.foo(4)); } + +#[test] +fn clear() { + let mut mock = MockSimpleStruct::new(); + mock.expect_foo() + .returning(|x| i64::from(x) + 1); + assert_eq!(5, mock.foo(4)); + + mock.expect_foo().returning(|x| i64::from(x) + 2); + assert_eq!(5, mock.foo(4)); + + mock.clear_foo(); + + mock.expect_foo().returning(|x| i64::from(x) + 2); + assert_eq!(6, mock.foo(4)); +} + +#[test] +fn clear_and_expect() { + let mut mock = MockSimpleStruct::new(); + mock.expect_foo() + .returning(|x| i64::from(x) + 1); + assert_eq!(5, mock.foo(4)); + + mock.expect_foo().returning(|x| i64::from(x) + 2); + assert_eq!(5, mock.foo(4)); + + mock.clear_and_expect_foo().returning(|x| i64::from(x) + 2); + assert_eq!(6, mock.foo(4)); +} + +#[test] +fn calling_foo_without_expectation_after_clear() { + let mut mock = MockSimpleStruct::new(); + mock.expect_foo() + .returning(|x| i64::from(x) + 1); + assert_eq!(5, mock.foo(4)); + + mock.clear_foo(); + let result = panic::catch_unwind(|| { + mock.foo(4) + }); + assert!(result.is_err()); +} \ No newline at end of file diff --git a/mockall/tests/automock_trait.rs b/mockall/tests/automock_trait.rs index 81261d6..49e01aa 100644 --- a/mockall/tests/automock_trait.rs +++ b/mockall/tests/automock_trait.rs @@ -2,6 +2,7 @@ //! automocking a trait #![deny(warnings)] +use std::panic; use mockall::*; #[automock] @@ -16,3 +17,48 @@ fn returning() { .returning(|x| x + 1); assert_eq!(5, mock.foo(4)); } + +#[test] +fn clear() { + let mut mock = MockSimpleTrait::new(); + mock.expect_foo() + .returning(|x| x + 1); + assert_eq!(5, mock.foo(4)); + + mock.expect_foo().returning(|x| x + 2); + assert_eq!(5, mock.foo(4)); + + mock.clear_foo(); + + mock.expect_foo().returning(|x| x + 2); + assert_eq!(6, mock.foo(4)); +} + +#[test] +fn clear_and_expect() { + let mut mock = MockSimpleTrait::new(); + mock.expect_foo() + .returning(|x| x + 1); + assert_eq!(5, mock.foo(4)); + + mock.expect_foo().returning(|x| x + 2); + assert_eq!(5, mock.foo(4)); + + mock.clear_and_expect_foo().returning(|x| x + 2); + assert_eq!(6, mock.foo(4)); +} + +#[test] +fn calling_foo_without_expectation_after_clear() { + let mut mock = MockSimpleTrait::new(); + mock.expect_foo() + .returning(|x| x + 1); + assert_eq!(5, mock.foo(4)); + + mock.clear_foo(); + let result = panic::catch_unwind(|| { + mock.foo(4) + }); + assert!(result.is_err()); +} + diff --git a/mockall_derive/src/mock_function.rs b/mockall_derive/src/mock_function.rs index 8e43739..5866dc0 100644 --- a/mockall_derive/src/mock_function.rs +++ b/mockall_derive/src/mock_function.rs @@ -722,6 +722,119 @@ impl MockFunction { ) } + /// Generate code for the clear_ method + /// + /// # Arguments + /// + /// * `modname`: Name of the parent struct's private module + // Supplying modname is an unfortunately hack. Ideally MockFunction + // wouldn't need to know that. + pub fn clear(&self, modname: &Ident) + -> impl ToTokens + { + let attrs = AttrFormatter::new(&self.attrs) + .doc(false) + .format(); + let name = self.name(); + let clear_ident = format_ident!("clear_{}", name); + let funcname = &self.sig.ident; + let (_, tg, _) = if self.is_method_generic() { + &self.egenerics + } else { + &self.call_generics + }.split_for_impl(); + let (ig, _, wc) = self.call_generics.split_for_impl(); + let mut wc = wc.cloned(); + if self.is_method_generic() && (self.return_ref || self.return_refmut) { + // Add Senc + Sync, required for downcast, since Expectation + // stores an Option<#owned_output> + send_syncify(&mut wc, self.owned_output.clone()); + } + let tbf = tg.as_turbofish(); + let vis = &self.call_vis; + + let substruct_obj = if let Some(trait_) = &self.trait_ { + let ident = format_ident!("{trait_}_expectations"); + quote!(#ident.) + } else { + quote!() + }; + let docstr = format!("Clear the [`Expectation`]({}/{}/struct.Expectation.html) array for the mocked `{}` method", + modname, self.inner_mod_ident(), funcname); + quote!( + #[doc = #docstr] + #(#attrs)* + #vis fn #clear_ident #ig(&mut self) + { + self.#substruct_obj #name.clear #tbf(); + } + ) + } + + /// Generate code for the clear_and_expect_ method + /// + /// # Arguments + /// + /// * `modname`: Name of the parent struct's private module + /// * `self_args`: If supplied, these are the + /// AngleBracketedGenericArguments of the self type of the + /// trait impl. e.g. The `T` in `impl Foo for Bar`. + // Supplying modname is an unfortunately hack. Ideally MockFunction + // wouldn't need to know that. + pub fn clear_and_expect(&self, modname: &Ident, self_args: Option<&PathArguments>) + -> impl ToTokens + { + let attrs = AttrFormatter::new(&self.attrs) + .doc(false) + .format(); + let name = self.name(); + let clear_and_expect_intent = format_ident!("clear_and_expect_{}", name); + let expectation_obj = self.expectation_obj(self_args); + let funcname = &self.sig.ident; + let (_, tg, _) = if self.is_method_generic() { + &self.egenerics + } else { + &self.call_generics + }.split_for_impl(); + let (ig, _, wc) = self.call_generics.split_for_impl(); + let mut wc = wc.cloned(); + if self.is_method_generic() && (self.return_ref || self.return_refmut) { + // Add Senc + Sync, required for downcast, since Expectation + // stores an Option<#owned_output> + send_syncify(&mut wc, self.owned_output.clone()); + } + let tbf = tg.as_turbofish(); + let vis = &self.call_vis; + + #[cfg(not(feature = "nightly_derive"))] + let must_use = quote!(#[must_use = + "Must set return value when not using the \"nightly\" feature" + ]); + #[cfg(feature = "nightly_derive")] + let must_use = quote!(); + + let substruct_obj = if let Some(trait_) = &self.trait_ { + let ident = format_ident!("{trait_}_expectations"); + quote!(#ident.) + } else { + quote!() + }; + let docstr = format!("Clear the [`Expectation`]({}/{}/struct.Expectation.html) array, before creating one for mocking the `{}` method", + modname, self.inner_mod_ident(), funcname); + quote!( + #must_use + #[doc = #docstr] + #(#attrs)* + #vis fn #clear_and_expect_intent #ig(&mut self) + -> &mut #modname::#expectation_obj + #wc + { + self.#substruct_obj #name.clear #tbf(); + self.#substruct_obj #name.expect #tbf() + } + ) + } + /// Return the name of this function's expecation object fn expectation_obj(&self, self_args: Option<&PathArguments>) -> impl ToTokens @@ -1256,6 +1369,11 @@ impl ToTokens for CommonExpectationsMethods<'_> { &mut self.0[__mockall_l - 1] } + #v fn clear(&mut self) + { + self.0.clear(); + } + #v const fn new() -> Self { Self(Vec::new()) } @@ -2576,6 +2694,11 @@ impl ToTokens for StaticGenericExpectations<'_> { .unwrap() .expect() } + + #v fn clear #ig (&mut self) + { + self.store.clear(); + } } ).to_tokens(tokens) } diff --git a/mockall_derive/src/mock_item_struct.rs b/mockall_derive/src/mock_item_struct.rs index 004577d..2b5a7b5 100644 --- a/mockall_derive/src/mock_item_struct.rs +++ b/mockall_derive/src/mock_item_struct.rs @@ -262,6 +262,14 @@ impl ToTokens for MockItemStruct { .filter(|meth| !meth.is_static()) .map(|meth| meth.expect(modname, None)) .collect::>(); + let clears = self.methods.0.iter() + .filter(|meth| !meth.is_static()) + .map(|meth| meth.clear(modname)) + .collect::>(); + let clear_and_expects = self.methods.0.iter() + .filter(|meth| !meth.is_static()) + .map(|meth| meth.clear_and_expect(modname, None)) + .collect::>(); let method_checkpoints = self.methods.checkpoints(); let new_method = self.new_method(); let priv_mods = self.methods.priv_mods(); @@ -350,6 +358,8 @@ impl ToTokens for MockItemStruct { #(#calls)* #(#contexts)* #(#expects)* + #(#clears)* + #(#clear_and_expects)* /// Validate that all current expectations for all methods have /// been satisfied, and discard them. pub fn checkpoint(&mut self) { diff --git a/mockall_derive/src/mock_trait.rs b/mockall_derive/src/mock_trait.rs index fc3253c..5200243 100644 --- a/mockall_derive/src/mock_trait.rs +++ b/mockall_derive/src/mock_trait.rs @@ -155,6 +155,21 @@ impl MockTrait { meth.expect(modname, Some(path_args)) } }).collect::>(); + let clears = self.methods.iter() + .filter(|meth| !meth.is_static()) + .map(|meth| + meth.clear(modname) + ).collect::>(); + let clear_and_expects = self.methods.iter() + .filter(|meth| !meth.is_static()) + .map(|meth| { + if meth.is_method_generic() { + // Specific impls with generic methods are TODO. + meth.clear_and_expect(modname, None) + } else { + meth.clear_and_expect(modname, Some(path_args)) + } + }).collect::>(); let trait_path = &self.trait_path; let self_path = &self.self_path; let types = &self.types; @@ -169,6 +184,8 @@ impl MockTrait { #(#impl_attrs)* impl #ig #self_path #wc { #(#expects)* + #(#clears)* + #(#clear_and_expects)* #(#contexts)* } )