Skip to content

Commit

Permalink
Print better expectation messages on stable (#425)
Browse files Browse the repository at this point in the history
This change allows better messages for "No matching expectation found"
on stable Rust. This is a great user experience upgrade as it allows
this behavior without requiring running with the nightly toolchain. This
PR applies the generalized technique for autoderef specialization
described at
https://lukaskalbertodt.github.io/2019/12/05/generalized-autoref-based-specialization.html
.
  • Loading branch information
jefftt committed Feb 9, 2023
1 parent 792e3ce commit 582640c
Show file tree
Hide file tree
Showing 15 changed files with 69 additions and 108 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).

- Raised MSRV to 1.60.0 because predicates-tree did.
([#430](https://github.com/asomers/mockall/pull/430))
- Better "No matching expectation found" messages on stable.
([#425](https://github.com/asomers/mockall/pull/425))

### Fixed

Expand Down
54 changes: 32 additions & 22 deletions mockall/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1146,7 +1146,7 @@
use downcast::*;
use std::{
any,
fmt::{self, Debug, Formatter},
fmt::Debug,
marker::PhantomData,
ops::{Range, RangeFrom, RangeFull, RangeInclusive, RangeTo,
RangeToInclusive},
Expand Down Expand Up @@ -1462,28 +1462,38 @@ pub struct DefaultReturner<O>(PhantomData<O>);
}
}

// Wrapper type to allow for better expectation messages for any type.
// Will first try Debug, otherwise will print '?'
#[doc(hidden)]
pub struct MaybeDebugger<'a, T>(pub &'a T);
::cfg_if::cfg_if! {
if #[cfg(feature = "nightly")] {
impl<'a, T> Debug for MaybeDebugger<'a, T> {
default fn fmt(&self, f: &mut Formatter<'_>)
-> Result<(), fmt::Error>
{
write!(f, "?")
}
}
impl<'a, T: Debug> Debug for MaybeDebugger<'a, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
self.0.fmt(f)
}
}
} else {
impl<'a, T> Debug for MaybeDebugger<'a, T> {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
write!(f, "?")
}
}
pub struct ArgPrinter<'a, T>(pub &'a T);

#[doc(hidden)]
pub struct DebugPrint<'a, T: Debug>(pub &'a T);
impl<'a, T> Debug for DebugPrint<'a, T> where T: Debug {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(self.0, f)
}
}
#[doc(hidden)]
pub trait ViaDebug<T> where T: Debug { fn debug_string(&self) -> DebugPrint<'_, T>; }
impl<'a, T: Debug> ViaDebug<T> for &ArgPrinter<'a, T> {
fn debug_string(&self) -> DebugPrint<'a, T> {
DebugPrint(self.0)
}
}

#[doc(hidden)]
pub struct NothingPrint;
impl Debug for NothingPrint {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "?")
}
}
#[doc(hidden)]
pub trait ViaNothing { fn debug_string(&self) -> NothingPrint; }
impl<'a, T> ViaNothing for ArgPrinter<'a, T> {
fn debug_string(&self) -> NothingPrint {
NothingPrint
}
}

