From 0d9d633ca7efcf01712e61a0c227ead56f81db13 Mon Sep 17 00:00:00 2001 From: Alan Somers Date: Fri, 23 Apr 2021 12:11:30 -0600 Subject: [PATCH] Support specific impls A specific impl is an implementation of a trait on a generic struct with specific generic parameters, like `impl Foo for Bar {}` Issue #271 --- CHANGELOG.md | 9 +++++ mockall/tests/specific_impl.rs | 49 +++++++++++++++++++++++ mockall_derive/src/lib.rs | 29 ++++++++++++++ mockall_derive/src/mock_function.rs | 32 ++++++++++----- mockall_derive/src/mock_item_struct.rs | 21 +++++++++- mockall_derive/src/mock_trait.rs | 54 ++++++++++++++++++-------- mockall_derive/src/mockable_struct.rs | 20 ++++++++++ 7 files changed, 186 insertions(+), 28 deletions(-) create mode 100644 mockall/tests/specific_impl.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b971394c..32506727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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` as opposed to `impl Foo for Bar`. 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. diff --git a/mockall/tests/specific_impl.rs b/mockall/tests/specific_impl.rs new file mode 100644 index 00000000..6ea9506b --- /dev/null +++ b/mockall/tests/specific_impl.rs @@ -0,0 +1,49 @@ +// vim: ts=80 +#![deny(warnings)] + +use mockall::*; + +trait Bar { + fn bar(&self); +} +//trait Baz { + //fn baz(&self, y: Y); +//} + +mock! { + pub Foo { + } + impl Bar for Foo { + fn bar(&self); + } + impl Bar for Foo { + fn bar(&self); + } + // TODO: support specific impls with generic methods, like this + //impl Baz for Foo { + //fn baz(&self, y: Y); + //} +} + +#[test] +fn return_const() { + let mut mocku = MockFoo::::new(); + mocku.expect_bar() + .return_const(()); + let mut mocki = MockFoo::::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::::new(); + //mocku.expect_baz::() + //.withf(|y| y == 3.14159) + //.return_const(()); + //mocku.baz::(3.14159); +//} diff --git a/mockall_derive/src/lib.rs b/mockall_derive/src/lib.rs index 78673dab..85cd3f1e 100644 --- a/mockall_derive/src/lib.rs +++ b/mockall_derive/src/lib.rs @@ -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 {} + impl Bar for Foo { + fn bar(&self); + } + impl Bar for Foo { + 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)); + assert_contains(&output, quote!(impl Bar for MockFoo)); + // 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 + )); + assert_contains(&output, quote!( + pub fn expect_bar(&mut self) -> &mut __mock_MockFoo_Bar::__bar::Expectation + )); + } } /// Various tests for overall code generation that are hard or impossible to diff --git a/mockall_derive/src/mock_function.rs b/mockall_derive/src/mock_function.rs index 8a3e4cdd..0642d66b 100644 --- a/mockall_derive/src/mock_function.rs +++ b/mockall_derive/src/mock_function.rs @@ -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`. // 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 @@ -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 @@ -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() diff --git a/mockall_derive/src/mock_item_struct.rs b/mockall_derive/src/mock_item_struct.rs index 115c4a1c..c8682ee9 100644 --- a/mockall_derive/src/mock_item_struct.rs +++ b/mockall_derive/src/mock_item_struct.rs @@ -2,6 +2,7 @@ use super::*; use quote::ToTokens; +use std::collections::HashSet; use crate::{ mock_function::MockFunction, @@ -51,6 +52,22 @@ fn phantom_fields(generics: &Generics) -> Vec { }).collect() } +/// Filter out multiple copies of the same trait, even if they're implemented on +/// different types. +fn unique_trait_iter<'a, I: Iterator>(i: I) + -> impl Iterator +{ + let mut hs = HashSet::::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); @@ -205,12 +222,12 @@ impl ToTokens for MockItemStruct { .collect::>(); let expects = self.methods.0.iter() .filter(|meth| !meth.is_static()) - .map(|meth| meth.expect(modname)) + .map(|meth| meth.expect(modname, None)) .collect::>(); 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(), diff --git a/mockall_derive/src/mock_trait.rs b/mockall_derive/src/mock_trait.rs index e4a3dd1a..f0b22833 100644 --- a/mockall_derive/src/mock_trait.rs +++ b/mockall_derive/src/mock_trait.rs @@ -1,4 +1,5 @@ // vim: tw=80 +use proc_macro2::Span; use quote::{ToTokens, format_ident, quote}; use syn::{ *, @@ -13,16 +14,19 @@ use crate::{ pub(crate) struct MockTrait { pub attrs: Vec, pub consts: Vec, - pub struct_generics: Generics, + pub generics: Generics, pub methods: Vec, - 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 } impl MockTrait { pub fn name(&self) -> &Ident { - &self.path.segments.last().unwrap().ident + &self.trait_path.segments.last().unwrap().ident } /// Create a new MockTrait @@ -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 { @@ -60,7 +73,7 @@ impl MockTrait { .call_levels(0) .struct_(structname) .struct_generics(struct_generics) - .trait_(ident) + .trait_(trait_ident) .build(); methods.push(mf); }, @@ -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 } } @@ -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::>(); @@ -104,19 +118,25 @@ impl MockTrait { .collect::>(); let expects = self.methods.iter() .filter(|meth| !meth.is_static()) - .map(|meth| meth.expect(&modname)) - .collect::>(); - 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::>(); + 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)* } diff --git a/mockall_derive/src/mockable_struct.rs b/mockall_derive/src/mockable_struct.rs index 1562286f..3c543ee6 100644 --- a/mockall_derive/src/mockable_struct.rs +++ b/mockall_derive/src/mockable_struct.rs @@ -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);