diff --git a/src/answer.rs b/src/answer.rs new file mode 100644 index 0000000..5946e61 --- /dev/null +++ b/src/answer.rs @@ -0,0 +1,76 @@ +use core::ops::Deref; + +use crate::{ + output::{Output, Respond}, + MockFn, Unimock, +}; + +/// A borrowed Answering function. +pub struct AnswerFn<'u, F: MockFn> { + pub(crate) answer_fn: &'u F::AnswerFn, +} + +impl<'u, F: MockFn> Deref for AnswerFn<'u, F> { + type Target = F::AnswerFn; + + fn deref(&self) -> &Self::Target { + self.answer_fn + } +} + +impl<'u, F: MockFn> AnswerFn<'u, F> { + /// Convert the answering function's response to a mock output. + pub fn make_output( + &self, + response: ::Type, + unimock: &'u Unimock, + ) -> as Output<'u, F::Response>>::Type { + as Output<'u, F::Response>>::from_response(response, &unimock.value_chain) + } +} + +/// Trait for converting a closure into [MockFn::AnswerFn] +pub trait IntoAnswerFn { + /// The resulting AnswerFn, some `dyn Fn`. + type AnswerFn: ?Sized + Send + Sync; + + /// Performs the conversion. + fn into_answer_fn(self) -> Box; +} + +impl IntoAnswerFn<()> for F +where + F: Fn() -> O + Send + Sync + 'static, +{ + type AnswerFn = dyn (Fn() -> O) + Send + Sync; + + fn into_answer_fn(self) -> Box { + Box::new(self) + } +} + +macro_rules! arg_tuples { + ($($a:ident),+) => { + impl IntoAnswerFn<($($a),+,)> for F + where + F: Fn($($a),+) -> O + Send + Sync + 'static, + { + type AnswerFn = dyn (Fn($($a),+) -> O) + Send + Sync; + + fn into_answer_fn(self) -> Box { + Box::new(self) + } + } + }; +} + +arg_tuples!(A0); +arg_tuples!(A0, A1); +arg_tuples!(A0, A1, A2); +arg_tuples!(A0, A1, A2, A3); +arg_tuples!(A0, A1, A2, A3, A4); +arg_tuples!(A0, A1, A2, A3, A4, A5); +arg_tuples!(A0, A1, A2, A3, A4, A5, A6); +arg_tuples!(A0, A1, A2, A3, A4, A5, A6, A7); +arg_tuples!(A0, A1, A2, A3, A4, A5, A6, A7, A8); +arg_tuples!(A0, A1, A2, A3, A4, A5, A6, A7, A8, A9); diff --git a/src/build.rs b/src/build.rs index 6e62861..0d9f2a4 100644 --- a/src/build.rs +++ b/src/build.rs @@ -1,5 +1,6 @@ use core::marker::PhantomData; +use crate::answer::IntoAnswerFn; use crate::call_pattern::*; use crate::clause::{self}; use crate::fn_mocker::PatternMatchMode; @@ -278,6 +279,26 @@ macro_rules! define_response_common_impl { self.quantify() } + /// Specify the response of the call pattern by invoking the given closure that can then compute it based on input parameters. + pub fn answers_fn(mut self, answer_fn: C) -> Quantify<'p, F, O> + where + C: IntoAnswerFn, + { + self.wrapper.push_responder( + AnswerFnResponder:: { + answer_fn: answer_fn.into_answer_fn(), + } + .into_dyn_responder(), + ); + self.quantify() + } + + pub fn answers_fn_box(mut self, answer_fn: Box) -> Quantify<'p, F, O> { + self.wrapper + .push_responder(AnswerFnResponder:: { answer_fn }.into_dyn_responder()); + self.quantify() + } + /// Specify the response of the call pattern by invoking the given closure that can then compute it based on input parameters. /// /// This variant passes an [AnswerContext] as the second parameter. diff --git a/src/call_pattern.rs b/src/call_pattern.rs index 397e989..ccbd3d0 100644 --- a/src/call_pattern.rs +++ b/src/call_pattern.rs @@ -104,6 +104,7 @@ pub(crate) enum DynResponder { Cell(DynCellResponder), Borrow(DynBorrowResponder), Function(DynFunctionResponder), + AnswerFn(DynAnswerFnResponder), Panic(String), Unmock, CallDefaultImpl, @@ -170,6 +171,7 @@ impl DynResponder { pub(crate) struct DynCellResponder(AnyBox); pub(crate) struct DynBorrowResponder(AnyBox); pub(crate) struct DynFunctionResponder(AnyBox); +pub(crate) struct DynAnswerFnResponder(AnyBox); pub trait DowncastResponder { type Downcasted; @@ -201,6 +203,14 @@ impl DowncastResponder for DynFunctionResponder { } } +impl DowncastResponder for DynAnswerFnResponder { + type Downcasted = AnswerFnResponder; + + fn downcast(&self) -> PatternResult<&Self::Downcasted> { + downcast_box(&self.0) + } +} + pub(crate) struct CellResponder { pub cell: Box::Type>>, } @@ -218,6 +228,10 @@ pub(crate) struct FunctionResponder { >, } +pub(crate) struct AnswerFnResponder { + pub answer_fn: Box, +} + impl CellResponder { pub fn into_dyn_responder(self) -> DynResponder { DynResponder::Cell(DynCellResponder(Box::new(self))) @@ -239,6 +253,12 @@ impl FunctionResponder { } } +impl AnswerFnResponder { + pub fn into_dyn_responder(self) -> DynResponder { + DynResponder::AnswerFn(DynAnswerFnResponder(Box::new(self))) + } +} + fn find_responder_by_call_index( responders: &[DynCallOrderResponder], call_index: usize, diff --git a/src/error.rs b/src/error.rs index 9ab34e5..6893622 100644 --- a/src/error.rs +++ b/src/error.rs @@ -49,6 +49,9 @@ pub(crate) enum MockError { NoDefaultImpl { info: MockFnInfo, }, + NoAnswer { + info: MockFnInfo, + }, ExplicitPanic { fn_call: debug::FnActualCall, pattern: debug::CallPatternDebug, @@ -127,6 +130,13 @@ impl core::fmt::Display for MockError { path = info.path ) } + Self::NoAnswer { info } => { + write!( + f, + "{path} did not produce an answer, this is a bug.", + path = info.path + ) + } Self::ExplicitPanic { fn_call, pattern, diff --git a/src/eval.rs b/src/eval.rs index 301561b..f78fa66 100644 --- a/src/eval.rs +++ b/src/eval.rs @@ -1,3 +1,4 @@ +use crate::answer::AnswerFn; use crate::build::AnswerContext; use crate::call_pattern::{ CallPattern, DowncastResponder, DynResponder, PatIndex, PatternError, PatternResult, @@ -84,6 +85,16 @@ pub(crate) fn eval<'u, 'i, F: MockFn>( ); Ok(Evaluation::Evaluated(output)) } + DynResponder::AnswerFn(dyn_answer_fn_responder) => { + let answer_fn_responder = + dyn_ctx.downcast_responder::(dyn_answer_fn_responder, &eval_responder)?; + Ok(Evaluation::Answer( + AnswerFn { + answer_fn: answer_fn_responder.answer_fn.as_ref(), + }, + inputs, + )) + } DynResponder::Panic(msg) => Err(MockError::ExplicitPanic { fn_call: dyn_ctx.fn_call(), pattern: eval_responder diff --git a/src/lib.rs b/src/lib.rs index 47f6fe8..92a2c3c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -433,6 +433,8 @@ #[cfg(not(feature = "std"))] extern crate alloc; +/// Answer functions. +pub mod answer; /// Builder pattern types used for defining mocked behaviour. pub mod build; /// Function outputs. @@ -1016,6 +1018,9 @@ pub trait MockFn: Sized + 'static { /// For methods without any mutable parameters, this type should be `()`. type Mutation<'m>: ?Sized; + /// Experimental: answer Fn + type AnswerFn: ?Sized + Send + Sync; + /// A type that describes how the mocked function responds. /// /// The Respond trait describes a type used internally to store a response value. diff --git a/src/mock/std.rs b/src/mock/std.rs index aaaa200..bf9fd5e 100644 --- a/src/mock/std.rs +++ b/src/mock/std.rs @@ -75,7 +75,10 @@ pub mod process { /// Unimock mock API for [std::process::Termination]. #[allow(non_snake_case)] pub mod TerminationMock { - use crate::{output::Owned, MockFn}; + use crate::{ + output::{Owned, Respond}, + MockFn, + }; #[allow(non_camel_case_types)] /// MockFn for [`Termination::report() -> ExitCode`](std::process::Termination::report). @@ -87,6 +90,7 @@ pub mod process { impl MockFn for report { type Inputs<'i> = (); type Mutation<'m> = (); + type AnswerFn = dyn Fn() -> ::Type + Send + Sync; type Response = Owned; type Output<'u> = Self::Response; diff --git a/src/private.rs b/src/private.rs index aa3c1fe..5a55d02 100644 --- a/src/private.rs +++ b/src/private.rs @@ -1,3 +1,4 @@ +use crate::answer::AnswerFn; use crate::call_pattern::InputIndex; use crate::debug; use crate::mismatch::{Mismatch, MismatchKind}; @@ -17,6 +18,8 @@ pub use crate::default_impl_delegator::*; pub enum Evaluation<'u, 'i, F: MockFn> { /// Function evaluated to its output. Evaluated( as Output<'u, F::Response>>::Type), + /// Function should be answered + Answer(AnswerFn<'u, F>, F::Inputs<'i>), /// Function not yet evaluated, should be unmocked. Unmocked(F::Inputs<'i>), /// Function not yet evaluated, should call default implementation. @@ -30,6 +33,7 @@ impl<'u, 'i, F: MockFn> Evaluation<'u, 'i, F> { pub fn unwrap(self, unimock: &Unimock) -> as Output<'u, F::Response>>::Type { let error = match self { Self::Evaluated(output) => return output, + Self::Answer(..) => error::MockError::NoAnswer { info: F::info() }, Self::Unmocked(_) => error::MockError::CannotUnmock { info: F::info() }, Self::CallDefaultImpl(_) => error::MockError::NoDefaultImpl { info: F::info() }, }; diff --git a/tests/it/basic.rs b/tests/it/basic.rs index 8f1dcfb..ec653fe 100644 --- a/tests/it/basic.rs +++ b/tests/it/basic.rs @@ -951,7 +951,7 @@ mod mutated_args { } } - struct LifetimeArg<'a> { + pub struct LifetimeArg<'a> { data: PhantomData<&'a ()>, } @@ -1110,3 +1110,28 @@ mod debug_mut_arg { fn f(&self, arg1: &mut Arg, arg2: &mut Arg); } } + +mod answer_fn { + use unimock::*; + + #[unimock(api = TraitMock)] + trait Trait { + fn foo(&self, a: &i32, b: &mut i32) -> i32; + } + + #[test] + fn test() { + let u = Unimock::new( + TraitMock::foo + .next_call(matching!(44, _)) + // .answers_fn(|a, b| 1337) + .answers_fn_box(Box::new(|_, b| { + *b += 1; + 1337 + })), + ); + let mut b = 0; + assert_eq!(1337, u.foo(&42, &mut b)); + assert_eq!(1, b); + } +} diff --git a/unimock_macros/src/unimock/method.rs b/unimock_macros/src/unimock/method.rs index 2361db2..1773494 100644 --- a/unimock_macros/src/unimock/method.rs +++ b/unimock_macros/src/unimock/method.rs @@ -337,7 +337,7 @@ pub fn extract_methods<'s>( ident: pat_ident.ident.clone(), ty: util::substitute_lifetimes( type_ref.elem.as_ref().clone(), - &syn::parse_quote!('m), + Some(&syn::parse_quote!('m)), ), }) } diff --git a/unimock_macros/src/unimock/mod.rs b/unimock_macros/src/unimock/mod.rs index ed41eb4..d32a2cd 100644 --- a/unimock_macros/src/unimock/mod.rs +++ b/unimock_macros/src/unimock/mod.rs @@ -1,5 +1,5 @@ use quote::{quote, quote_spanned, ToTokens}; -use syn::parse_quote; +use syn::{parse_quote, FnArg}; mod associated_future; mod attr; @@ -239,6 +239,20 @@ fn def_mock_fn( let input_lifetime = &attr.input_lifetime; let input_types_tuple = InputTypesTuple::new(method, trait_info, attr); + let answer_args = method + .adapted_sig + .inputs + .iter() + .filter_map(|input| match input { + FnArg::Receiver(_) => None, + FnArg::Typed(pat_type) => Some(pat_type.ty.as_ref().clone()), + }) + .map(|mut ty| { + ty = util::substitute_lifetimes(ty, None); + ty = util::self_type_to_unimock(ty, trait_info.input_trait, attr); + ty + }); + let mutation = if let Some(mutated_arg) = &method.mutated_arg { let ty = &mutated_arg.ty; quote! { #ty } @@ -280,6 +294,7 @@ fn def_mock_fn( impl #generic_params #prefix::MockFn for #mock_fn_path #generic_args #where_clause { type Inputs<#input_lifetime> = #input_types_tuple; type Mutation<'m> = #mutation; + type AnswerFn = dyn Fn(#(#answer_args),*) -> ::Type + Send + Sync; type Response = #response_associated_type; type Output<'u> = #output_associated_type; @@ -466,12 +481,27 @@ fn def_method_impl( match &receiver { Receiver::MutRef { .. } | Receiver::Pin { .. } => { - let default_impl_delegate_arm_polonius = if method.method.default.is_some() { - let eval_pattern = method.inputs_destructuring( - InputsSyntax::EvalPatternMutAsWildcard, - Tupled(true), + let eval_pattern = method.inputs_destructuring( + InputsSyntax::EvalPatternMutAsWildcard, + Tupled(true), + attr, + ); + + let answer_arm = { + let fn_params = method.inputs_destructuring( + InputsSyntax::FnParams, + Tupled(false), attr, ); + quote! { + #prefix::private::Evaluation::Answer(__answer_fn, #eval_pattern) => { + #prefix::polonius::_return!( + __answer_fn.make_output(__answer_fn(#fn_params), #self_ref) + ) + } + } + }; + let default_impl_delegate_arm_polonius = if method.method.default.is_some() { let args = method.inputs_destructuring(InputsSyntax::FnParams, Tupled(true), attr); Some(quote! { @@ -486,13 +516,14 @@ fn def_method_impl( let polonius_return_type: syn::Type = match method.method.sig.output.clone() { syn::ReturnType::Default => syn::parse_quote!(()), syn::ReturnType::Type(_arrow, ty) => { - util::substitute_lifetimes(*ty, &syn::parse_quote!('polonius)) + util::substitute_lifetimes(*ty, Some(&syn::parse_quote!('polonius))) } }; let polonius = quote_spanned! { span=> #prefix::polonius::_polonius!(|#self_ref| -> #polonius_return_type { match #prefix::private::eval::<#mock_fn_path #eval_generic_args>(#self_ref, #inputs_eval_params, #mutated_param) { + #answer_arm #unmock_arm #default_impl_delegate_arm_polonius e => #prefix::polonius::_return!(e.unwrap(#self_ref)) @@ -516,12 +547,25 @@ fn def_method_impl( } } _ => { - let default_impl_delegate_arm = if method.method.default.is_some() { - let eval_pattern = method.inputs_destructuring( - InputsSyntax::EvalPatternMutAsWildcard, - Tupled(true), + let eval_pattern = method.inputs_destructuring( + InputsSyntax::EvalPatternMutAsWildcard, + Tupled(true), + attr, + ); + + let answer_arm = { + let fn_params = method.inputs_destructuring( + InputsSyntax::FnParams, + Tupled(false), attr, ); + quote! { + #prefix::private::Evaluation::Answer(__answer_fn, #eval_pattern) => { + __answer_fn.make_output(__answer_fn(#fn_params), #self_ref) + } + } + }; + let default_impl_delegate_arm = if method.method.default.is_some() { Some(quote! { #prefix::private::Evaluation::CallDefaultImpl(#eval_pattern) => { #default_delegator_call @@ -533,6 +577,7 @@ fn def_method_impl( quote_spanned! { span=> match #prefix::private::eval::<#mock_fn_path #eval_generic_args>(#self_ref, #inputs_eval_params, #mutated_param) { + #answer_arm #unmock_arm #default_impl_delegate_arm e => e.unwrap(#self_ref) @@ -661,7 +706,7 @@ impl InputTypesTuple { }, ) .map(|mut ty| { - ty = util::substitute_lifetimes(ty, input_lifetime); + ty = util::substitute_lifetimes(ty, Some(input_lifetime)); ty = util::self_type_to_unimock(ty, trait_info.input_trait, attr); ty }) diff --git a/unimock_macros/src/unimock/util.rs b/unimock_macros/src/unimock/util.rs index ca70ae6..13c4638 100644 --- a/unimock_macros/src/unimock/util.rs +++ b/unimock_macros/src/unimock/util.rs @@ -270,19 +270,22 @@ impl<'t> quote::ToTokens for TypedPhantomData<'t> { } } -pub fn substitute_lifetimes(mut ty: syn::Type, lifetime: &syn::Lifetime) -> syn::Type { +pub fn substitute_lifetimes(mut ty: syn::Type, lifetime: Option<&syn::Lifetime>) -> syn::Type { struct LifetimeReplace<'s> { - lifetime: &'s syn::Lifetime, + lifetime: Option<&'s syn::Lifetime>, } impl<'s> syn::visit_mut::VisitMut for LifetimeReplace<'s> { fn visit_type_reference_mut(&mut self, reference: &mut syn::TypeReference) { - reference.lifetime = Some(self.lifetime.clone()); + reference.lifetime = self.lifetime.cloned(); syn::visit_mut::visit_type_reference_mut(self, reference); } fn visit_lifetime_mut(&mut self, lifetime: &mut syn::Lifetime) { - *lifetime = self.lifetime.clone(); + *lifetime = match self.lifetime { + Some(lt) => lt.clone(), + None => syn::Lifetime::new("'_", lifetime.span()), + }; syn::visit_mut::visit_lifetime_mut(self, lifetime); } }