1 /**
2   Copyright: 2017 © LLC CERERIS
3   License: MIT
4   Authors: LLC CERERIS
5 */
6 
7 module checkit.mock;
8 
9 import checkit.exception;
10 import core.exception;
11 import std..string;
12 
13 /// Class for create Mock object
14 class Mock(T)
15 {
16   public:
17     /// Default constructor
18     this()
19     {
20       _implementation = new MockAbstract;
21     }
22 
23     /** Add a value to be returned by the mock.
24 
25       Params:
26         functionName = The name of function mock.
27         i = The count of realization for override function.
28         values = Are return values.
29 
30       Throws:
31         If function not virtual, will throw an AssertError.
32 
33       Examples:
34         ---
35         interface Dummy
36         {
37           int test();
38         }
39         auto mock = new Mock!Dummy;
40         mock.returnValue!"test"(1,2);
41         assert(mock.test() == 1);
42         assert(mock.test() == 2);
43         ---
44     */
45     void returnValue(string functionName, ubyte i = 0, V...)(V values)
46     {
47       import std.conv: text;
48 
49       alias member = Identity!(__traits(getMember, T, functionName));
50       static assert(__traits(isVirtualMethod, member), "Can't use return value for <" ~ functionName ~ ">");
51 
52       enum varName = functionName ~ text(`_`, i, `_returnValues`);
53       foreach(v; values)
54       {
55         mixin(varName ~ ` ~=  v;`);
56       }
57     }
58 
59     /// Mock implementation
60     MockAbstract _implementation;
61 
62   private:
63     /// Abstract Mock template
64     class MockAbstract: T
65     {
66       import std.conv: to;
67       import std.traits: Parameters, ReturnType;
68       import std.typecons: tuple;
69       mixin(generateDefaultImplementationMixin!T);
70       mixin MockCommon;
71     }
72 
73     alias _implementation this;
74 }
75 
76 /// Mixin for test behavior
77 mixin template MockCommon()
78 {
79   public:
80     /** Used to assert function call once
81 
82       Params:
83         functionName = The name of expected function.
84         message = Exception message.
85         file = The file name that the assert failed in. Should be left as default.
86         line = The file line that the assert failed in. Should be left as default.
87         values = Are expected parameter values.
88 
89       Throws:
90         If function is not call with parameters, will throw an UnitTestException.
91 
92       Examples:
93         ---
94         interface Dummy
95         {
96           void test();
97           void test(int a);
98         }
99 
100         auto mock = new Mock!Dummy;
101         mock.test();
102         mock.test(12);
103         mock.expectCalled!"test"();
104         mock.expectCalled!"test"(12);
105 
106         // Will throw an exception like "UnitTestException@example.d(6): <test> expected call with <50> but called with <Tuple!int(12)>
107         mock.exceptCalled!"test"(50);
108         ---
109     */
110     void expectCalled(string functionName, 
111                       string message = null, 
112                       string file = __FILE__, 
113                       size_t line = __LINE__, 
114                       V...)(auto ref V values)
115     {
116       expectCalled!(functionName, 1, message, file, line)(values);
117     }
118 
119     /** Used to assert function not call
120 
121       Params:
122         functionName = The name of expected function.
123         message = Exception message.
124         file = The file name that the assert failed in. Should be left as default.
125         line = The file line that the assert failed in. Should be left as default.
126         values = Are expected parameter values.
127 
128       Throws:
129         If function is call with parameters, will throw an UnitTestException.
130 
131       Examples:
132         ---
133         interface Dummy
134         {
135           void test();
136           void test(int a);
137         }
138 
139         auto mock = new Mock!Dummy;
140         mock.test(12);
141         mock.expectNotCalled!"test"();
142         mock.expectNotCalled!"test"(24);
143 
144         // Will throw an exception like "UnitTestException@example.d(6): <test> expected call with <12> <0> count but called <1> counts        
145         mock.exceptNotCalled!"test"(12);
146         ---
147     */
148     void expectNotCalled( string functionName, 
149                           string message = null, 
150                           string file = __FILE__, 
151                           size_t line = __LINE__, 
152                           V...)(auto ref V values)
153     {
154       expectCalled!(functionName, 0, message, file, line)(values);
155     }
156 
157     /** Used to assert function call many counts
158 
159       Params:
160         functionName = The name of expected function.
161         count = Count of calls.
162         message = Exception message.
163         file = The file name that the assert failed in. Should be left as default.
164         line = The file line that the assert failed in. Should be left as default.
165         values = Are expected parameter values.
166 
167       Throws:
168         If function is not call many count with parameters, will throw an UnitTestException.
169 
170       Examples:
171         ---
172         interface Dummy
173         {
174           void test();
175           void test(int a);
176         }
177 
178         auto mock = new Mock!Dummy;
179         mock.test();
180         mock.test(12);
181         mock.expectCalled!("test", 1)();
182         mock.expectCalled!("test", 1)(12);
183 
184         // Will throw an exception like "UnitTestException@example.d(6): <test> expected call with <12> <2> count but called <1> counts
185         mock.exceptCalled!("test", 2)(12);
186         ---
187     */
188     void expectCalled(string functionName, 
189                       ulong count, 
190                       string message = null, 
191                       string file = __FILE__, 
192                       size_t line = __LINE__, 
193                       V...)(auto ref V values)
194     {
195       string[] callVariants;
196       ulong callCount;
197 
198       foreach(i, calledFunctionName; _calledFuncs)
199       {
200         if(calledFunctionName == functionName)
201         {
202           if(tuple(values).to!string() == _calledValues[i])
203           {
204             callCount++;
205           }
206           else
207           {
208             callVariants ~= _calledValues[i];
209           }
210         }
211       }
212 
213       if(callCount != count)
214       {
215         if(!message)
216         {
217           string[] arguments;
218           foreach(value; values)
219           {
220             arguments ~= to!string(value);
221           }
222 
223           if(callCount > 0)
224           {
225 		        throw new UnitTestException(
226                 "<%s> expected call with <%s> <%d> count but called <%d> counts".format(functionName, 
227                                                                                         arguments.join(","), 
228                                                                                         count, 
229                                                                                         callCount), 
230                 file, 
231                 line);
232           }
233           else
234           {
235 		        throw new UnitTestException(
236                 "<%s> expected call with <%s> but called with <%s>".format( functionName, 
237                                                                             arguments.join(","), 
238                                                                             callVariants.join(",")), 
239                 file, 
240                 line);
241           }
242 		    }
243         else
244         {
245 		      throw new UnitTestException(message, file, line);
246         }
247       }
248     }
249 
250   private:
251     string[] _calledFuncs;
252     string[] _calledValues;
253 }
254 
255 alias Identity(alias T) = T;
256 private enum isPrivate(T, string member) = !__traits(compiles, __traits(getMember, T, member));
257 
258 private string generateDefaultImplementationMixin(T)()
259 {
260   import std.array: join;
261   import std.conv: text;
262   import std.format: format;
263   import std.traits: arity, FunctionAttribute, functionAttributes, Parameters;
264 
265   string[] code;
266 
267   if(!__ctfe) return null;
268 
269   // foreach all member in the class
270   foreach(memberName; __traits(allMembers, T))
271   {
272     // If not private member
273     static if(!isPrivate!(T, memberName))
274     {
275       alias member = Identity!(__traits(getMember, T, memberName));
276       // If it is virtual method
277       static if(__traits(isVirtualMethod, member))
278       {
279         foreach(i, overload; __traits(getOverloads, T, memberName))
280         {
281           static if(!(functionAttributes!member & FunctionAttribute.const_))
282           {
283             enum overloadName = text(memberName, "_", i);
284             enum overloadString = `Identity!(__traits(getOverloads, T, "%s")[%s])`.format(memberName, i);
285             code ~= "private alias %s_parameters = Parameters!(%s);".format(overloadName, overloadString);
286             code ~= "private alias %s_returnType = ReturnType!(%s);".format(overloadName, overloadString);
287 
288             static if(functionAttributes!member & FunctionAttribute.nothrow_)
289             {
290               enum tryIndent = "  ";
291             }
292             else
293             {
294               enum tryIndent = "";
295             }
296 
297             string[] returnDefault;
298 
299             static if(typeof(overload).stringof.indexOf("void(") != 0)
300             {
301               enum varName = overloadName ~ `_returnValues`;
302               code ~= `private %s_returnType[] %s;`.format(overloadName, varName);
303               code ~= "";
304               returnDefault = [
305                 ` if(` ~ varName ~ `.length > 0)`,
306                 ` {`,
307                 `   auto ret = ` ~ varName ~ `[0];`,
308                 `   ` ~ varName ~ ` = ` ~ varName ~ `[1..$];`,
309                 `   return ret;`,
310                 ` }`,
311                 ` else`,
312                 ` {`,
313                 `   return %s_returnType.init;`.format(overloadName),
314                 ` }`];
315             }
316 
317             string[] parts;
318             string[] tupleParts;
319             foreach(j, t; Parameters!overload)
320             {
321               parts ~= "%s_parameters[%s] arg%s".format(overloadName, j, j);
322               tupleParts ~= "arg%s".format(j);
323             }
324 
325             code ~= `override ` ~ overloadName ~ "_returnType " ~ memberName ~ "(" ~ parts.join(", ") ~ 
326               ")" ~ " " ~ functionAttributesString!member;
327             code ~= "{";
328 
329             static if(functionAttributes!member & FunctionAttribute.nothrow_)
330             {
331               code ~= "try";
332               code ~= "{";
333             }
334 
335             code ~= tryIndent ~ ` _calledFuncs ~= "` ~ memberName ~ `";`;
336             code ~= tryIndent ~ ` _calledValues ~= tuple` ~ "(" ~ tupleParts.join(", ")  ~ ")" ~ `.to!string;`;
337 
338             static if(functionAttributes!member & FunctionAttribute.nothrow_)
339             {
340               code ~= "}";
341               code ~= "catch(Exception) {}";
342             }
343 
344             code ~= returnDefault.join("\n");
345             code ~= `}`;
346             code ~= "";
347           }
348         }
349       }
350     }
351   }
352   
353   return code.join("\n");
354 }
355 
356 private string functionAttributesString(alias F)()
357 {
358   import std.array: join;
359   import std.traits: FunctionAttribute, functionAttributes;
360 
361   string[] parts;
362   const attrs = functionAttributes!F;
363 
364   if(attrs & FunctionAttribute.pure_) parts ~= "pure";
365   if(attrs & FunctionAttribute.nothrow_) parts ~= "nothrow";
366   if(attrs & FunctionAttribute.trusted) parts ~= "@trusted";
367   if(attrs & FunctionAttribute.safe) parts ~= "@safe";
368   if(attrs & FunctionAttribute.nogc) parts ~= "@nogc";
369   if(attrs & FunctionAttribute.system) parts ~= "@system";
370   if(attrs & FunctionAttribute.shared_) parts ~= "shared";
371 
372   return parts.join(" ");
373 }