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