1 /**
2 Date: 2015-2017, Joakim Brännström
3 License: MPL-2, Mozilla Public License 2.0
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 
6 Generate a google mock implementation of a C++ class with at least one virtual.
7 */
8 module cpptooling.generator.gmock;
9 
10 import std.ascii : newline;
11 import std.algorithm : joiner, map;
12 import std.conv : text;
13 import std.format : format;
14 import std.range : chain, only, retro, takeOne;
15 import std.typecons : Yes, No, Flag;
16 import std.variant : visit;
17 
18 import logger = std.experimental.logger;
19 
20 import dsrcgen.cpp : CppModule, noIndent;
21 
22 import dextool.type : DextoolVersion, CustomHeader, FileName;
23 import cpptooling.data.representation : CppCtor, CppDtor, CppClass,
24     CppNamespace, CppMethodOp, CppMethod, joinParams, joinParamNames;
25 import cpptooling.data : toStringDecl;
26 
27 @safe:
28 
29 private string gmockMacro(size_t len, bool isConst)
30 in {
31     assert(len <= 10);
32 }
33 body {
34     if (isConst)
35         return "MOCK_CONST_METHOD" ~ len.text;
36     else
37         return "MOCK_METHOD" ~ len.text;
38 }
39 
40 private void ignore() {
41 }
42 
43 private void genOp(const CppMethodOp m, CppModule hdr) {
44     import cpptooling.data : MemberVirtualType;
45 
46     static string translateOp(string op) {
47         switch (op) {
48         case "=":
49             return "opAssign";
50         case "==":
51             return "opEqual";
52         default:
53             logger.errorf(
54                     "Operator '%s' is not supported. Create an issue on github containing the operator and example code.",
55                     op);
56             return "operator not supported";
57         }
58     }
59 
60     static void genMockMethod(const CppMethodOp m, CppModule hdr) {
61         string params = m.paramRange().joinParams();
62         string gmock_name = translateOp(m.op);
63         string gmock_macro = gmockMacro(m.paramRange().length, m.isConst);
64         //TODO should use the toString function for TypeKind + TypeAttr, otherwise const isn't affecting it.
65         string stmt = format("%s(%s, %s(%s))", gmock_macro, gmock_name,
66                 m.returnType.toStringDecl, params);
67         hdr.stmt(stmt);
68     }
69 
70     static void genMockCaller(const CppMethodOp m, CppModule hdr) {
71         import dsrcgen.cpp : E;
72 
73         string gmock_name = translateOp(m.op);
74 
75         //TODO should use the toString function for TypeKind + TypeAttr, otherwise const isn't affecting it.
76         CppModule code = hdr.method_inline(Yes.isVirtual, m.returnType.toStringDecl,
77                 m.name, m.isConst ? Yes.isConst : No.isConst, m.paramRange().joinParams());
78         auto call = E(gmock_name)(m.paramRange().joinParamNames);
79 
80         if (m.returnType.toStringDecl == "void") {
81             code.stmt(call);
82         } else {
83             code.return_(call);
84         }
85     }
86 
87     genMockMethod(m, hdr);
88     genMockCaller(m, hdr);
89 }
90 
91 private void genMethod(const CppMethod m, CppModule hdr) {
92     enum MAX_GMOCK_PARAMS = 10;
93 
94     void genMethodWithFewParams(const CppMethod m, CppModule hdr) {
95         hdr.stmt(format("%s(%s, %s(%s))", gmockMacro(m.paramRange().length,
96                 m.isConst), m.name, m.returnType.toStringDecl, m.paramRange().joinParams()));
97         return;
98     }
99 
100     void genMethodWithManyParams(const CppMethod m, CppModule hdr) {
101         import std.range : chunks, enumerate, dropBackOne;
102 
103         static string partName(string name, size_t part_no) {
104             return format("%s_MockPart%s", name, part_no);
105         }
106 
107         static void genPart(T)(size_t part_no, T a, const CppMethod m,
108                 CppModule code, CppModule delegate_mock) {
109             import dsrcgen.cpp : E;
110 
111             // dfmt off
112             // inject gmock macro
113             code.stmt(format("%s(%s, void(%s))",
114                              gmockMacro(a.length, m.isConst),
115                              partName(m.name, part_no),
116                              a.joinParams));
117             // inject delegation call to gmock macro
118             delegate_mock.stmt(E(partName(m.name, part_no))(a.joinParamNames));
119             // dfmt on
120         }
121 
122         static void genLastPart(T)(size_t part_no, T p, const CppMethod m,
123                 CppModule code, CppModule delegate_mock) {
124             import dsrcgen.cpp : E;
125 
126             auto part_name = partName(m.name, part_no);
127             code.stmt(format("%s(%s, %s(%s))", gmockMacro(p.length, m.isConst),
128                     part_name, m.returnType.toStringDecl, p.joinParams));
129 
130             auto stmt = E(part_name)(p.joinParamNames);
131 
132             if (m.returnType.toStringDecl == "void") {
133                 delegate_mock.stmt(stmt);
134             } else {
135                 delegate_mock.return_(stmt);
136             }
137         }
138 
139         // Code block for gmock macros
140         auto code = hdr.base();
141         code.suppressIndent(1);
142 
143         // Generate mock method that delegates to partial mock methods
144         auto delegate_mock = hdr.method_inline(Yes.isVirtual, m.returnType.toStringDecl,
145                 m.name, cast(Flag!"isConst") m.isConst, m.paramRange().joinParams());
146 
147         auto param_chunks = m.paramRange.chunks(MAX_GMOCK_PARAMS);
148 
149         // dfmt off
150         foreach (a; param_chunks
151                  .save // don't modify the range
152                  .dropBackOne // separate last chunk to simply logic,
153                  // all methods will thus return void
154                  .enumerate(1)) {
155             genPart(a.index, a.value, m, code, delegate_mock);
156         }
157         // dfmt on
158 
159         // if the mocked function returns a value it is simulated via the "last
160         // part".
161         genLastPart(param_chunks.length, param_chunks.back, m, code, delegate_mock);
162     }
163 
164     if (m.paramRange().length <= MAX_GMOCK_PARAMS) {
165         genMethodWithFewParams(m, hdr);
166     } else {
167         genMethodWithManyParams(m, hdr);
168     }
169 }
170 
171 /** Generate a Google Mock that implements in_c.
172  *
173  * Gmock has a restriction of max 10 parameters in a method. This gmock
174  * generator has a work-around for the limitation by splitting the parameters
175  * over many gmock functions. To fulfill the interface the generator then
176  * generates an inlined function that in turn calls the gmocked functions.
177  *
178  * See test case class_interface_more_than_10_params.hpp.
179  *
180  * Params:
181  *   in_c = Class to generate a mock implementation of.
182  *   hdr = Header to generate the code in.
183  *   ns_name = namespace the mock is generated in.
184  */
185 void generateGmock(const CppClass in_c, CppModule hdr)
186 in {
187     assert(in_c.isVirtual);
188 }
189 body {
190     // dfmt off
191     // fully qualified class the mock inherit from
192     auto base_class = "public " ~
193         chain(
194               // when joined ensure the qualifier start with "::"
195               in_c.nsNestingRange.takeOne.map!(a => ""),
196               in_c.nsNestingRange.retro.map!(a => a),
197               only(cast(string) in_c.name))
198         .joiner("::")
199         .text;
200     // dfmt on
201     auto c = hdr.class_("Mock" ~ in_c.name, base_class);
202     auto pub = c.public_();
203     pub.dtor(Yes.isVirtual, "Mock" ~ in_c.name)[$.end = " {}" ~ newline];
204 
205     foreach (m; in_c.methodRange()) {
206         // dfmt off
207         () @trusted {
208         m.visit!((const CppMethod m) => genMethod(m, pub),
209                  (const CppMethodOp m) => genOp(m, pub),
210                  (const CppCtor m) => ignore(),
211                  (const CppDtor m) => ignore());
212         }();
213         // dfmt on
214     }
215     hdr.sep(2);
216 }
217 
218 auto generateGmockHdr(FileName if_file, FileName incl_guard, DextoolVersion ver,
219         CustomHeader custom_hdr, CppModule gmock) {
220     import std.path : baseName;
221     import dsrcgen.cpp : CppHModule;
222     import cpptooling.generator.includes : convToIncludeGuard, makeHeader;
223 
224     auto o = CppHModule(convToIncludeGuard(incl_guard));
225     o.header.append(makeHeader(incl_guard, ver, custom_hdr));
226     o.content.include(if_file.baseName);
227     o.content.include("gmock/gmock.h");
228     o.content.sep(2);
229     o.content.append(gmock);
230 
231     return o;
232 }
233 
234 /** Make a gmock implementation of the class.
235  *
236  * The generated gmock have unique USR and ID.
237  */
238 auto makeGmock(const CppClass c) {
239     import std.array : array;
240     import std.variant : visit;
241     import cpptooling.data : makeUniqueUSR, nextUniqueID, getName, CppAccess,
242         CppConstMethod, CppVirtualMethod, AccessType, MemberVirtualType;
243     import cpptooling.utility.sort : indexSort;
244 
245     // Make all protected and private methods public to allow testing, for good
246     // and bad. The policy can be changed. It is not set in stone. It is what I
247     // thought was good at the time. If it creates problems in the future.
248     // Identified use cases etc. Change it.
249     static auto conv(T)(T m_) {
250         import std.array : array;
251 
252         auto params = m_.paramRange.array();
253 
254         auto m = CppMethod(m_.usr, m_.name, params, m_.returnType, CppAccess(AccessType.Public),
255                 CppConstMethod(m_.isConst), CppVirtualMethod(MemberVirtualType.Pure));
256 
257         return m;
258     }
259 
260     auto rclass = CppClass(c.name, c.inherits, c.resideInNs);
261     rclass.usr = makeUniqueUSR;
262     rclass.unsafeForceID = nextUniqueID;
263 
264     //dfmt off
265     foreach (m_in; c.methodRange
266              .array()
267              .indexSort!((ref a, ref b) => getName(a) < getName(b))
268              ) {
269         () @trusted{
270             m_in.visit!((const CppMethod m) => m.isVirtual ? rclass.put(conv(m)) : false,
271                         (const CppMethodOp m) => m.isVirtual ? rclass.put(conv(m)) : false,
272                         (const CppCtor m) {},
273                         (const CppDtor m) => m.isVirtual ? rclass.put(m) : false);
274         }();
275     }
276     //dfmt on
277 
278     return rclass;
279 }