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 }