Skip to content

Commit

Permalink
simplify mock store
Browse files Browse the repository at this point in the history
* moved the unsafest code into its own module
* removed a lot of the unecessary locking and ref counting
  • Loading branch information
nrxus committed Mar 14, 2022
1 parent 12a7f59 commit 2db2b8a
Show file tree
Hide file tree
Showing 13 changed files with 439 additions and 317 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 1 addition & 1 deletion faux_macros/src/methods/morphed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<Self>::#faux_ident,
Expand Down
106 changes: 100 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,9 +250,6 @@
//!
//! [mocks]: https://martinfowler.com/articles/mocksArentStubs.html
mod mock_store;
mod stub;

pub mod matcher;
pub mod when;

Expand Down Expand Up @@ -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: Sync>(_: T) {}
///
/// implements_sync(3);
/// implements_sync(faux::MaybeFaux::Real(3));
/// ```
///
/// ```
/// fn implements_debug<T: std::fmt::Debug>(_: T) {}
///
/// implements_debug(3);
/// implements_debug(faux::MaybeFaux::Real(3));
/// ```
///
/// ```
/// fn implements_default<T: Default>(_: T) {}
///
/// implements_default(3);
/// implements_default(faux::MaybeFaux::Real(3));
/// ```
#[doc(hidden)]
#[derive(Clone, Debug)]
pub enum MaybeFaux<T> {
Real(T),
Faux(Faux),
}

impl<T: Default> Default for MaybeFaux<T> {
fn default() -> Self {
MaybeFaux::Real(T::default())
}
}

impl<T> MaybeFaux<T> {
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<mock::Store>,
}

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<R, I, O>(&self, id: fn(R, I) -> O, input: I) -> Result<O, String> {
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;
65 changes: 65 additions & 0 deletions src/mock.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<Stub<'stub, I, O>>>,
}

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<O, Vec<String>> {
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<I, O> Default for Mock<'_, I, O> {
fn default() -> Self {
Self::new()
}
}

impl<I, O> fmt::Debug for Mock<'_, I, O> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Mock").field("stubs", &self.stubs).finish()
}
}
30 changes: 30 additions & 0 deletions src/mock/store.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use std::collections::HashMap;

use super::{unchecked::Unchecked, Mock};

#[derive(Debug, Default)]
pub struct Store {
stubs: HashMap<usize, Unchecked<'static>>,
}

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<R, I, O>(&mut self, id: fn(R, I) -> O) -> &mut Mock<I, O> {
let mock = self.stubs.entry(id as usize).or_insert_with(|| {
let mock: Mock<I, O> = 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<R, I, O>(&self, id: fn(R, I) -> O) -> Option<&Mock<I, O>> {
self.stubs.get(&(id as usize)).map(|m| m.as_typed())
}
}
89 changes: 89 additions & 0 deletions src/mock/stub.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use std::{
fmt::{self, Formatter},
num::NonZeroUsize,
};

use crate::matcher::InvocationMatcher;

pub struct Stub<'a, I, O> {
matcher: Box<dyn InvocationMatcher<I> + Send>,
answer: Answer<'a, I, O>,
}

pub enum Answer<'a, I, O> {
Exhausted,
Once(Box<dyn FnOnce(I) -> O + Send + 'a>),
Many {
stub: Box<dyn FnMut(I) -> O + Send + 'a>,
times: Times,
},
}

#[derive(Debug, Clone, Copy)]
pub enum Times {
Always,
Times(NonZeroUsize),
}

impl Times {
pub fn decrement(self) -> Option<Self> {
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<I> + Send + 'static,
) -> Self {
Stub {
matcher: Box::new(matcher),
answer: stub,
}
}

pub fn call(&mut self, input: I) -> Result<O, (I, String)> {
// 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<O, (I, String)> {
// 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()
}
}
Loading

0 comments on commit 2db2b8a

Please sign in to comment.