Expand Down
7 changes: 1 addition & 6 deletions mockall/tests/automock_foreign_c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,7 @@ pub fn normal_usage() {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "mock_ffi::foo1(5): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "mock_ffi::foo1(?): No matching expectation found"
))]
#[should_panic(expected = "mock_ffi::foo1(5): No matching expectation found")]
fn with_no_matches() {
let ctx = mock_ffi::foo1_context();
ctx.expect()
Expand Down
7 changes: 1 addition & 6 deletions mockall/tests/automock_foreign_extern.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,7 @@ extern "C" {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "mock_ffi::foo1(5): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "mock_ffi::foo1(?): No matching expectation found"
))]
#[should_panic(expected = "mock_ffi::foo1(5): No matching expectation found")]
fn with_no_matches() {
let ctx = mock_ffi::foo1_context();
ctx.expect()
Expand Down
7 changes: 1 addition & 6 deletions mockall/tests/automock_module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@ pub mod m {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "mock_foo::bar1(5): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "mock_foo::bar1(?): No matching expectation found"
))]
#[should_panic(expected = "mock_foo::bar1(5): No matching expectation found")]
fn with_no_matches() {
let ctx = mock_foo::bar1_context();
ctx.expect()
Expand Down
7 changes: 1 addition & 6 deletions mockall/tests/automock_nondebug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ pub trait Foo {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::foo(?): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::foo(?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::foo(?): No matching expectation found")]
fn with_no_matches() {
let mock = MockFoo::new();
mock.foo(NonDebug(5));
Expand Down
7 changes: 1 addition & 6 deletions mockall/tests/automock_slice_arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,7 @@ mod withf {
use super::*;

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::foo([1, 2, 3, 4]): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::foo(?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::foo([1, 2, 3, 4]): No matching expectation found")]
fn fail() {
let mut mock = MockFoo::new();
mock.expect_foo()
Expand Down
19 changes: 12 additions & 7 deletions mockall/tests/mock_generic_arguments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use mockall::*;

mock! {
Foo {
fn foo<T: 'static>(&self, t: T) -> i32;
fn foo<T: 'static + std::fmt::Debug>(&self, t: T) -> i32;
fn bar<T: 'static>(&self, t: T) -> i32;
}
}
Expand Down Expand Up @@ -47,17 +47,22 @@ mod with {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::foo(4): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::foo(?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::foo(4): No matching expectation found")]
fn wrong_generic_type() {
let mut mock = MockFoo::new();
mock.expect_foo::<i16>()
.with(predicate::eq(4))
.return_const(0);
mock.foo(4i32);
}

#[test]
#[should_panic(expected = "MockFoo::bar(?): No matching expectation found")]
fn no_debug_trait_bound() {
let mut mock = MockFoo::new();
mock.expect_bar::<i16>()
.with(predicate::eq(4))
.return_const(0);
mock.bar(4i32);
}
}
15 changes: 5 additions & 10 deletions mockall/tests/mock_generic_struct_with_generic_static_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ use mockall::*;
use std::sync::Mutex;

mock! {
Foo<T: 'static> {
fn foo<Q: 'static>(t: T, q: Q) -> u64;
Foo<T: 'static + std::fmt::Debug> {
fn foo<Q: 'static + std::fmt::Debug>(t: T, q: Q) -> u64;
// We must use a different method for every should_panic test, so the
// shared mutex doesn't get poisoned.
fn foo2<Q: 'static>(t: T, q: Q) -> u64;
fn foo3<Q: 'static>(t: T, q: Q) -> u64;
fn foo2<Q: 'static + std::fmt::Debug>(t: T, q: Q) -> u64;
fn foo3<Q: 'static + std::fmt::Debug>(t: T, q: Q) -> u64;
}
}

Expand Down Expand Up @@ -48,12 +48,7 @@ fn ctx_checkpoint() {

// Expectations should be cleared when a context object drops
#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::foo3(42, 69): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::foo3(?, ?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::foo3(42, 69): No matching expectation found")]
fn ctx_hygiene() {
{
let ctx0 = MockFoo::<u32>::foo3_context();
Expand Down
5 changes: 1 addition & 4 deletions mockall/tests/mock_return_mutable_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,7 @@ mod sequence {
use super::*;

#[test]
#[cfg_attr(feature = "nightly",
should_panic(expected = "MockFoo::foo(4): Method sequence violation"))]
#[cfg_attr(not(feature = "nightly"),
should_panic(expected = "MockFoo::foo(?): Method sequence violation"))]
#[should_panic(expected = "MockFoo::foo(4): Method sequence violation")]
fn fail() {
let mut seq = Sequence::new();
let mut mock = MockFoo::new();
Expand Down
5 changes: 1 addition & 4 deletions mockall/tests/mock_return_reference.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,7 @@ mod sequence {
}

#[test]
#[cfg_attr(feature = "nightly",
should_panic(expected = "MockFoo::foo(4): Method sequence violation"))]
#[cfg_attr(not(feature = "nightly"),
should_panic(expected = "MockFoo::foo(?): Method sequence violation"))]
#[should_panic(expected = "MockFoo::foo(4): Method sequence violation")]
fn fail() {
let mut seq = Sequence::new();
let mut mock = MockFoo::new();
Expand Down
21 changes: 3 additions & 18 deletions mockall/tests/mock_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,7 @@ mod checkpoint {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::foo(0): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::foo(?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::foo(0): No matching expectation found")]
fn removes_old_expectations() {
let mut mock = MockFoo::new();
mock.expect_foo()
Expand Down Expand Up @@ -123,12 +118,7 @@ mod r#match {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::bar(5): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::bar(?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::bar(5): No matching expectation found")]
fn with_no_matches() {
let mut mock = MockFoo::new();
mock.expect_bar()
Expand Down Expand Up @@ -156,12 +146,7 @@ mod r#match {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::bar(5): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::bar(?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::bar(5): No matching expectation found")]
fn withf_no_matches() {
let mut mock = MockFoo::new();
mock.expect_bar()
Expand Down
7 changes: 1 addition & 6 deletions mockall/tests/mock_struct_with_static_method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,7 @@ fn ctx_checkpoint() {

// Expectations should be cleared when a context object drops
#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::bar3(42): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::bar3(?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::bar3(42): No matching expectation found")]
fn ctx_hygiene() {
{
let ctx0 = MockFoo::bar3_context();
Expand Down
7 changes: 1 addition & 6 deletions mockall/tests/mock_unsized.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,7 @@ fn with_eq() {
}

#[test]
#[cfg_attr(feature = "nightly", should_panic(
expected = "MockFoo::foo(\"xxx\"): No matching expectation found"
))]
#[cfg_attr(not(feature = "nightly"), should_panic(
expected = "MockFoo::foo(?): No matching expectation found"
))]
#[should_panic(expected = "MockFoo::foo(\"xxx\"): No matching expectation found")]
fn with_never() {
let mut foo = MockFoo::new();
foo.expect_foo()
Expand Down
7 changes: 6 additions & 1 deletion mockall_derive/src/mock_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@ impl MockFunction {
#(#attrs)*
#dead_code
#vis #sig {
use ::mockall::{ViaDebug, ViaNothing};
let no_match_msg = #no_match_msg;
#deref {
let __mockall_guard = #outer_mod_path::EXPECTATIONS
Expand All @@ -532,6 +533,7 @@ impl MockFunction {
#(#attrs)*
#dead_code
#vis #sig {
use ::mockall::{ViaDebug, ViaNothing};
let no_match_msg = #no_match_msg;
#deref self.#substruct_obj #name.#call #tbf(#(#call_exprs,)*)
.expect(&no_match_msg)
Expand Down Expand Up @@ -602,7 +604,7 @@ impl MockFunction {
};
let fields = vec!["{:?}"; argnames.len()].join(", ");
let fstr = format!("{name}({fields})");
quote!(std::format!(#fstr, #(::mockall::MaybeDebugger(&#argnames)),*))
quote!(std::format!(#fstr, #((&&::mockall::ArgPrinter(&#argnames)).debug_string()),*))
}

/// Generate code for the expect_ method
Expand Down Expand Up @@ -1952,6 +1954,7 @@ impl<'a> ToTokens for RefExpectation<'a> {
/// Call this [`Expectation`] as if it were the real method.
#v fn call #lg (&self, #(#argnames: #argty, )*) -> #output
{
use ::mockall::{ViaDebug, ViaNothing};
self.common.call(&#desc);
self.rfunc.call().unwrap_or_else(|m| {
let desc = std::format!(
Expand Down Expand Up @@ -2016,6 +2019,7 @@ impl<'a> ToTokens for RefMutExpectation<'a> {
#v fn call_mut #lg (&mut self, #(#argnames: #argty, )*)
-> &mut #owned_output
{
use ::mockall::{ViaDebug, ViaNothing};
self.common.call(&#desc);
let desc = std::format!(
"{}", self.common.matcher.lock().unwrap());
Expand Down Expand Up @@ -2104,6 +2108,7 @@ impl<'a> ToTokens for StaticExpectation<'a> {
#[doc(hidden)]
#v fn call #lg (&self, #(#argnames: #argty, )* ) -> #output
{
use ::mockall::{ViaDebug, ViaNothing};
self.common.call(&#desc);
self.rfunc.lock().unwrap().call_mut(#(#argnames, )*)
.unwrap_or_else(|message| {
Expand Down

0 comments on commit 582640c

Please sign in to comment.