1 /**
2    This module is an attempt to alleviate compile times by including the bare
3    minimum. The idea is that while the reporting usually done by unit-threaded
4    is welcome, it only really matters when tests fail. Otherwise, no news is
5    good news.
6 
7    Likewise, naming and selecting tests are features used when certain tests
8    fail. The usual way to run tests is to run all of them and be happy if
9    they all pass.
10 
11    This module makes it so that unit-threaded gets out of the way, and if
12    needed the full features can be turned on at the cost of compiling
13    much more slowly.
14 
15    There aren't even any template constraints on the `should` functions
16    to avoid imports as much as possible.
17  */
18 module unit_threaded.light;
19 
20 alias UnitTestException = Exception;
21 
22 
23 /**
24    Dummy version so "normal" code compiles
25  */
26 mixin template runTestsMain(Modules...) if(Modules.length > 0) {
27     int main() {
28         import unit_threaded.light: runTestsImpl;
29         return runTestsImpl;
30     }
31 }
32 
33 /**
34    Dummy version of runTests so "normal" code compiles.
35  */
36 int runTests(T...)(in string[] args) {
37     return runTestsImpl;
38 }
39 
40 /// ditto
41 int runTests(T)(string[] args, T testData) {
42     return runTestsImpl;
43 }
44 
45 int runTestsImpl() {
46     import core.runtime: Runtime;
47     import core.stdc.stdio: printf;
48 
49     version(Posix)
50         printf("\033[32;1mOk\033[0;;m");
51     else
52         printf("Ok");
53 
54     printf(": All tests passed\n\n");
55 
56     return 0;
57 }
58 
59 
60 /**
61    Dummy version so "normal" code compiles
62  */
63 int[] allTestData(T...)() {
64     return [];
65 }
66 
67 /**
68    No-op version of writelnUt
69  */
70 void writelnUt(T...)(auto ref T args) {
71 
72 }
73 
74 /**
75    Same as unit_threaded.property.check
76  */
77 void check(alias F)(int numFuncCalls = 100,
78                     in string file = __FILE__, in size_t line = __LINE__) @trusted {
79     import unit_threaded.property: utCheck = check;
80     utCheck!F(numFuncCalls, file, line);
81 }
82 
83 /**
84    Same as unit_threaded.property.checkCustom
85  */
86 void checkCustom(alias Generator, alias Predicate)
87                 (int numFuncCalls = 100, in string file = __FILE__, in size_t line = __LINE__) @trusted {
88     import unit_threaded.property: utCheckCustom = checkCustom;
89     utCheckCustom!(Generator, Predicate)(numFuncCalls, file, line);
90 }
91 
92 
93 /**
94    Generic output interface
95  */
96 interface Output {
97     void send(in string output) @safe;
98     void flush() @safe;
99 }
100 
101 /**
102    Dummy version of unit_threaded.testcase.TestCase
103  */
104 class TestCase {
105     abstract void test();
106     void setup() {}
107     void shutdown() {}
108     static TestCase currentTest() { return new class TestCase { override void test() {}}; }
109     Output getWriter() { return new class Output { override void send(in string output) {} override void flush() {}}; }
110 }
111 
112 
113 /**
114    Same as unit_threaded.mock.mock
115  */
116 auto mock(T)() {
117     import unit_threaded.mock: utMock = mock;
118     return utMock!T;
119 }
120 
121 /**
122    Same as unit_threaded.mock.mockStruct
123  */
124 auto mockStruct(T...)(auto ref T returns) {
125     import unit_threaded.mock: utMockStruct = mockStruct;
126     return utMockStruct(returns);
127 }
128 
129 /**
130    Throw if condition is not true.
131  */
132 void shouldBeTrue(E)(lazy E condition, in string file = __FILE__, in size_t line = __LINE__) {
133     assert_(cast(bool)condition(), file, line);
134 }
135 
136 /// Throw if condition not false.
137 void shouldBeFalse(E)(lazy E condition, in string file = __FILE__, in size_t line = __LINE__) {
138     assert_(!cast(bool)condition(), file, line);
139 }
140 
141 /// Assert value is equal to expected
142 void shouldEqual(V, E)(auto ref V value, auto ref E expected, in string file = __FILE__, in size_t line = __LINE__) {
143 
144     void checkInputRange(T)(auto ref const(T) _) @trusted {
145         auto obj = cast(T)_;
146         bool e = obj.empty;
147         auto f = obj.front;
148         obj.popFront;
149     }
150     enum isInputRange(T) = is(T: Elt[], Elt) || is(typeof(checkInputRange(T.init)));
151 
152     static if(is(V == class)) {
153 
154         import unit_threaded.should: isEqual;
155         assert_(isEqual(value, expected), file, line);
156 
157     } else static if(isInputRange!V && isInputRange!E) {
158 
159         auto ref unqual(OriginalType)(auto ref OriginalType obj) @trusted {
160 
161             // copied from std.traits
162             template Unqual(T) {
163                      static if (is(T U ==          immutable U)) alias Unqual = U;
164                 else static if (is(T U == shared inout const U)) alias Unqual = U;
165                 else static if (is(T U == shared inout       U)) alias Unqual = U;
166                 else static if (is(T U == shared       const U)) alias Unqual = U;
167                 else static if (is(T U == shared             U)) alias Unqual = U;
168                 else static if (is(T U ==        inout const U)) alias Unqual = U;
169                 else static if (is(T U ==        inout       U)) alias Unqual = U;
170                 else static if (is(T U ==              const U)) alias Unqual = U;
171                 else                                             alias Unqual = T;
172             }
173 
174             static if(__traits(compiles, obj[])) {
175                 static if(!is(typeof(obj[]) == OriginalType)) {
176                     return unqual(obj[]);
177                 } else static if(__traits(compiles, cast(Unqual!OriginalType) obj)) {
178                     return cast(Unqual!OriginalType) obj;
179                 } else {
180                     return obj;
181                 }
182             } else  static if(__traits(compiles, cast(Unqual!OriginalType) obj)) {
183                 return cast(Unqual!OriginalType) obj;
184             } else
185                 return obj;
186         }
187 
188         auto ref unvoid(OriginalType)(auto ref OriginalType obj) @trusted {
189             static if(is(OriginalType == void[]))
190                 return cast(ubyte[]) obj;
191             else
192                 return obj;
193         }
194 
195         import std.algorithm: equal;
196         assert_(equal(unvoid(unqual(value)), unvoid(unqual(expected))), file, line);
197 
198     } else {
199         assert_(cast(const)value == cast(const)expected, file, line);
200     }
201 }
202 
203 
204 
205 /// Assert value is not equal to expected.
206 void shouldNotEqual(V, E)(in auto ref V value, in auto ref E expected, in string file = __FILE__, in size_t line = __LINE__) {
207     assert_(value != expected, file, line);
208 }
209 
210 /// Assert value is null.
211 void shouldBeNull(T)(in auto ref T value, in string file = __FILE__, in size_t line = __LINE__) {
212     assert_(value is null, file, line);
213 }
214 
215 /// Assert value is not null
216 void shouldNotBeNull(T)(in auto ref T value, in string file = __FILE__, in size_t line = __LINE__) {
217     assert_(value !is null, file, line);
218 }
219 
220 enum isLikeAssociativeArray(T, K) = is(typeof({
221     if(K.init in T) { }
222     if(K.init !in T) { }
223 }));
224 static assert(isLikeAssociativeArray!(string[string], string));
225 static assert(!isLikeAssociativeArray!(string[string], int));
226 
227 
228 /// Assert that value is in container.
229 void shouldBeIn(T, U)(in auto ref T value, in auto ref U container, in string file = __FILE__, in size_t line = __LINE__)
230     if(isLikeAssociativeArray!(U, T)) {
231     assert_(cast(bool)(value in container), file, line);
232 }
233 
234 /// ditto.
235 void shouldBeIn(T, U)(in auto ref T value, U container, in string file = __FILE__, in size_t line = __LINE__)
236     if (!isLikeAssociativeArray!(U, T))
237 {
238     import std.algorithm: find;
239     import std.array: empty;
240     assert_(!find(container, value).empty, file, line);
241 }
242 
243 /// Assert value is not in container.
244 void shouldNotBeIn(T, U)(in auto ref T value, in auto ref U container, in string file = __FILE__, in size_t line = __LINE__)
245     if(isLikeAssociativeArray!U) {
246     assert_(!cast(bool)(value in container), file, line);
247 }
248 
249 /// ditto.
250 void shouldNotBeIn(T, U)(in auto ref T value, U container, in string file = __FILE__, in size_t line = __LINE__)
251     if (!isLikeAssociativeArray!(U, T))
252 {
253     import std.algorithm: find;
254     import std.array: empty;
255     assert_(find(container, value).empty, file, line);
256 }
257 
258 /// Assert that expr throws.
259 void shouldThrow(T : Throwable = Exception, E)
260                 (lazy E expr, in string file = __FILE__, in size_t line = __LINE__) {
261     auto threw = false;
262     () @trusted {
263         try {
264             expr();
265         } catch(T _) {
266             threw = true;
267         }
268     }();
269     assert_(threw, file, line);
270 }
271 
272 /// Assert that expr throws an Exception that must have the type E, derived types won't do.
273 void shouldThrowExactly(T : Throwable = Exception, E)
274                        (lazy E expr, in string file = __FILE__, in size_t line = __LINE__)
275 {
276     T throwable = null;
277 
278     () @trusted {
279         try {
280             expr();
281             assert_(false, file, line);
282         } catch(T t) {
283             throwable = t;
284         }
285     }();
286 
287     //Object.opEquals is @system and impure
288     const sameType = () @trusted { return throwable !is null && typeid(throwable) == typeid(T); }();
289     assert_(sameType, file, line);
290 
291 }
292 
293 /// Assert that expr doesn't throw
294 void shouldNotThrow(T: Throwable = Exception, E)
295                    (lazy E expr, in string file = __FILE__, in size_t line = __LINE__) {
296     () @trusted {
297         try
298             expr();
299         catch(T _)
300             assert_(false, file, line);
301     }();
302 }
303 
304 /// Assert that expr throws and the exception message is msg.
305 void shouldThrowWithMessage(T : Throwable = Exception, E)(lazy E expr,
306                                                           string msg,
307                                                           string file = __FILE__,
308                                                           size_t line = __LINE__) {
309     T throwable = null;
310 
311     () @trusted {
312         try {
313             expr();
314         } catch(T ex) {
315             throwable = ex;
316         }
317     }();
318 
319     assert_(throwable !is null && throwable.msg == msg, file, line);
320 }
321 
322 /// Assert that value is approximately equal to expected.
323 void shouldApproxEqual(V, E)(in V value, in E expected, double maxRelDiff = 1e-2, double maxAbsDiff = 1e-5, string file = __FILE__, size_t line = __LINE__) {
324     import std.math: approxEqual;
325     assert_(approxEqual(value, expected, maxRelDiff, maxAbsDiff), file, line);
326 }
327 
328 /// assert that rng is empty.
329 void shouldBeEmpty(R)(in auto ref R rng, in string file = __FILE__, in size_t line = __LINE__) {
330     import std.range: isInputRange;
331     import std.traits: isAssociativeArray;
332     import std.array;
333 
334     static if(isInputRange!R)
335         assert_(rng.empty, file, line);
336     else static if(isAssociativeArray!R)
337         () @trusted { assert_(rng.keys.empty, file, line); }();
338     else
339         static assert(false, "Cannot call shouldBeEmpty on " ~ R.stringof);
340 }
341 
342 /// Assert that rng is not empty.
343 void shouldNotBeEmpty(R)(in auto ref R rng, in string file = __FILE__, in size_t line = __LINE__) {
344     import std.range: isInputRange;
345     import std.traits: isAssociativeArray;
346     import std.array;
347 
348     static if(isInputRange!R)
349         assert_(!rnd.empty, file, line);
350     else static if(isAssociativeArray!R)
351         () @trusted { assert_(!rng.keys.empty, file, line); }();
352     else
353         static assert(false, "Cannot call shouldBeEmpty on " ~ R.stringof);
354 }
355 
356 /// Assert that t should be greater than u.
357 void shouldBeGreaterThan(T, U)(in auto ref T t, in auto ref U u,
358                                in string file = __FILE__, in size_t line = __LINE__)
359 {
360     assert_(t > u, file, line);
361 }
362 
363 /// Assert that t should be smaller than u.
364 void shouldBeSmallerThan(T, U)(in auto ref T t, in auto ref U u,
365                                in string file = __FILE__, in size_t line = __LINE__)
366 {
367     assert_(t < u, file, line);
368 }
369 
370 /// Assert that value is the same set as expected (i.e. order doesn't matter)
371 void shouldBeSameSetAs(V, E)(in auto ref V value, in auto ref E expected, in string file = __FILE__, in size_t line = __LINE__) {
372     assert_(isSameSet(value, expected), file, line);
373 }
374 
375 /// Assert that value is not the same set as expected.
376 void shouldNotBeSameSetAs(V, E)(in auto ref V value, in auto ref E expected, in string file = __FILE__, in size_t line = __LINE__) {
377     assert_(!isSameSet(value, expected), file, line);
378 }
379 
380 private bool isSameSet(T, U)(in auto ref T t, in auto ref U u) {
381     import std.array: array;
382     import std.algorithm: canFind;
383 
384     //sort makes the element types have to implement opCmp
385     //instead, try one by one
386     auto ta = t.array;
387     auto ua = u.array;
388     if (ta.length != ua.length) return false;
389     foreach(element; ta)
390     {
391         if (!ua.canFind(element)) return false;
392     }
393 
394     return true;
395 }
396 
397 /// Assert that actual and expected represent the same JSON (i.e. formatting doesn't matter)
398 void shouldBeSameJsonAs(in string actual,
399                         in string expected,
400                         in string file = __FILE__,
401                         in size_t line = __LINE__)
402     @trusted // not @safe pure due to parseJSON
403 {
404     import std.json: parseJSON, JSONException;
405 
406     auto parse(in string str) {
407         try
408             return str.parseJSON;
409         catch(JSONException ex) {
410             assert_(false, "Failed to parse " ~ str, file, line);
411         }
412         assert(0);
413     }
414 
415     assert_(parse(actual) == parse(expected), file, line);
416 }
417 
418 
419 private void assert_(in bool value, in string file, in size_t line) @safe pure {
420     assert_(value, "Assertion failure", file, line);
421 }
422 
423 private void assert_(bool value, in string message, in string file, in size_t line) @trusted pure {
424     if(!value)
425         throw new Exception(message, file, line);
426 }
427 
428 void fail(in string output, in string file, in size_t line) @safe pure {
429     assert_(false, output, file, line);
430 }
431 
432 
433 auto should(E)(lazy E expr) {
434 
435     struct Should {
436 
437         bool opEquals(U)(auto ref U other,
438                          in string file = __FILE__,
439                          in size_t line = __LINE__)
440         {
441             expr.shouldEqual(other, file, line);
442             return true;
443         }
444     }
445 
446     return Should();
447 }