1 module unit_threaded.mock;
2 
3 import unit_threaded.should: fail;
4 import std.traits;
5 import std.typecons;
6 import std.meta: allSatisfy;
7 
8 version(unittest) {
9     import unit_threaded.asserts;
10     import unit_threaded.should;
11 }
12 
13 
14 alias Identity(alias T) = T;
15 private enum isPrivate(T, string member) = !__traits(compiles, __traits(getMember, T, member));
16 
17 
18 string implMixinStr(T)() {
19     import std.array: join;
20     import std.format : format;
21     import std.range : iota;
22     import std.traits: functionAttributes, FunctionAttribute;
23 
24     string[] lines;
25 
26     foreach(m; __traits(allMembers, T)) {
27 
28         static if(!isPrivate!(T, m)) {
29 
30             alias member = Identity!(__traits(getMember, T, m));
31 
32             static if(__traits(isAbstractFunction, member)) {
33 
34                 lines ~= "private alias %s_parameters = Parameters!(Identity!(__traits(getMember, T, \"%s\")));".format(m, m);
35                 lines ~= "private alias %s_returnType = ReturnType!(Identity!(__traits(getMember, T, \"%s\")));".format(m, m);
36 
37                 static if(functionAttributes!member & FunctionAttribute.nothrow_)
38                     enum tryIndent = "    ";
39                 else
40                     enum tryIndent = "";
41 
42 
43                 static if(is(ReturnType!member == void))
44                     enum returnDefault = "";
45                 else {
46                     enum varName = m ~ `_returnValues`;
47                     lines ~= `%s_returnType[] %s;`.format(m, varName);
48                     lines ~= "";
49                     enum returnDefault = [`    if(` ~ varName ~ `.length > 0) {`,
50                                           `        auto ret = ` ~ varName ~ `[0];`,
51                                           `        ` ~ varName ~ ` = ` ~ varName ~ `[1..$];`,
52                                           `        return ret;`,
53                                           `    } else`,
54                                           `        return %s_returnType.init;`.format(m)];
55                 }
56 
57                 lines ~= `override ` ~ m ~ "_returnType " ~ m ~ typeAndArgsParens!(Parameters!member)(m) ~ ` {`;
58 
59                 static if(functionAttributes!member & FunctionAttribute.nothrow_)
60                     lines ~= "try {";
61 
62                 lines ~= tryIndent ~ `    calledFuncs ~= "` ~ m ~ `";`;
63                 lines ~= tryIndent ~ `    calledValues ~= tuple` ~ argNamesParens(arity!member) ~ `.to!string;`;
64 
65                 static if(functionAttributes!member & FunctionAttribute.nothrow_)
66                     lines ~= "    } catch(Exception) {}";
67 
68                 lines ~= returnDefault;
69 
70                 lines ~= `}`;
71                 lines ~= "";
72             }
73         }
74     }
75 
76     return lines.join("\n");
77 }
78 
79 private string argNamesParens(int N) @safe pure {
80     return "(" ~ argNames(N) ~ ")";
81 }
82 
83 private string argNames(int N) @safe pure {
84     import std.range;
85     import std.algorithm;
86     import std.conv;
87     return iota(N).map!(a => "arg" ~ a.to!string).join(", ");
88 }
89 
90 private string typeAndArgsParens(T...)(string prefix) {
91     import std.array;
92     import std.conv;
93     import std.format : format;
94     string[] parts;
95     foreach(i, t; T)
96         parts ~= "%s_parameters[%s] arg%s".format(prefix, i, i);
97     return "(" ~ parts.join(", ") ~ ")";
98 }
99 
100 mixin template MockImplCommon() {
101     bool verified;
102     string[] expectedFuncs;
103     string[] calledFuncs;
104     string[] expectedValues;
105     string[] calledValues;
106 
107     void expect(string funcName, V...)(V values) @safe pure {
108         import std.conv: to;
109         import std.typecons: tuple;
110 
111         expectedFuncs ~= funcName;
112         static if(V.length > 0)
113             expectedValues ~= tuple(values).to!string;
114         else
115             expectedValues ~= "";
116     }
117 
118     void expectCalled(string func, string file = __FILE__, size_t line = __LINE__, V...)(V values) {
119         expect!func(values);
120         verify(file, line);
121     }
122 
123     void verify(string file = __FILE__, size_t line = __LINE__) @safe pure {
124         import std.range;
125         import std.conv;
126 
127         if(verified)
128             fail("Mock already verified", file, line);
129 
130         verified = true;
131 
132         for(int i = 0; i < expectedFuncs.length; ++i) {
133 
134             if(i >= calledFuncs.length)
135                 fail("Expected nth " ~ i.to!string ~ " call to " ~ expectedFuncs[i] ~ " did not happen", file, line);
136 
137             if(expectedFuncs[i] != calledFuncs[i])
138                 fail("Expected nth " ~ i.to!string ~ " call to " ~ expectedFuncs[i] ~ " but got " ~ calledFuncs[i] ~
139                      " instead",
140                      file, line);
141 
142             if(expectedValues[i] != calledValues[i] && expectedValues[i] != "")
143                 throw new UnitTestException([expectedFuncs[i] ~ " was called with unexpected " ~ calledValues[i],
144                                              " ".repeat.take(expectedFuncs[i].length + 4).join ~
145                                              "instead of the expected " ~ expectedValues[i]] ,
146                                             file, line);
147         }
148     }
149 }
150 
151 private enum isString(alias T) = is(typeof(T) == string);
152 
153 struct Mock(T) {
154 
155     MockAbstract _impl;
156     alias _impl this;
157 
158     class MockAbstract: T {
159         import std.conv: to;
160         //pragma(msg, "\n\n", implMixinStr!T, "\n\n");
161         mixin(implMixinStr!T);
162         mixin MockImplCommon;
163     }
164 
165     this(int/* force constructor*/) {
166         _impl = new MockAbstract;
167     }
168 
169     ~this() pure @safe {
170         if(!verified) verify;
171     }
172 
173     void returnValue(string funcName, V...)(V values) {
174         enum varName = funcName ~ `_returnValues`;
175         foreach(v; values)
176             mixin(varName ~ ` ~=  v;`);
177     }
178 }
179 
180 private string importsString(string module_, string[] Modules...) {
181     auto ret = `import ` ~ module_ ~ ";\n";
182     foreach(extraModule; Modules) {
183         ret ~= `import ` ~ extraModule ~ ";\n";
184     }
185     return ret;
186 }
187 
188 auto mock(T)() {
189     return Mock!T(0);
190 }
191 
192 
193 @("mock interface positive test no params")
194 @safe pure unittest {
195     interface Foo {
196         int foo(int, string) @safe pure;
197         void bar() @safe pure;
198     }
199 
200     int fun(Foo f) {
201         return 2 * f.foo(5, "foobar");
202     }
203 
204     auto m = mock!Foo;
205     m.expect!"foo";
206     fun(m);
207 }
208 
209 @("mock interface positive test with params")
210 @safe pure unittest {
211     import unit_threaded.asserts;
212 
213     interface Foo {
214         int foo(int, string) @safe pure;
215         void bar() @safe pure;
216     }
217 
218     int fun(Foo f) {
219         return 2 * f.foo(5, "foobar");
220     }
221 
222     {
223         auto m = mock!Foo;
224         m.expect!"foo"(5, "foobar");
225         fun(m);
226     }
227 
228     {
229         auto m = mock!Foo;
230         m.expect!"foo"(6, "foobar");
231         fun(m);
232         assertExceptionMsg(m.verify,
233                            "    source/unit_threaded/mock.d:123 - foo was called with unexpected Tuple!(int, string)(5, \"foobar\")\n" ~
234                            "    source/unit_threaded/mock.d:123 -        instead of the expected Tuple!(int, string)(6, \"foobar\")");
235     }
236 
237     {
238         auto m = mock!Foo;
239         m.expect!"foo"(5, "quux");
240         fun(m);
241         assertExceptionMsg(m.verify,
242                            "    source/unit_threaded/mock.d:123 - foo was called with unexpected Tuple!(int, string)(5, \"foobar\")\n" ~
243                            "    source/unit_threaded/mock.d:123 -        instead of the expected Tuple!(int, string)(5, \"quux\")");
244     }
245 }
246 
247 
248 @("mock interface negative test")
249 @safe pure unittest {
250     interface Foo {
251         int foo(int, string) @safe pure;
252     }
253 
254     auto m = mock!Foo;
255     m.expect!"foo";
256     m.verify.shouldThrowWithMessage("Expected nth 0 call to foo did not happen");
257 }
258 
259 // can't be in the unit test itself
260 version(unittest)
261 private class Class {
262     abstract int foo(int, string) @safe pure;
263     final int timesTwo(int i) @safe pure nothrow const { return i * 2; }
264     int timesThree(int i) @safe pure nothrow const { return i * 3; }
265 }
266 
267 @("mock class positive test")
268 @safe pure unittest {
269 
270     int fun(Class f) {
271         return 2 * f.foo(5, "foobar");
272     }
273 
274     auto m = mock!Class;
275     m.expect!"foo";
276     fun(m);
277 }
278 
279 
280 @("mock interface multiple calls")
281 @safe pure unittest {
282     interface Foo {
283         int foo(int, string) @safe pure;
284         int bar(int) @safe pure;
285     }
286 
287     void fun(Foo f) {
288         f.foo(3, "foo");
289         f.bar(5);
290         f.foo(4, "quux");
291     }
292 
293     auto m = mock!Foo;
294     m.expect!"foo"(3, "foo");
295     m.expect!"bar"(5);
296     m.expect!"foo"(4, "quux");
297     fun(m);
298     m.verify;
299 }
300 
301 @("interface expectCalled")
302 @safe pure unittest {
303     interface Foo {
304         int foo(int, string) @safe pure;
305         void bar() @safe pure;
306     }
307 
308     int fun(Foo f) {
309         return 2 * f.foo(5, "foobar");
310     }
311 
312     auto m = mock!Foo;
313     fun(m);
314     m.expectCalled!"foo"(5, "foobar");
315 }
316 
317 @("interface return value")
318 @safe pure unittest {
319     interface Foo {
320         int timesN(int i) @safe pure;
321     }
322 
323     int fun(Foo f) {
324         return f.timesN(3) * 2;
325     }
326 
327     auto m = mock!Foo;
328     m.returnValue!"timesN"(42);
329     immutable res = fun(m);
330     res.shouldEqual(84);
331 }
332 
333 @("interface return values")
334 @safe pure unittest {
335     interface Foo {
336         int timesN(int i) @safe pure;
337     }
338 
339     int fun(Foo f) {
340         return f.timesN(3) * 2;
341     }
342 
343     auto m = mock!Foo;
344     m.returnValue!"timesN"(42, 12);
345     fun(m).shouldEqual(84);
346     fun(m).shouldEqual(24);
347     fun(m).shouldEqual(0);
348 }
349 
350 
351 auto mockStruct(T...)(T returns) {
352 
353     struct Mock {
354 
355         MockImpl* _impl;
356         alias _impl this;
357 
358         static struct MockImpl {
359 
360             static if(T.length > 0) {
361                 alias FirstType = typeof(returns[0]);
362                 private FirstType[] _returnValues;
363             }
364 
365             mixin MockImplCommon;
366 
367             auto opDispatch(string funcName, V...)(V values) {
368                 import std.conv: to;
369                 import std.typecons: tuple;
370                 calledFuncs ~= funcName;
371                 calledValues ~= tuple(values).to!string;
372                 static if(T.length > 0) {
373                     if(_returnValues.length == 0) return typeof(_returnValues[0]).init;
374                     auto ret = _returnValues[0];
375                      _returnValues = _returnValues[1..$];
376                     return ret;
377                 }
378             }
379         }
380     }
381 
382     Mock m;
383     m._impl = new Mock.MockImpl;
384     static if(T.length > 0)
385         foreach(r; returns)
386             m._impl._returnValues ~= r;
387     return m;
388 }
389 
390 
391 @("mock struct positive")
392 @safe pure unittest {
393     void fun(T)(T t) {
394         t.foobar;
395     }
396     auto m = mockStruct;
397     m.expect!"foobar";
398     fun(m);
399     m.verify;
400 }
401 
402 @("mock struct negative")
403 @safe pure unittest {
404     auto m = mockStruct;
405     m.expect!"foobar";
406     assertExceptionMsg(m.verify,
407                        "    source/unit_threaded/mock.d:123 - Expected nth 0 call to foobar did not happen\n");
408 
409 }
410 
411 
412 @("mock struct values positive")
413 @safe pure unittest {
414     void fun(T)(T t) {
415         t.foobar(2, "quux");
416     }
417 
418     auto m = mockStruct;
419     m.expect!"foobar"(2, "quux");
420     fun(m);
421     m.verify;
422 }
423 
424 @("mock struct values negative")
425 @safe pure unittest {
426     void fun(T)(T t) {
427         t.foobar(2, "quux");
428     }
429 
430     auto m = mockStruct;
431     m.expect!"foobar"(3, "quux");
432     fun(m);
433     assertExceptionMsg(m.verify,
434                        "    source/unit_threaded/mock.d:123 - foobar was called with unexpected Tuple!(int, string)(2, \"quux\")\n" ~
435                        "    source/unit_threaded/mock.d:123 -           instead of the expected Tuple!(int, string)(3, \"quux\")");
436 }
437 
438 
439 @("struct return value")
440 @safe pure unittest {
441     int fun(T)(T f) {
442         return f.timesN(3) * 2;
443     }
444 
445     auto m = mockStruct(42, 12);
446     fun(m).shouldEqual(84);
447     fun(m).shouldEqual(24);
448     fun(m).shouldEqual(0);
449     m.expectCalled!"timesN";
450 }
451 
452 @("struct expectCalled")
453 @safe pure unittest {
454     void fun(T)(T t) {
455         t.foobar(2, "quux");
456     }
457 
458     auto m = mockStruct;
459     fun(m);
460     m.expectCalled!"foobar"(2, "quux");
461 }
462 
463 
464 @("const(ubyte)[] return type]")
465 @safe pure unittest {
466     interface Interface {
467         const(ubyte)[] fun();
468     }
469 
470     auto m = mock!Interface;
471 }
472 
473 @("safe pure nothrow")
474 @safe pure unittest {
475     interface Interface {
476         int twice(int i) @safe pure nothrow /*@nogc*/;
477     }
478     auto m = mock!Interface;
479 }