Skip to content

Commit

Permalink
Support specific impls
Browse files Browse the repository at this point in the history
A specific impl is an implementation of a trait on a generic struct with
specific generic parameters, like `impl Foo for Bar<i32> {}`

Issue #271
  • Loading branch information
asomers committed Apr 24, 2021
1 parent 87a53c8 commit 0d9d633
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 28 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).

## [ Unreleased ] - ReleaseDate

### Added

- Added support for specific impls. A specific impl is an implementation of a
trait on a generic struct with specified generic parameters. For example,
`impl Foo for Bar<i32>` as opposed to `impl<T> Foo for Bar<T>`. Mockall does
not yet support generic methods in such traits.
([#274](https://github.com/asomers/mockall/pull/274))

### Changed

- Mockall is picker now about how you mock a trait on a generic struct.
Expand Down
49 changes: 49 additions & 0 deletions mockall/tests/specific_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// vim: ts=80
#![deny(warnings)]

use mockall::*;

trait Bar {
fn bar(&self);
}
//trait Baz {
//fn baz<Y: 'static>(&self, y: Y);
//}

mock! {
pub Foo<T: 'static> {
}
impl Bar for Foo<u32> {
fn bar(&self);
}
impl Bar for Foo<i32> {
fn bar(&self);
}
// TODO: support specific impls with generic methods, like this
//impl Baz for Foo<u32> {
//fn baz<Y: 'static>(&self, y: Y);
//}
}

#[test]
fn return_const() {
let mut mocku = MockFoo::<u32>::new();
mocku.expect_bar()
.return_const(());
let mut mocki = MockFoo::<i32>::new();
mocki.expect_bar()
.return_const(());

mocku.bar();
mocki.bar();
}

///// Make sure generic methods work with specific impls, too
//#[test]
//fn withf() {
//let mut mocku = MockFoo::<u32>::new();
//mocku.expect_baz::<f32>()
//.withf(|y| y == 3.14159)
//.return_const(());
//mocku.baz::<f32>(3.14159);
//}
29 changes: 29 additions & 0 deletions mockall_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1119,6 +1119,35 @@ mod mock {
assert_contains(&output, quote!(pub(super) fn expect_bean));
assert_contains(&output, quote!(pub(in crate::outer) fn expect_boom));
}

#[test]
fn specific_impl() {
let code = r#"
pub Foo<T: 'static> {}
impl Bar for Foo<u32> {
fn bar(&self);
}
impl Bar for Foo<i32> {
fn bar(&self);
}
"#;
let ts = proc_macro2::TokenStream::from_str(code).unwrap();
let output = do_mock(ts).to_string();
assert_contains(&output, quote!(impl Bar for MockFoo<u32>));
assert_contains(&output, quote!(impl Bar for MockFoo<i32>));
// Ensure we don't duplicate the checkpoint function
assert_not_contains(&output, quote!(
self.Bar_expectations.checkpoint();
self.Bar_expectations.checkpoint();
));
// The expect methods should return specific types, not generic ones
assert_contains(&output, quote!(
pub fn expect_bar(&mut self) -> &mut __mock_MockFoo_Bar::__bar::Expectation<u32>
));
assert_contains(&output, quote!(
pub fn expect_bar(&mut self) -> &mut __mock_MockFoo_Bar::__bar::Expectation<i32>
));
}
}

