Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Bring back AnswerFn with support for parameter borrows #47

Merged
merged 1 commit into from
Mar 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Unimock now supports very flexible argument mutation, instead of one hard-coded parameter.
A new `applies()` function-responder has been added, in favor of the old `answers()`, `answers_leaked_ref()`, `mutates()`.
The function passed to `applies()` API can mutate all its inputs freely.
The downside to this new mechanism is that its return type can't be generic (i.e. `Ret: IntoResponse`).
Flexible return types are still supported though, but now a response has to be created explicitly calling `unimock::respond(return_value)`. ([#43](https://github.com/audunhalland/unimock/pull/43))
To achieve this, the `answers` API had to be redesigned with a new signature based on a `dyn Fn`.
This dynamic function type has one fixed signature per `MockFn`, so its return type isn't generic as it used to be in `0.5.x`.
All generated borrows have to be done explicitly through `Unimock::make_ref` for this to work. ([#43](https://github.com/audunhalland/unimock/pull/43), [#47](https://github.com/audunhalland/unimock/pull/47))
- The function passed to `answers` must be a `&static` Fn, _or_ it can be an `Arc` closure that can be registered by calling `answers_arc`.
- The parameters passed to this function are the same as passed to the mocked trait method, including `self`.
- Output trait hierarchy (which allows safely mocking borrowed return values) rewritten to be more flexible and future-proof than previously ([#46](https://github.com/audunhalland/unimock/pull/46))
- `default_implementation` renamed to `applies_default_impl`.
### Added
- Mocks for `tokio-1` and `futures-0-3` async read/write traits ([#45](https://github.com/audunhalland/unimock/pull/45))
### Fixed
- Fix `matching!` against references to number literals ([#42](https://github.com/audunhalland/unimock/pull/42))
- Borrows from function arguments can now be made without leaking memory ([#47](https://github.com/audunhalland/unimock/pull/47))
### Removed
- The `mutates` builder APIs. These are now handled using `answers`.

## [0.5.8] - 2024-01-15
### Fixed
Expand Down
16 changes: 6 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,22 +105,18 @@ There are different constraints acting on return values based on how the clause
### Mutating inputs
Many traits uses the argument mutation pattern, where there are one or more `&mut` parameters.

To access the `&mut` parameters, a function is applied to the call pattern using `applies`:
To access the `&mut` parameters, a function is applied to the call pattern using `answers`:

```rust
let mocked = Unimock::new(
mock::core::fmt::DisplayMock::fmt
.next_call(matching!(_))
.applies(&|f| respond(write!(f, "mutation!")))
.answers(&|_, f| write!(f, "mutation!"))
);

assert_eq!("mutation!", format!("{mocked}"));
```

The applied function also specifies the response, which is constructed by calling [respond].
In the `fmt` case, the return type is [core::fmt::Result].
The respond function helps with automatic type conversion and automatic borrowing.

## Combining setup clauses
`Unimock::new()` accepts as argument anything that implements [Clause].
Basic setup clauses can be combined into composite clauses by using _tuples_:
Expand All @@ -146,10 +142,10 @@ assert_eq!(
&Unimock::new((
FooMock::foo
.some_call(matching!(_))
.applies(&|arg| respond(arg * 3)),
.answers(&|_, arg| arg * 3),
BarMock::bar
.some_call(matching!((arg) if *arg > 20))
.applies(&|arg| respond(arg * 2)),
.answers(&|_, arg| arg * 2),
)),
7
)
Expand All @@ -163,10 +159,10 @@ assert_eq!(
&Unimock::new((
FooMock::foo.stub(|each| {
each.call(matching!(1337)).returns(1024);
each.call(matching!(_)).applies(&|arg| respond(arg * 3));
each.call(matching!(_)).answers(&|_, arg| arg * 3);
}),
BarMock::bar.stub(|each| {
each.call(matching!((arg) if *arg > 20)).applies(&|arg| respond(arg * 2));
each.call(matching!((arg) if *arg > 20)).answers(&|_, arg| arg * 2);
}),
)),
7
Expand Down
73 changes: 59 additions & 14 deletions src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ use crate::alloc::{String, ToString, Vec};
use crate::call_pattern::*;
use crate::fn_mocker::PatternMatchMode;
use crate::output::{IntoReturn, IntoReturnOnce, Return, ReturnDefault};
use crate::private::{AnswerClosure, AnswerClosureInner};
use crate::property::*;
use crate::responder::{Applier, BoxedApplier, DynResponder, IntoReturner};
use crate::responder::{Answerer, DynResponder, IntoReturner};
use crate::*;
use dyn_builder::*;

Expand Down Expand Up @@ -327,10 +328,12 @@ assert_eq!(None, u.get());
self.quantify()
}


/// Specify the response of the call pattern by applying the given function that can then compute it based on input parameters.
///
/// The applied function can also respond with types that don't implement [Send] and [Sync].
/// The applied function can respond with types that don't implement [Send] and [Sync].
///
/// The function signature has the same signature as the trait function that it mocks, including `self`.
/// The `self` argument is included because of a potential need for calling .e.g. [`make_ref`](crate::Unimock::make_ref) on it.
///
/// # Example
#[doc = concat!("\
Expand All @@ -346,7 +349,7 @@ trait Trait {
let u = Unimock::new(
TraitMock::get_cell
.next_call(matching!(84))
.applies(&|input| respond(Cell::new(input / 2)))
.answers(&|_, input| Cell::new(input / 2))
);

assert_eq!(Cell::new(42), u.get_cell(84));
Expand All @@ -367,9 +370,8 @@ trait Trait {
let u = Unimock::new(
TraitMock::mutate
.next_call(matching!(41))
.applies(&|input| {
.answers(&|_, input| {
*input += 1;
respond(())
})
);

Expand All @@ -379,9 +381,52 @@ assert_eq!(number, 42);
```
",
)]
pub fn applies(mut self, apply_fn: &'static F::ApplyFn) -> Quantify<'p, F, O> {
/// # Borrow from self
#[doc = concat!("\
```
# use unimock::*;
#[unimock(api=TraitMock)]
trait Trait {
fn get(&self) -> &u32;
}

let u = Unimock::new(
TraitMock::get
.next_call(matching!())
.answers(&|u| u.make_ref(42))
);

assert_eq!(u.get(), &42);
```
",
)]
/// # Recursion
#[doc = concat!("\
```
# use unimock::*;
#[unimock(api=FibMock)]
trait Fib {
fn fib(&self, input: i32) -> i32;
}

let u = Unimock::new((
FibMock::fib
.each_call(matching!(1 | 2))
.returns(1),
FibMock::fib
.each_call(matching!(_))
.answers(&|u, input| {
u.fib(input - 1) + u.fib(input - 2)
})
));

assert_eq!(55, u.fib(10));
```
",
)]
pub fn answers(mut self, answer_fn: &'static F::AnswerFn) -> Quantify<'p, F, O> {
self.wrapper
.push_responder(Applier::<F> { apply_fn }.into_dyn_responder());
.push_responder(Answerer::<F> { answer_closure: AnswerClosure(AnswerClosureInner::Ref(answer_fn)) }.into_dyn_responder());
self.quantify()
}

Expand All @@ -401,10 +446,10 @@ let mutex: Arc<Mutex<i32>> = Arc::new(Mutex::new(0));
let u = Unimock::new(
TraitMock::get
.each_call(matching!())
.applies_closure({
.answers_arc({
let mutex = mutex.clone();
Box::new(move || {
respond(*mutex.lock().unwrap())
Arc::new(move |_| {
*mutex.lock().unwrap()
})
})
);
Expand All @@ -415,12 +460,12 @@ assert_eq!(42, u.get());
```
",
)]
pub fn applies_closure(
pub fn answers_arc(
mut self,
apply_fn: crate::alloc::Box<F::ApplyFn>,
answer_fn: crate::alloc::Arc<F::AnswerFn>,
) -> Quantify<'p, F, O> {
self.wrapper
.push_responder(BoxedApplier::<F> { apply_fn }.into_dyn_responder());
.push_responder(Answerer::<F> { answer_closure: AnswerClosure(AnswerClosureInner::Arc(answer_fn)) }.into_dyn_responder());
self.quantify()
}

Expand Down
6 changes: 3 additions & 3 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub(crate) enum MockError {
NoDefaultImpl {
info: MockFnInfo,
},
NotApplied {
NotAnswered {
info: MockFnInfo,
},
ExplicitPanic {
Expand Down Expand Up @@ -130,10 +130,10 @@ impl core::fmt::Display for MockError {
path = info.path
)
}
Self::NotApplied { info } => {
Self::NotAnswered { info } => {
write!(
f,
"{path} did not apply the function, this is a bug.",
"{path} did not apply the answer function, this is a bug.",
path = info.path
)
}
Expand Down
38 changes: 13 additions & 25 deletions src/eval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::error::{self};
use crate::error::{MockError, MockResult};
use crate::fn_mocker::{FnMocker, PatternMatchMode};
use crate::mismatch::Mismatches;
use crate::private::{ApplyClosure, Evaluation, MismatchReporter};
use crate::private::{Continuation, Eval, MismatchReporter};
use crate::responder::{DowncastResponder, DynResponder};
use crate::state::SharedState;
use crate::{debug, MockFnInfo, Unimock};
Expand All @@ -25,7 +25,7 @@ struct EvalResponder<'u> {
pub(crate) fn eval<'u, 'i, F: MockFn>(
unimock: &'u Unimock,
inputs: F::Inputs<'i>,
) -> MockResult<Evaluation<'u, 'i, F>> {
) -> MockResult<Eval<'u, 'i, F>> {
let dyn_ctx = DynCtx {
info: F::info(),
shared_state: &unimock.shared_state,
Expand All @@ -39,7 +39,7 @@ pub(crate) fn eval<'u, 'i, F: MockFn>(
.downcast_responder::<F, _>(dyn_return_responder, &eval_responder)?
.get_output()
{
Some(output) => Ok(Evaluation::Evaluated(output)),
Some(output) => Ok(Eval::Return(output)),
None => Err(MockError::CannotReturnValueMoreThanOnce {
fn_call: dyn_ctx.fn_call(),
pattern: eval_responder
Expand All @@ -48,25 +48,11 @@ pub(crate) fn eval<'u, 'i, F: MockFn>(
}),
}
}
DynResponder::StaticApply(dyn_responder) => {
let apply_fn_responder =
DynResponder::Answer(dyn_responder) => {
let answerer =
dyn_ctx.downcast_responder::<F, _>(dyn_responder, &eval_responder)?;
Ok(Evaluation::Apply(
ApplyClosure {
unimock,
apply_fn: apply_fn_responder.apply_fn,
},
inputs,
))
}
DynResponder::BoxedApply(dyn_responder) => {
let apply_fn_responder =
dyn_ctx.downcast_responder::<F, _>(dyn_responder, &eval_responder)?;
Ok(Evaluation::Apply(
ApplyClosure {
unimock,
apply_fn: apply_fn_responder.apply_fn.as_ref(),
},
Ok(Eval::Continue(
Continuation::Answer(answerer.answer_closure.clone()),
inputs,
))
}
Expand All @@ -77,11 +63,13 @@ pub(crate) fn eval<'u, 'i, F: MockFn>(
.debug_pattern(eval_responder.pat_index),
msg: msg.clone(),
}),
DynResponder::Unmock => Ok(Evaluation::Unmocked(inputs)),
DynResponder::ApplyDefaultImpl => Ok(Evaluation::CallDefaultImpl(inputs)),
DynResponder::Unmock => Ok(Eval::Continue(Continuation::Unmock, inputs)),
DynResponder::ApplyDefaultImpl => {
Ok(Eval::Continue(Continuation::CallDefaultImpl, inputs))
}
},
EvalResult::Unmock => Ok(Evaluation::Unmocked(inputs)),
EvalResult::CallDefaultImpl => Ok(Evaluation::CallDefaultImpl(inputs)),
EvalResult::Unmock => Ok(Eval::Continue(Continuation::Unmock, inputs)),
EvalResult::CallDefaultImpl => Ok(Eval::Continue(Continuation::CallDefaultImpl, inputs)),
}
}

Expand Down
Loading
Loading