Skip to content

Commit

Permalink
Eliminate the generic static methods hack
Browse files Browse the repository at this point in the history
It is no longer necessary to duplicate the struct's generic parameters
in the mock method's definition.
  • Loading branch information
asomers committed Aug 7, 2019
1 parent 2ae168d commit 3b65da0
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 49 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ This project adheres to [Semantic Versioning](http://semver.org/).
## [Unreleased] - ReleaseDate
### Added

- Static methods of generic structs (and traits) can now be mocked without the
hack of duplicating the struct's (or trait's) generic parameters.
([#18](https://github.com/asomers/mockall/pull/18))

- Methods with closure arguments can now be mocked. Technically they always
could be, but until now it wasn't possible to call the closure argument from
`withf` or `returning`. No special tricks are required by the user.
Expand Down
29 changes: 7 additions & 22 deletions mockall/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -679,18 +679,19 @@
//!
//! Without trait specialization, it probably won't be possible for Mockall
//! to support all specializing methods. However, most specializing methods can
//! be mocked as generic methods with repeated type parameters, just like static
//! methods of generic structs.
//! be mocked as generic methods with repeated type parameters.
//!
//! ```
//! # use mockall::*;
//! // A struct like this
//! struct Foo<T>(T);
//! impl<T> Foo<T> {
//! fn foo(&self, t: T) where T: Copy {
//! // ...
//! # unimplemented!()
//! }
//! }
//! // Can be mocked like this
//! mock! {
//! Foo<T: 'static> {
//! fn foo<T2: Copy + 'static>(&self, t: T2);
Expand Down Expand Up @@ -841,35 +842,19 @@
//!
//! ### Generic static methods
//!
//! Mocking static methods of generic structs is a little bit tricky. If the
//! static method uses any generic parameters, then those generic parameters
//! must be duplicated as generic parameters of the static method itself.
//! Here's an example:
//! Mocking static methods of generic structs or traits, whether or not the
//! methods themselves are generic, should work seamlessly.
//!
//! ```
//! # use mockall::*;
//! // A struct like this:
//! struct Foo<T> {
//! // ...
//! # _t0: std::marker::PhantomData<T>
//! }
//! impl<T> Foo<T> {
//! fn new(t: T) -> Self {
//! // ...
//! # unimplemented!()
//! }
//! }
//!
//! // Can be mocked like this:
//! mock! {
//! Foo<T: 'static> {
//! fn new<T2: 'static>(t: T2) -> MockFoo<T2>;
//! fn new(t: T) -> MockFoo<T>;
//! }
//! }
//!
//! // And used like this:
//! # fn main() {
//! MockFoo::<u32>::expect_new::<u32>()
//! MockFoo::<u32>::expect_new()
//! .returning(|_| MockFoo::default());
//! let mock = MockFoo::<u32>::new(42u32);
//! # }
Expand Down
18 changes: 18 additions & 0 deletions mockall/tests/automock_generic_struct_with_static_method.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// vim: tw=80
//! static non-generic methods of generic structs shouldn't require any special
//! treatment when mocking. Prior to version 0.3.0, the struct's generic
//! parameters had to be duplicated as generic parameters of the method.
use mockall::*;

#[automock]
trait Foo<T: 'static> {
fn foo(t: T);
}

#[test]
fn returning() {
MockFoo::<u32>::expect_foo()
.returning(|_| ());
MockFoo::foo(42u32);
}
4 changes: 2 additions & 2 deletions mockall/tests/mock_generic_constructor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ use mockall::*;

mock! {
pub Foo<T: Default +'static> {
fn build<T2: Default + 'static>() -> MockFoo<T2>;
fn build() -> MockFoo<T>;
}
}

#[test]
fn returning_once() {
MockFoo::<i16>::expect_build::<i16>()
MockFoo::<i16>::expect_build()
.return_once(MockFoo::<i16>::default);

let _mock: MockFoo<i16> = MockFoo::<i16>::build();
Expand Down
4 changes: 2 additions & 2 deletions mockall/tests/mock_generic_constructor_with_where_clause.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@ use mockall::*;

mock! {
pub Foo<T> where T: Default + 'static {
fn build<T2>() -> MockFoo<T2> where T2: Default + 'static;
fn build() -> MockFoo<T>;
}
}

#[test]
fn returning_once() {
MockFoo::<i16>::expect_build::<i16>()
MockFoo::<i16>::expect_build()
.return_once(MockFoo::<i16>::default);

let _mock: MockFoo<i16> = MockFoo::<i16>::build();
Expand Down
18 changes: 18 additions & 0 deletions mockall/tests/mock_generic_struct_with_generic_static_method.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// vim: ts=80
//! A generic struct with a generic method on a different parameter
use mockall::*;

mock! {
Foo<T: 'static> {
fn foo<Q: 'static>(t: T, q: Q);
}
}

#[test]
fn with() {
MockFoo::<u32>::expect_foo::<i16>()
.with(predicate::eq(42u32), predicate::eq(99i16))
.returning(|_, _| ());
MockFoo::foo(42u32, 99i16);
}
20 changes: 7 additions & 13 deletions mockall/tests/mock_generic_struct_with_static_method.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
// vim: ts=80
// vim: tw=80
//! static non-generic methods of generic structs shouldn't require any special
//! treatment when mocking. Prior to version 0.3.0, the struct's generic
//! parameters had to be duplicated as generic parameters of the method.
use mockall::*;

