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