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