/// Various tests for overall code generation that are hard or impossible to
Expand Down
32 changes: 23 additions & 9 deletions mockall_derive/src/mock_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -556,15 +556,20 @@ impl MockFunction {
/// # Arguments
///
/// * `modname`: Name of the parent struct's private module
/// * `self_args`: If supplied, these are the
/// AngleBracketedGenericArguments of the self type of the
/// trait impl. e.g. The `T` in `impl Foo for Bar<T>`.
// Supplying modname is an unfortunately hack. Ideally MockFunction
// wouldn't need to know that.
pub fn expect(&self, modname: &Ident) -> impl ToTokens {
pub fn expect(&self, modname: &Ident, self_args: Option<&PathArguments>)
-> impl ToTokens
{
let attrs = AttrFormatter::new(&self.attrs)
.doc(false)
.format();
let name = self.name();
let expect_ident = format_ident!("expect_{}", &name);
let expectation_obj = self.expectation_obj();
let expectation_obj = self.expectation_obj(self_args);
let funcname = &self.sig.ident;
let (_, tg, _) = if self.is_method_generic() {
&self.egenerics
Expand Down Expand Up @@ -610,13 +615,22 @@ impl MockFunction {
}

/// Return the name of this function's expecation object
pub fn expectation_obj(&self) -> impl ToTokens {
fn expectation_obj(&self, self_args: Option<&PathArguments>)
-> impl ToTokens
{
let inner_mod_ident = self.inner_mod_ident();
// staticize any lifetimes. This is necessary for methods that return
// non-static types, because the Expectation itself must be 'static.
let segenerics = staticize(&self.egenerics);
let (_, tg, _) = segenerics.split_for_impl();
quote!(#inner_mod_ident::Expectation #tg)
if let Some(sa) = self_args {
assert!(!self.is_method_generic(),
"specific impls with generic methods are TODO");
quote!(#inner_mod_ident::Expectation #sa)
} else {
// staticize any lifetimes. This is necessary for methods that
// return non-static types, because the Expectation itself must be
// 'static.
let segenerics = staticize(&self.egenerics);
let (_, tg, _) = segenerics.split_for_impl();
quote!(#inner_mod_ident::Expectation #tg)
}
}

/// Return the name of this function's expecations object
Expand Down Expand Up @@ -677,7 +691,7 @@ impl MockFunction {

/// Is the mock method generic (as opposed to a non-generic method of a
/// generic mock struct)?
fn is_method_generic(&self) -> bool {
pub fn is_method_generic(&self) -> bool {
self.call_generics.params.iter().any(|p| {
matches!(p, GenericParam::Type(_))
}) || self.call_generics.where_clause.is_some()
Expand Down
21 changes: 19 additions & 2 deletions mockall_derive/src/mock_item_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
use super::*;

use quote::ToTokens;
use std::collections::HashSet;

use crate::{
mock_function::MockFunction,
Expand Down Expand Up @@ -51,6 +52,22 @@ fn phantom_fields(generics: &Generics) -> Vec<TokenStream> {
}).collect()
}

/// Filter out multiple copies of the same trait, even if they're implemented on
/// different types.
fn unique_trait_iter<'a, I: Iterator<Item = &'a MockTrait>>(i: I)
-> impl Iterator<Item = &'a MockTrait>
{
let mut hs = HashSet::<Path>::default();
i.filter(move |mt| {
if hs.contains(&mt.trait_path) {
false
} else {
hs.insert(mt.trait_path.clone());
true
}
})
}

/// A collection of methods defined in one spot
struct Methods(Vec<MockFunction>);

Expand Down Expand Up @@ -205,12 +222,12 @@ impl ToTokens for MockItemStruct {
.collect::<Vec<_>>();
let expects = self.methods.0.iter()
.filter(|meth| !meth.is_static())
.map(|meth| meth.expect(modname))
.map(|meth| meth.expect(modname, None))
.collect::<Vec<_>>();
let method_checkpoints = self.methods.checkpoints();
let new_method = self.new_method();
let priv_mods = self.methods.priv_mods();
let substructs = self.traits.iter()
let substructs = unique_trait_iter(self.traits.iter())
.map(|trait_| {
MockItemTraitImpl {
attrs: trait_.attrs.clone(),
Expand Down
54 changes: 37 additions & 17 deletions mockall_derive/src/mock_trait.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// vim: tw=80
use proc_macro2::Span;
use quote::{ToTokens, format_ident, quote};
use syn::{
*,
Expand All @@ -13,16 +14,19 @@ use crate::{
pub(crate) struct MockTrait {
pub attrs: Vec<Attribute>,
pub consts: Vec<ImplItemConst>,
pub struct_generics: Generics,
pub generics: Generics,
pub methods: Vec<MockFunction>,
pub path: Path,
structname: Ident,
/// Fully-qualified name of the trait
pub trait_path: Path,
/// Path on which the trait is implemented. Usually will be the same as
/// structname, but might include concrete generic parameters.
self_path: PathSegment,
pub types: Vec<ImplItemType>
}

impl MockTrait {
pub fn name(&self) -> &Ident {
&self.path.segments.last().unwrap().ident
&self.trait_path.segments.last().unwrap().ident
}

/// Create a new MockTrait
Expand All @@ -40,13 +44,22 @@ impl MockTrait {
let mut consts = Vec::new();
let mut methods = Vec::new();
let mut types = Vec::new();
let path = if let Some((_, path, _)) = impl_.trait_ {
let trait_path = if let Some((_, path, _)) = impl_.trait_ {
path
} else {
compile_error(impl_.span(), "impl block must implement a trait");
Path::from(format_ident!("__mockall_invalid"))
};
let ident = &path.segments.last().unwrap().ident;
let trait_ident = &trait_path.segments.last().unwrap().ident;
let self_path = match *impl_.self_ty {
Type::Path(mut type_path) =>
type_path.path.segments.pop().unwrap().into_value(),
x => {
compile_error(x.span(),
"mockall_derive only supports mocking traits and structs");
PathSegment::from(Ident::new("", Span::call_site()))
}
};

for ii in impl_.items.into_iter() {
match ii {
Expand All @@ -60,7 +73,7 @@ impl MockTrait {
.call_levels(0)
.struct_(structname)
.struct_generics(struct_generics)
.trait_(ident)
.trait_(trait_ident)
.build();
methods.push(mf);
},
Expand All @@ -76,10 +89,10 @@ impl MockTrait {
MockTrait {
attrs: impl_.attrs,
consts,
struct_generics: struct_generics.clone(),
generics: impl_.generics,
methods,
path,
structname: structname.clone(),
trait_path,
self_path,
types
}
}
Expand All @@ -93,8 +106,9 @@ impl MockTrait {
// wouldn't need to know that.
pub fn trait_impl(&self, modname: &Ident) -> impl ToTokens {
let attrs = &self.attrs;
let (ig, tg, wc) = self.struct_generics.split_for_impl();
let (ig, _tg, wc) = self.generics.split_for_impl();
let consts = &self.consts;
let path_args = &self.self_path.arguments;
let calls = self.methods.iter()
.map(|meth| meth.call(Some(modname)))
.collect::<Vec<_>>();
Expand All @@ -104,19 +118,25 @@ impl MockTrait {
.collect::<Vec<_>>();
let expects = self.methods.iter()
.filter(|meth| !meth.is_static())
.map(|meth| meth.expect(&modname))
.collect::<Vec<_>>();
let path = &self.path;
let structname = &self.structname;
.map(|meth| {
if meth.is_method_generic() {
// Specific impls with generic methods are TODO.
meth.expect(&modname, None)
} else {
meth.expect(&modname, Some(path_args))
}
}).collect::<Vec<_>>();
let trait_path = &self.trait_path;
let self_path = &self.self_path;
let types = &self.types;
quote!(
#(#attrs)*
impl #ig #path for #structname #tg #wc {
impl #ig #trait_path for #self_path #wc {
#(#consts)*
#(#types)*
#(#calls)*
}
impl #ig #structname #tg #wc {
impl #ig #self_path #wc {
#(#expects)*
#(#contexts)*
}
Expand Down
20 changes: 20 additions & 0 deletions mockall_derive/src/mockable_struct.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,30 @@ fn add_lifetime_parameters(sig: &mut Signature) {
}
}

/// Add "Mock" to the front of the named type
fn mock_ident_in_type(ty: &mut Type) {
match ty {
Type::Path(type_path) => {
if type_path.path.segments.len() != 1 {
compile_error(type_path.path.span(),
"mockall_derive only supports structs defined in the current module");
return;
}
let ident = &mut type_path.path.segments.last_mut().unwrap().ident;
*ident = gen_mock_ident(ident)
},
x => {
compile_error(x.span(),
"mockall_derive only supports mocking traits and structs");
}
};
}

/// Performs transformations on the ItemImpl to make it mockable
fn mockable_item_impl(mut impl_: ItemImpl, name: &Ident, generics: &Generics)
-> ItemImpl
{
mock_ident_in_type(&mut impl_.self_ty);
for item in impl_.items.iter_mut() {
if let ImplItem::Method(ref mut iim) = item {
mockable_method(iim, name, generics);
Expand Down

0 comments on commit 0d9d633

Please sign in to comment.