Skip to content

Commit

Permalink
restructured error handling in mock store
Browse files Browse the repository at this point in the history
  • Loading branch information
nrxus committed May 15, 2022
1 parent 2db2b8a commit 04e8c93
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 68 deletions.
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ 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
3 changes: 2 additions & 1 deletion faux_macros/src/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,14 @@ impl From<Mockable> for proc_macro::TokenStream {
let Mockable { real, morphed } = mockable;
let (impl_generics, ty_generics, where_clause) = real.generics.split_for_impl();
let name = &morphed.ident;
let name_str = name.to_string();

proc_macro::TokenStream::from(quote! {
#morphed

impl #impl_generics #name #ty_generics #where_clause {
pub fn faux() -> Self {
Self(faux::MaybeFaux::faux())
Self(faux::MaybeFaux::faux(#name_str))
}
}

Expand Down
12 changes: 6 additions & 6 deletions faux_macros/src/methods/morphed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,15 +215,13 @@ impl<'a> Signature<'a> {
quote! { (#(#args,)*) }
};

let struct_and_method_name =
format!("{}::{}", morphed_ty.to_token_stream(), name);
let fn_name = name.to_string();

quote! {
unsafe {
match q.call_stub(<Self>::#faux_ident, #args) {
match q.call_stub(<Self>::#faux_ident, #fn_name, #args) {
std::result::Result::Ok(o) => o,
std::result::Result::Err(e) => {
panic!("failed to call stub on '{}':\n{}", #struct_and_method_name, e);
}
std::result::Result::Err(e) => panic!("{}", e),
}
}
}
Expand Down Expand Up @@ -322,12 +320,14 @@ impl<'a> MethodData<'a> {

let empty = syn::parse_quote! { () };
let output = output.unwrap_or(&empty);
let name_str = name.to_string();

let when_method = syn::parse_quote! {
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,
#name_str,
faux
),
faux::MaybeFaux::Real(_) => panic!("not allowed to stub a real instance!"),
Expand Down
73 changes: 57 additions & 16 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,8 @@ pub use matcher::ArgMatcher;

mod mock;

use core::fmt;
use std::fmt::Formatter;
use std::sync::Arc;

/// What all mockable structs get transformed into.
Expand Down Expand Up @@ -945,8 +947,8 @@ impl<T: Default> Default for MaybeFaux<T> {
}

impl<T> MaybeFaux<T> {
pub fn faux() -> Self {
MaybeFaux::Faux(Faux::default())
pub fn faux(name: &'static str) -> Self {
MaybeFaux::Faux(Faux::new(name))
}
}

Expand All @@ -956,17 +958,23 @@ impl<T> MaybeFaux<T> {
/// documented. Its mere existence is an implementation detail and not
/// meant to be relied upon.
#[doc(hidden)]
#[derive(Clone, Debug, Default)]
#[derive(Clone, Debug)]
pub struct Faux {
store: Arc<mock::Store>,
store: Arc<mock::Store<'static>>,
}

impl Faux {
pub fn new(name: &'static str) -> Self {
Faux {
store: Arc::new(mock::Store::new(name)),
}
}

/// 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> {
pub(crate) fn unique_store(&mut self) -> Option<&mut mock::Store<'static>> {
Arc::get_mut(&mut self.store)
}

Expand All @@ -983,19 +991,52 @@ impl Faux {
///
/// 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())?;
pub unsafe fn call_stub<R, I, O>(
&self,
id: fn(R, I) -> O,
fn_name: &'static str,
input: I,
) -> Result<O, InvocationError> {
let mock = self.store.get(id, fn_name)?;
mock.call(input).map_err(|stub_error| InvocationError {
fn_name: mock.name(),
struct_name: self.store.struct_name,
stub_error,
})
}
}

pub struct InvocationError {
struct_name: &'static str,
fn_name: &'static str,
stub_error: mock::InvocationError,
}

mock.call(input).map_err(|errors| {
if errors.is_empty() {
"✗ method was never stubbed".to_owned()
} else {
errors.join("\n\n")
impl fmt::Display for InvocationError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match &self.stub_error {
mock::InvocationError::NeverStubbed => write!(
f,
"`{}::{}` was called but never stubbed",
self.struct_name, self.fn_name
),
mock::InvocationError::Stub(errors) => {
writeln!(
f,
"`{}::{}` had no suitable stubs. Existing stubs failed because:",
self.struct_name, self.fn_name
)?;
let mut errors = errors.iter();
if let Some(e) = errors.next() {
f.write_str("✗ ")?;
fmt::Display::fmt(e, f)?;
}
errors.try_for_each(|e| {
f.write_str("\n\n✗ ")?;
fmt::Display::fmt(e, f)
})
}
})
}
}
}

Expand Down
41 changes: 27 additions & 14 deletions src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,29 @@ pub mod stub;
mod store;
mod unchecked;

use std::fmt::{self, Formatter};
use std::{
fmt::{self, Formatter},
sync::Mutex,
};

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>>>,
fn_name: &'static str,
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![] }
pub fn new(fn_name: &'static str) -> Self {
Self {
fn_name,
stubs: vec![],
}
}

/// Attempts to invoke the mock
Expand All @@ -30,34 +35,42 @@ impl<'stub, I, O> Mock<'stub, I, O> {
/// 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>> {
pub fn call(&self, mut input: I) -> Result<O, InvocationError> {
let mut errors = vec![];

for stub in self.stubs.iter().rev() {
match stub.lock().call(input) {
match stub.lock().unwrap().call(input) {
Err((i, e)) => {
errors.push(format!("✗ {}", e));
errors.push(e);
input = i
}
Ok(o) => return Ok(o),
}
}

Err(errors)
Err(if errors.is_empty() {
InvocationError::NeverStubbed
} else {
InvocationError::Stub(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()
pub fn name(&self) -> &'static str {
self.fn_name
}
}

#[derive(Debug)]
pub enum InvocationError {
NeverStubbed,
Stub(Vec<stub::Error>),
}

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()
Expand Down
58 changes: 49 additions & 9 deletions src/mock/store.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,70 @@
use std::collections::HashMap;

use crate::InvocationError;

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

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

impl Store {
impl<'stub> Store<'stub> {
pub fn new(struct_name: &'static str) -> Self {
Store {
struct_name,
stubs: HashMap::new(),
}
}

/// 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> {
pub fn get_or_create<R, I, O>(
&mut self,
id: fn(R, I) -> O,
fn_name: &'static str,
) -> &mut Mock<'stub, I, O> {
let mock = self.stubs.entry(id as usize).or_insert_with(|| {
let mock: Mock<I, O> = Mock::new();
let mock: Mock<I, O> = Mock::new(fn_name);
mock.into()
});

unsafe { mock.as_typed_mut() }
let mock = unsafe { mock.as_typed_mut() };
assert_name(mock, fn_name);
mock
}

/// 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())
pub unsafe fn get<R, I, O>(
&self,
id: fn(R, I) -> O,
fn_name: &'static str,
) -> Result<&Mock<'stub, I, O>, InvocationError> {
match self.stubs.get(&(id as usize)).map(|m| m.as_typed()) {
Some(mock) => {
assert_name(mock, fn_name);
Ok(mock)
}
None => Err(InvocationError {
fn_name,
struct_name: self.struct_name,
stub_error: super::InvocationError::NeverStubbed,
}),
}
}
}

fn assert_name<I, O>(mock: &Mock<I, O>, fn_name: &'static str) {
assert_eq!(
mock.name(),
fn_name,
"faux bug: conflicting mock names: '{}' vs '{}'",
mock.name(),
fn_name
);
}
Loading

0 comments on commit 04e8c93

Please sign in to comment.