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