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