From 2db2b8a9f8763268f8b8038605bc13b5c5288da2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Medina?= Date: Mon, 14 Mar 2022 05:16:20 -0700 Subject: [PATCH] simplify mock store * moved the unsafest code into its own module * removed a lot of the unecessary locking and ref counting --- CHANGELOG.md | 11 +++ Cargo.toml | 1 + faux_macros/src/methods/morphed.rs | 2 +- src/lib.rs | 106 +++++++++++++++++++-- src/mock.rs | 65 +++++++++++++ src/mock/store.rs | 30 ++++++ src/mock/stub.rs | 89 ++++++++++++++++++ src/mock/unchecked.rs | 80 ++++++++++++++++ src/mock_store.rs | 117 ----------------------- src/stub.rs | 144 ----------------------------- src/when.rs | 69 +++++++------- src/when/once.rs | 29 +++--- tests/clone.rs | 13 ++- 13 files changed, 439 insertions(+), 317 deletions(-) create mode 100644 src/mock.rs create mode 100644 src/mock/store.rs create mode 100644 src/mock/stub.rs create mode 100644 src/mock/unchecked.rs delete mode 100644 src/mock_store.rs delete mode 100644 src/stub.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eebec7..a25140a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # CHANGELOG +## NEXT + +### Minor Breaking Change +* Panics when adding mocks to a cloned mock. + * The goal (apart from optimizations) is to reduce magical + action-at-a-distance problems caused by having two mock instances + add new stubs that affect each other. The goal behind allowing + cloning in the previous release was so that the code under test + can call for clone without the test needing to be aware of those + implementation details, not for the test itself to be able to clone. + ## v0.1.6 * Support cloning mocks using `#[derive(Clone)]` * If a mockable struct is annotated with `#[derive(Clone)]`, cloning diff --git a/Cargo.toml b/Cargo.toml index 18e1b31..bcf5588 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ readme = "README.md" [dependencies] faux_macros = { path = "faux_macros", version = "0.1.6" } paste = "1.0.4" +parking_lot = "0.11.2" [dev-dependencies] futures = "0.3.9" diff --git a/faux_macros/src/methods/morphed.rs b/faux_macros/src/methods/morphed.rs index 263ba96..4b797b2 100644 --- a/faux_macros/src/methods/morphed.rs +++ b/faux_macros/src/methods/morphed.rs @@ -324,7 +324,7 @@ impl<'a> MethodData<'a> { let output = output.unwrap_or(&empty); let when_method = syn::parse_quote! { - pub fn #when_ident(&mut self) -> faux::When<#receiver_tokens, (#(#arg_types),*), #output, faux::matcher::AnyInvocation> { + pub fn #when_ident<'m>(&'m mut self) -> faux::When<'m, #receiver_tokens, (#(#arg_types),*), #output, faux::matcher::AnyInvocation> { match &mut self.0 { faux::MaybeFaux::Faux(faux) => faux::When::new( ::#faux_ident, diff --git a/src/lib.rs b/src/lib.rs index 915252e..3d5ea9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -250,9 +250,6 @@ //! //! [mocks]: https://martinfowler.com/articles/mocksArentStubs.html -mod mock_store; -mod stub; - pub mod matcher; pub mod when; @@ -901,9 +898,106 @@ pub use when::When; #[doc(inline)] pub use matcher::ArgMatcher; -// exported so generated code can call for it -// but purposefully not documented -pub use mock_store::{MaybeFaux, MockStore}; +mod mock; + +use std::sync::Arc; + +/// What all mockable structs get transformed into. +/// +/// Either a real instance or a mock store to store/retrieve all the +/// mocks. +/// +/// Exposed so generated code can use it for it but purposefully not +/// documented. Its definition is an implementation detail and thus +/// not meant to be relied upon. +/// +/// ``` +/// fn implements_sync(_: T) {} +/// +/// implements_sync(3); +/// implements_sync(faux::MaybeFaux::Real(3)); +/// ``` +/// +/// ``` +/// fn implements_debug(_: T) {} +/// +/// implements_debug(3); +/// implements_debug(faux::MaybeFaux::Real(3)); +/// ``` +/// +/// ``` +/// fn implements_default(_: T) {} +/// +/// implements_default(3); +/// implements_default(faux::MaybeFaux::Real(3)); +/// ``` +#[doc(hidden)] +#[derive(Clone, Debug)] +pub enum MaybeFaux { + Real(T), + Faux(Faux), +} + +impl Default for MaybeFaux { + fn default() -> Self { + MaybeFaux::Real(T::default()) + } +} + +impl MaybeFaux { + pub fn faux() -> Self { + MaybeFaux::Faux(Faux::default()) + } +} + +/// The internal representation of a mock object +/// +/// Exposed so generated code can use it but purposefully not +/// documented. Its mere existence is an implementation detail and not +/// meant to be relied upon. +#[doc(hidden)] +#[derive(Clone, Debug, Default)] +pub struct Faux { + store: Arc, +} + +impl Faux { + /// Return a mutable reference to its internal mock store + /// + /// Returns `None` if the store is being shared by multiple mock + /// instances. This occurs when cloning a mock instance. + pub(crate) fn unique_store(&mut self) -> Option<&mut mock::Store> { + Arc::get_mut(&mut self.store) + } + + #[doc(hidden)] + /// Attempt to call a stub for a given function and input. + /// + /// Stubs are attempted in the reverse order of how they were + /// inserted. Namely, the last inserted stub will be attempted + /// first. The first stub for whom the input passes its invocation + /// matcher will be activated and its output returned. If one + /// cannot be found an error is returned. + /// + /// # Safety + /// + /// Do *NOT* call this function directly. + /// This should only be called by the generated code from #[faux::methods] + pub unsafe fn call_stub(&self, id: fn(R, I) -> O, input: I) -> Result { + let mock = self + .store + .get(id) + .ok_or_else(|| "✗ method was never stubbed".to_owned())?; + + mock.call(input).map_err(|errors| { + if errors.is_empty() { + "✗ method was never stubbed".to_owned() + } else { + errors.join("\n\n") + } + }) + } +} #[cfg(doc)] mod readme_tests; diff --git a/src/mock.rs b/src/mock.rs new file mode 100644 index 0000000..e3bdaec --- /dev/null +++ b/src/mock.rs @@ -0,0 +1,65 @@ +pub mod stub; + +mod store; +mod unchecked; + +use std::fmt::{self, Formatter}; + +pub use self::{store::Store, stub::Stub}; + +use parking_lot::Mutex; + +/// A function mock +/// +/// Stores information about a mock, such as its stubs, with its +/// inputs and output typed. +pub struct Mock<'stub, I, O> { + pub(super) stubs: Vec>>, +} + +impl<'stub, I, O> Mock<'stub, I, O> { + /// Creates an empty mock + pub fn new() -> Self { + Self { stubs: vec![] } + } + + /// Attempts to invoke the mock + /// + /// Checks the given input against the stored stubs, invoking the + /// first stub whose invocation matcher suceeds for the + /// inputs. The stubs are checked in reverse insertion order such + /// that the last inserted stub is the first attempted + /// one. Returns an error if no stub is found for the given input. + pub fn call(&self, mut input: I) -> Result> { + let mut errors = vec![]; + + for stub in self.stubs.iter().rev() { + match stub.lock().call(input) { + Err((i, e)) => { + errors.push(format!("✗ {}", e)); + input = i + } + Ok(o) => return Ok(o), + } + } + + Err(errors) + } + + /// Adds a new stub for the mocked function + pub fn add_stub(&mut self, stub: Stub<'stub, I, O>) { + self.stubs.push(Mutex::new(stub)) + } +} + +impl Default for Mock<'_, I, O> { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Debug for Mock<'_, I, O> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Mock").field("stubs", &self.stubs).finish() + } +} diff --git a/src/mock/store.rs b/src/mock/store.rs new file mode 100644 index 0000000..9449e75 --- /dev/null +++ b/src/mock/store.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; + +use super::{unchecked::Unchecked, Mock}; + +#[derive(Debug, Default)] +pub struct Store { + stubs: HashMap>, +} + +impl Store { + /// Returns a mutable reference to a [`Mock`] for a given function + /// + /// If the given function has not yet been mocked, an empty mock + /// is created for the function. + pub fn get_or_create(&mut self, id: fn(R, I) -> O) -> &mut Mock { + let mock = self.stubs.entry(id as usize).or_insert_with(|| { + let mock: Mock = Mock::new(); + mock.into() + }); + + unsafe { mock.as_typed_mut() } + } + + /// Returns a reference to a [`Mock`] for a given function + /// + /// `None` is returned if the function was never mocked + pub unsafe fn get(&self, id: fn(R, I) -> O) -> Option<&Mock> { + self.stubs.get(&(id as usize)).map(|m| m.as_typed()) + } +} diff --git a/src/mock/stub.rs b/src/mock/stub.rs new file mode 100644 index 0000000..73a45cc --- /dev/null +++ b/src/mock/stub.rs @@ -0,0 +1,89 @@ +use std::{ + fmt::{self, Formatter}, + num::NonZeroUsize, +}; + +use crate::matcher::InvocationMatcher; + +pub struct Stub<'a, I, O> { + matcher: Box + Send>, + answer: Answer<'a, I, O>, +} + +pub enum Answer<'a, I, O> { + Exhausted, + Once(Box O + Send + 'a>), + Many { + stub: Box O + Send + 'a>, + times: Times, + }, +} + +#[derive(Debug, Clone, Copy)] +pub enum Times { + Always, + Times(NonZeroUsize), +} + +impl Times { + pub fn decrement(self) -> Option { + match self { + Times::Always => Some(self), + Times::Times(n) => NonZeroUsize::new(n.get() - 1).map(Times::Times), + } + } +} + +impl<'a, I, O> Stub<'a, I, O> { + pub fn new( + stub: Answer<'a, I, O>, + matcher: impl InvocationMatcher + Send + 'static, + ) -> Self { + Stub { + matcher: Box::new(matcher), + answer: stub, + } + } + + pub fn call(&mut self, input: I) -> Result { + // TODO: should the error message be different if the stub is also exhausted? + if let Err(e) = self.matcher.matches(&input) { + return Err((input, e)); + } + + self.answer.call(input) + } +} + +impl<'a, I, O> Answer<'a, I, O> { + fn call(&mut self, input: I) -> Result { + // no need to replace if we can keep decrementing + if let Answer::Many { stub, times } = self { + if let Some(decremented) = times.decrement() { + *times = decremented; + return Ok(stub(input)); + } + } + + // otherwise replace it with an exhaust + match std::mem::replace(self, Answer::Exhausted) { + Answer::Exhausted => Err((input, "this stub has been exhausted".to_string())), + Answer::Once(stub) => Ok(stub(input)), + Answer::Many { mut stub, .. } => Ok(stub(input)), + } + } +} + +impl<'a, I, O> fmt::Debug for Stub<'a, I, O> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Stub") + // TODO: Add debug information for InvocationMatcher + // .field("matcher", &self.matcher) + .field("answer", match &self.answer { + Answer::Exhausted => &"Exhausted", + Answer::Once(_) => &"Once", + Answer::Many { .. } => &"Many", + }) + .finish() + } +} diff --git a/src/mock/unchecked.rs b/src/mock/unchecked.rs new file mode 100644 index 0000000..52d0ae5 --- /dev/null +++ b/src/mock/unchecked.rs @@ -0,0 +1,80 @@ +//! THIS IS ONE OF THE MOST DANGEROUS PARTS OF `FAUX`. PROCEED WITH +//! EXTREME CAUTION. + +use std::fmt::{self, Formatter}; + +use super::Mock; + +/// Stores the a mock with its generics "erased" +/// +/// This allows different mocks to be saved in the same collections. +/// Ideally we would use something like `std::any::Any` instead but +/// dynamic casting only works on static types and we do not want to +/// limit `faux` to only working with static inputs/outputs. +pub struct Unchecked<'stub> { + unsafe_mock: Mock<'stub, (), ()>, + debug_repr: String, +} + +impl<'stub> Unchecked<'stub> { + /// Returns a reference to the mock with its types re-added. + /// + /// # Safety + /// + /// This method is *extremely* unsafe. This is only safe if you + /// know precisely what the input (I), output (O) were of the + /// original [`Mock`] this came from. + pub unsafe fn as_typed(&self) -> &Mock { + // Might be safer to only transmute only the matcher and stub + // of each mock instead of the entire object. This works + // though, and I don't see any reason why it wouldn't but if + // we start seeing seg-faults this is a potential thing to + // change. + let mock = &self.unsafe_mock; + std::mem::transmute(mock) + } + + /// Returns a mutable reference to the mock with its types + /// re-added. + /// + /// # Safety + /// + /// This method is *extremely* unsafe. This is only safe if you + /// know precisely what the input (I), output (O) were of the + /// original [`Mock`] this came from. + pub unsafe fn as_typed_mut(&mut self) -> &mut Mock { + // Might be safer to only transmute only the matcher and stub + // of each mock instead of the entire object. This works + // though, and I don't see any reason why it wouldn't but if + // we start seeing seg-faults this is a potential thing to + // change. + let mock = &mut self.unsafe_mock; + std::mem::transmute(mock) + } +} + +impl fmt::Debug for Unchecked<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.write_str(&self.debug_repr) + } +} + +impl<'stub, I, O> From> for Unchecked<'stub> { + fn from(mock: Mock) -> Self { + // we are about to lose all information about the mock so + // let's save its debug representation so we can print it as + // our own + let debug_repr = format!("{:?}", mock); + // Safety: + // The only posible actions on the returned `Saved` are: + // * as_checked_mut: already marked as `unsafe` + // * debug format: does not look into the unsafe fields + unsafe { + let unsafe_mock = std::mem::transmute(mock); + Self { + unsafe_mock, + debug_repr, + } + } + } +} diff --git a/src/mock_store.rs b/src/mock_store.rs deleted file mode 100644 index d05e6ec..0000000 --- a/src/mock_store.rs +++ /dev/null @@ -1,117 +0,0 @@ -use crate::stub::{self, Stub}; -use std::{ - collections::HashMap, - sync::{Arc, Mutex}, -}; - -#[doc(hidden)] -/// ``` -/// fn implements_sync(_: T) {} -/// -/// implements_sync(3); -/// implements_sync(faux::MaybeFaux::Real(3)); -/// ``` -/// -/// ``` -/// fn implements_debug(_: T) {} -/// -/// implements_debug(3); -/// implements_debug(faux::MaybeFaux::Real(3)); -/// ``` -/// -/// ``` -/// fn implements_default(_: T) {} -/// -/// implements_default(3); -/// implements_default(faux::MaybeFaux::Real(3)); -/// ``` - -#[derive(Clone, Debug)] -pub enum MaybeFaux { - Real(T), - Faux(MockStore), -} - -impl Default for MaybeFaux { - fn default() -> Self { - MaybeFaux::Real(T::default()) - } -} - -impl MaybeFaux { - pub fn faux() -> Self { - MaybeFaux::Faux(MockStore::new()) - } -} - -#[derive(Clone, Debug, Default)] -#[doc(hidden)] -pub struct MockStore { - stubs: Arc>>>>>>, -} - -impl MockStore { - fn new() -> Self { - MockStore { - stubs: Arc::new(Mutex::new(HashMap::new())), - } - } - - pub(crate) fn stub(&mut self, id: fn(R, I) -> O, stub: Stub<'static, I, O>) { - self.store_stub(id, stub) - } - - pub(crate) unsafe fn stub_unchecked( - &mut self, - id: fn(R, I) -> O, - stub: Stub<'_, I, O>, - ) { - // pretend the lifetime is static - self.store_stub(id, std::mem::transmute(stub)) - } - - fn store_stub(&mut self, id: fn(R, I) -> O, stub: Stub<'static, I, O>) { - let stubs = self - .stubs - .lock() - .unwrap() - .entry(id as usize) - .or_default() - .clone(); - - stubs.lock().unwrap().push(unsafe { stub.unchecked() }); - } - - #[doc(hidden)] - /// # Safety - /// - /// Do *NOT* call this function directly. - /// This should only be called by the generated code from #[faux::methods] - pub unsafe fn call_stub(&self, id: fn(R, I) -> O, mut input: I) -> Result { - let locked_store = self.stubs.lock().unwrap(); - let potential_stubs = locked_store - .get(&(id as usize)) - .cloned() - .ok_or_else(|| "✗ method was never stubbed".to_string())?; - - // drop the lock before calling the stub to avoid deadlocking in the mock - std::mem::drop(locked_store); - - let mut potential_subs = potential_stubs.lock().unwrap(); - let mut errors = vec![]; - - for stub in potential_subs.iter_mut().rev() { - match stub.call(input) { - Err((i, e)) => { - errors.push(format!("✗ {}", e)); - input = i - } - Ok(o) => return Ok(o), - } - } - - assert!(!errors.is_empty()); - - Err(errors.join("\n\n")) - } -} diff --git a/src/stub.rs b/src/stub.rs deleted file mode 100644 index 3c1c869..0000000 --- a/src/stub.rs +++ /dev/null @@ -1,144 +0,0 @@ -use crate::matcher::InvocationMatcher; -use std::fmt::{self, Formatter}; - -pub struct Stub<'a, I, O> { - matcher: Box + Send>, - stub: Answer<'a, I, O>, -} - -pub enum Answer<'a, I, O> { - Once(Box O + Send + 'a>), - Many { - stub: Box O + Send + 'a>, - times: Times, - }, -} - -pub struct Saved<'a> { - transmuted_matcher: Box + Send>, - stub: SavedAnswer<'a>, -} - -pub enum SavedAnswer<'a> { - Exhausted, - Once { - transmuted_stub: Box, - }, - Many { - transmuted_stub: Box, - times: Times, - }, -} - -#[derive(Debug)] -pub enum Times { - Always, - Times(usize), -} - -impl Times { - pub fn decrement(&mut self) { - if let Times::Times(times) = self { - *times -= 1; - } - } -} - -impl<'a, I, O> Stub<'a, I, O> { - pub fn new( - stub: Answer<'a, I, O>, - matcher: impl InvocationMatcher + Send + 'static, - ) -> Self { - Stub { - matcher: Box::new(matcher), - stub, - } - } - - pub unsafe fn unchecked(self) -> Saved<'a> { - let transmuted_matcher: Box + Send> = - std::mem::transmute(self.matcher); - let stub = match self.stub { - Answer::Once(stub) => SavedAnswer::Once { - transmuted_stub: std::mem::transmute(stub), - }, - Answer::Many { stub, times } => SavedAnswer::Many { - times, - transmuted_stub: std::mem::transmute(stub), - }, - }; - Saved { - transmuted_matcher, - stub, - } - } -} - -impl<'a> Saved<'a> { - /// # Safety - /// - /// Only call this method if you know for sure these are the right - /// input and output from the non-transmuted stubs - pub unsafe fn call(&mut self, input: I) -> Result { - let matcher = &mut *(&mut self.transmuted_matcher as *mut Box<_> - as *mut Box>); - - // TODO: should the error message be different if the stub is also exhausted? - if let Err(e) = matcher.matches(&input) { - return Err((input, e)); - } - - let just_exhausted = match &mut self.stub { - SavedAnswer::Once { .. } - | SavedAnswer::Many { - times: Times::Times(0), - .. - } - | SavedAnswer::Many { - times: Times::Times(1), - .. - } => std::mem::replace(&mut self.stub, SavedAnswer::Exhausted), - SavedAnswer::Many { - times, - transmuted_stub, - } => { - times.decrement(); - let stub = &mut *(transmuted_stub as *mut Box - as *mut Box O + Send>); - return Ok(stub(input)); - } - SavedAnswer::Exhausted => { - return Err((input, "this stub has been exhausted".to_string())) - } - }; - - match just_exhausted { - SavedAnswer::Once { transmuted_stub } => { - let stub: Box O> = std::mem::transmute(transmuted_stub); - Ok(stub(input)) - } - SavedAnswer::Many { - times: Times::Times(0), - .. - } => Err((input, "this stub has been exhausted".to_string())), - SavedAnswer::Many { - times: Times::Times(1), - transmuted_stub, - } => { - let mut stub: Box O> = std::mem::transmute(transmuted_stub); - Ok(stub(input)) - } - _ => unreachable!(), - } - } -} - -impl fmt::Debug for Saved<'_> { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - match &self.stub { - SavedAnswer::Exhausted => f.write_str("exhausted stub"), - SavedAnswer::Once { .. } => f.write_str("once stub"), - SavedAnswer::Many { times, .. } => write!(f, "stub {:?} times", times), - } - } -} diff --git a/src/when.rs b/src/when.rs index c389317..03b0d5a 100644 --- a/src/when.rs +++ b/src/when.rs @@ -2,12 +2,16 @@ mod once; +use std::num::NonZeroUsize; + use crate::{ matcher::{AnyInvocation, InvocationMatcher}, - mock_store::MockStore, - stub::{self, Stub}, + mock::{self, stub}, + Faux, }; + pub use once::Once; +use stub::Stub; /// Provides methods to stub the implementation or return value of the /// stubbed method. @@ -31,26 +35,28 @@ pub use once::Once; /// [`once`]: When::once /// [`times`]: When::times /// [`with_args`]: When::with_args -pub struct When<'q, R, I, O, M: InvocationMatcher> { +pub struct When<'m, R, I, O, M: InvocationMatcher> { id: fn(R, I) -> O, - store: &'q mut MockStore, - times: stub::Times, + store: &'m mut mock::Store, + times: Option, matcher: M, } -impl<'q, R, I, O> When<'q, R, I, O, AnyInvocation> { +impl<'m, R, I, O> When<'m, R, I, O, AnyInvocation> { #[doc(hidden)] - pub fn new(id: fn(R, I) -> O, store: &'q mut MockStore) -> Self { + pub fn new(id: fn(R, I) -> O, faux: &'m mut Faux) -> Self { + let store = faux.unique_store().expect("faux: failed to get unique handle to mock. Adding stubs to a mock instance may only be done prior to cloning the mock."); + When { id, store, matcher: AnyInvocation, - times: stub::Times::Always, + times: Some(mock::stub::Times::Always), } } } -impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> When<'q, R, I, O, M> { +impl<'m, R, I, O, M: InvocationMatcher + Send + 'static> When<'m, R, I, O, M> { /// Sets the return value of the stubbed method. /// /// Requires the value to be static. For a more lax but unsafe @@ -158,16 +164,7 @@ impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> When<'q, R, I, O, M> where O: 'static, { - self.store.stub( - self.id, - Stub::new( - stub::Answer::Many { - times: self.times, - stub: Box::new(stub), - }, - self.matcher, - ), - ); + self.add_stub(Box::new(stub)); } /// Analog of [`then_return`] that allows stubbing non-static @@ -320,17 +317,10 @@ impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> When<'q, R, I, O, M> /// ``` /// /// [`then`]: When::then - pub unsafe fn then_unchecked(self, mock: impl FnMut(I) -> O + Send) { - self.store.stub_unchecked( - self.id, - Stub::new( - stub::Answer::Many { - times: self.times, - stub: Box::new(mock), - }, - self.matcher, - ), - ); + pub unsafe fn then_unchecked(self, stub: impl FnMut(I) -> O + Send) { + let stub: Box O + Send> = Box::new(stub); + // pretend the lifetime is 'static + self.add_stub(std::mem::transmute(stub)); } /// Limits the number of calls for which a mock is active. @@ -397,7 +387,7 @@ impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> When<'q, R, I, O, M> /// } /// ``` pub fn times(mut self, times: usize) -> Self { - self.times = stub::Times::Times(times); + self.times = NonZeroUsize::new(times).map(stub::Times::Times); self } @@ -455,7 +445,7 @@ impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> When<'q, R, I, O, M> /// mock.single_arg(8); /// } /// ``` - pub fn once(self) -> Once<'q, R, I, O, M> { + pub fn once(self) -> Once<'m, R, I, O, M> { Once::new(self.id, self.store, self.matcher) } @@ -478,12 +468,23 @@ impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> When<'q, R, I, O, M> pub fn with_args + Send + 'static>( self, matcher: N, - ) -> When<'q, R, I, O, N> { + ) -> When<'m, R, I, O, N> { When { matcher, - times: self.times, id: self.id, store: self.store, + times: self.times, } } + + fn add_stub(self, stub: Box O + Send + 'static>) { + let answer = match self.times { + None => stub::Answer::Exhausted, + Some(times) => stub::Answer::Many { times, stub }, + }; + + self.store + .get_or_create(self.id) + .add_stub(Stub::new(answer, self.matcher)); + } } diff --git a/src/when/once.rs b/src/when/once.rs index 5fd3e67..ae16902 100644 --- a/src/when/once.rs +++ b/src/when/once.rs @@ -1,7 +1,6 @@ use crate::{ matcher::InvocationMatcher, - stub::{self, Stub}, - MockStore, + mock::{self, stub, Stub}, }; /// Similar to [When](struct.When), but only stubs once. @@ -12,15 +11,15 @@ use crate::{ /// Do *NOT* rely on the signature of `Once`. While changing the /// methods of `Once` will be considered a breaking change, changing /// the generics within `Once` will not. -pub struct Once<'q, R, I, O, M: InvocationMatcher> { +pub struct Once<'m, R, I, O, M: InvocationMatcher> { id: fn(R, I) -> O, - store: &'q mut MockStore, + store: &'m mut mock::Store, matcher: M, } -impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> Once<'q, R, I, O, M> { +impl<'m, R, I, O, M: InvocationMatcher + Send + 'static> Once<'m, R, I, O, M> { #[doc(hidden)] - pub fn new(id: fn(R, I) -> O, store: &'q mut MockStore, matcher: M) -> Self { + pub fn new(id: fn(R, I) -> O, store: &'m mut mock::Store, matcher: M) -> Self { Once { id, store, matcher } } @@ -92,10 +91,7 @@ impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> Once<'q, R, I, O, M> where O: 'static, { - self.store.stub( - self.id, - Stub::new(stub::Answer::Once(Box::new(stub)), self.matcher), - ); + self.add_stub(Box::new(stub)) } /// Analog of [When.then_unchecked_return] where the value does @@ -172,9 +168,14 @@ impl<'q, R, I, O, M: InvocationMatcher + Send + 'static> Once<'q, R, I, O, M> /// See [When.then_unchecked's safety]. /// pub unsafe fn then_unchecked(self, stub: impl FnOnce(I) -> O + Send) { - self.store.stub_unchecked( - self.id, - Stub::new(stub::Answer::Once(Box::new(stub)), self.matcher), - ); + let stub: Box O + Send> = Box::new(stub); + // pretend the lifetime is 'static + self.add_stub(std::mem::transmute(stub)); + } + + fn add_stub(self, stub: Box O + Send + 'static>) { + self.store + .get_or_create(self.id) + .add_stub(Stub::new(stub::Answer::Once(stub), self.matcher)); } } diff --git a/tests/clone.rs b/tests/clone.rs index 20f2785..9a01185 100644 --- a/tests/clone.rs +++ b/tests/clone.rs @@ -27,7 +27,18 @@ fn can_clone_real() { #[test] fn can_clone_mock() { let mut mock = Foo::faux(); - let cloned = mock.clone(); faux::when!(mock.get()).then_return(4); + + let cloned = mock.clone(); assert_eq!(cloned.get(), 4); + assert_eq!(mock.get(), 4); +} + +#[test] +#[should_panic] +fn panics_when_mocking_clone() { + let mock = Foo::faux(); + let mut cloned = mock.clone(); + + faux::when!(cloned.get()).then_return(4); }