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 }