// Static methods parameterized on the struct's generic parameter need to be
// turned into generic methods for mocking. A struct like this:
//
// struct Foo<T> {}
// impl<T> Foo<T> {
// fn foo(t: T) {...}
// }
//
// Can be mocked like this:
mock! {
Foo<T: 'static> {
fn foo<T2: 'static>(t: T2);
fn foo(t: T);
}
}

#[test]
fn returning() {
MockFoo::<u32>::expect_foo::<u32>()
MockFoo::<u32>::expect_foo()
.returning(|_| ());
MockFoo::<u32>::foo(42u32);
MockFoo::foo(42u32);
}
19 changes: 13 additions & 6 deletions mockall_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ use crate::mock::{Mock, do_mock};

struct MethodTypes {
is_static: bool,
/// Is the Expectation type generic? This can be true even if the method is
/// not generic
is_expectation_generic: bool,
/// Type of Expectation returned by the expect method
expectation: Type,
/// Generics applicable to the Expectation object
Expand Down Expand Up @@ -396,6 +399,8 @@ fn gen_mod_ident(struct_: &Ident, trait_: Option<&Ident>)
/// Combine two Generics structs, producing a new one that has the union of
/// their parameters.
fn merge_generics(x: &Generics, y: &Generics) -> Generics {
//dbg!(x);
//dbg!(y);
/// Compare only the identifiers of two GenericParams
fn cmp_gp_idents(x: &GenericParam, y: &GenericParam) -> bool {
use GenericParam::*;
Expand Down Expand Up @@ -547,7 +552,6 @@ fn method_types(sig: &MethodSig, generics: Option<&Generics>)
let ident = &sig.ident;
let (expectation_generics, expectation_inputs, call_exprs) =
declosurefy(&sig.decl.generics, &sig.decl.inputs);
let is_generic = !sig.decl.generics.params.is_empty();
let merged_g = if let Some(g) = generics {
merge_generics(&g, &expectation_generics)
} else {
Expand Down Expand Up @@ -620,16 +624,19 @@ fn method_types(sig: &MethodSig, generics: Option<&Generics>)
}
};

let is_expectation_generic = !sig.decl.generics.params.is_empty() ||
(is_static && generics.filter(|g| !g.params.is_empty()).is_some());

let expectation_ident = Ident::new("Expectation", span);
let expectations_ident = if is_generic {
let expectations_ident = if is_expectation_generic {
Ident::new("GenericExpectations", span)
} else {
Ident::new("Expectations", span)
};
let expectations = parse2(
quote!(#ident::#expectations_ident)
).unwrap();
let expect_obj = if is_generic {
let expect_obj = if is_expectation_generic {
parse2(quote!(#expectations)).unwrap()
} else {
parse2(quote!(#expectations #tg)).unwrap()
Expand All @@ -639,9 +646,9 @@ fn method_types(sig: &MethodSig, generics: Option<&Generics>)
let mut output = sig.decl.output.clone();
deimplify(&mut output);

MethodTypes{is_static, expectation, expectation_generics,
expectation_inputs, expectations, call, expect_obj, call_exprs,
inputs, output, altargs, matchexprs}
MethodTypes{is_static, is_expectation_generic, expectation,
expectation_generics, expectation_inputs, expectations, call,
expect_obj, call_exprs, inputs, output, altargs, matchexprs}
}

/// Manually mock a structure.
Expand Down
10 changes: 6 additions & 4 deletions mockall_derive/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -270,9 +270,7 @@ fn gen_mock_method(mod_ident: Option<&syn::Ident>,
}

let (ig, ex_tg, wc) = meth_types.expectation_generics.split_for_impl();
let is_generic_method = !meth_types.expectation_generics.params.is_empty();
//let tg = ex_tg;
let tg = if meth_types.is_static || is_generic_method {
let tg = if meth_types.is_static || meth_types.is_expectation_generic {
// For generic and static methods only, the trait's generic parameters
// become generic parameters of the method.
merged_g.split_for_impl().1
Expand Down Expand Up @@ -309,10 +307,14 @@ fn gen_mock_method(mod_ident: Option<&syn::Ident>,
let ltd = syn::LifetimeDef::new(lt);
let mut g = meth_types.expectation_generics.clone();
g.params.push(syn::GenericParam::Lifetime(ltd.clone()));
if g.lt_token.is_none() {
g.lt_token = Some(Token![<](Span::call_site()));
g.gt_token = Some(Token![>](Span::call_site()));
}
let merged_g = merge_generics(&generics, &g);
let (ig, _, _) = g.split_for_impl();
let (_, tg, _) = merged_g.split_for_impl();
let guard_name = if sig.decl.generics.params.is_empty() {
let guard_name = if !meth_types.is_expectation_generic {
syn::Ident::new("ExpectationGuard", Span::call_site())
} else {
syn::Ident::new("GenericExpectationGuard", Span::call_site())
Expand Down

0 comments on commit 3b65da0

Please sign in to comment.