1 /******************************************************************************
2 
3   Defines base exception class thrown by test checks and set of helper
4   functions to define actual test cases. These helpers are supposed to be
5   used in unittest blocks instead of asserts.
6 
7   There were three reasons why dedicated function got introduced:
8 
9   1) Bultin `assert` throws an `Error`, which makes implementing test runner
10      that doesn't stop on first failure illegal by language specification.
11   2) These `test` functions can provide more informational formatting compared
12      to plain `assert`, for example `test!("==")(a, b)` will print `a` and `b`
13      values on failure.
14   3) Having dedicated exception type for test failures makes possible to
15      distinguish in test runners between contract failures and test failures.
16 
17   Copyright:
18       Copyright (c) 2009-2016 dunnhumby Germany GmbH.
19       All rights reserved.
20 
21   License:
22       Boost Software License Version 1.0. See LICENSE_BOOST.txt for details.
23       Alternatively, this file may be distributed under the terms of the Tango
24       3-Clause BSD License (see LICENSE_BSD.txt for details).
25 
26 *******************************************************************************/
27 
28 module ocean.core.Test;
29 
30 
31 import ocean.transition;
32 
33 import core.memory;
34 import ocean.core.Enforce;
35 import ocean.text.convert.Formatter;
36 
37 /******************************************************************************
38 
39     Exception class to be thrown from unot tests blocks.
40 
41 *******************************************************************************/
42 
43 class TestException : Exception
44 {
45     /***************************************************************************
46 
47       wraps parent constructor
48 
49      ***************************************************************************/
50 
51     public this ( string msg, string file = __FILE__, int line = __LINE__ )
52     {
53         super( msg, file, line );
54     }
55 }
56 
57 /******************************************************************************
58 
59     Effectively partial specialization alias:
60         test = enforceImpl!(TestException)
61 
62     Same arguments as enforceImpl.
63 
64 ******************************************************************************/
65 
66 public void test ( T ) ( T ok, cstring msg = "",
67     string file = __FILE__, int line = __LINE__ )
68 {
69     if (!msg.length)
70     {
71         msg = "unit test has failed";
72     }
73     enforceImpl!(TestException, T)(ok, idup(msg), file, line);
74 }
75 
76 /******************************************************************************
77 
78     ditto
79 
80 ******************************************************************************/
81 
82 public void test ( string op, T1, T2 ) ( T1 a,
83     T2 b, string file = __FILE__, int line = __LINE__ )
84 {
85     enforceImpl!(op, TestException)(a, b, file, line);
86 }
87 
88 unittest
89 {
90     try
91     {
92         test(false);
93         assert(false);
94     }
95     catch (TestException e)
96     {
97         assert(e.message() == "unit test has failed");
98         assert(e.line == __LINE__ - 6);
99     }
100 
101     try
102     {
103         test!("==")(2, 3);
104         assert(false);
105     }
106     catch (TestException e)
107     {
108         assert(e.message() == "expression '2 == 3' evaluates to false");
109         assert(e.line == __LINE__ - 6);
110     }
111 }
112 
113 /******************************************************************************
114 
115     Verifies that given expression throws exception instance of expected type.
116 
117     Params:
118         E = exception type to expect, Exception by default
119         expr = expression that is expected to throw during evaluation
120         strict = if 'true', accepts only exact exception type, disallowing
121             polymorphic conversion
122         file = file of origin
123         line = line of origin
124 
125     Throws:
126         `TestException` if nothing has been thrown from `expr`
127         Propagates any thrown exception which is not `E`
128         In strict mode (default) also propagates any children of E (disables
129         polymorphic catching)
130 
131 ******************************************************************************/
132 
133 public void testThrown ( E : Exception = Exception ) ( lazy void expr,
134     bool strict = true, string file = __FILE__, int line = __LINE__ )
135 {
136     bool was_thrown = false;
137     try
138     {
139         expr;
140     }
141     catch (E e)
142     {
143         if (strict)
144         {
145             if (E.classinfo == e.classinfo)
146             {
147                 was_thrown = true;
148             }
149             else
150             {
151                 throw e;
152             }
153         }
154         else
155         {
156             was_thrown = true;
157         }
158     }
159 
160     if (!was_thrown)
161     {
162         throw new TestException(
163             "Expected '" ~ E.stringof ~ "' to be thrown, but it wasn't",
164             file,
165             line
166         );
167     }
168 }
169 
170 unittest
171 {
172     void foo() { throw new Exception(""); }
173     testThrown(foo());
174 
175     void test_foo() { throw new TestException("", "", 0); }
176     testThrown!(TestException)(test_foo());
177 
178     // make sure only exact exception type is caught
179     testThrown!(TestException)(
180         testThrown!(Exception)(test_foo())
181     );
182 
183     // .. unless strict matching is disabled
184     testThrown!(Exception)(test_foo(), false);
185 }
186 
187 /******************************************************************************
188 
189     Utility class useful in scenarios where actual testing code is reused in
190     different contexts and file+line information is not enough to uniquely
191     identify failed case.
192 
193     NamedTest is also exception class on its own - when test condition fails
194     it throws itself.
195 
196 ******************************************************************************/
197 
198 class NamedTest : TestException
199 {
200     /***************************************************************************
201 
202       Field to store test name this check belongs to. Useful
203       when you have a common verification code reused by different test cases
204       and file+line is not enough for identification.
205 
206      ***************************************************************************/
207 
208     private string name;
209 
210     /**************************************************************************
211 
212         Constructor
213 
214     ***************************************************************************/
215 
216     this(string name)
217     {
218         super(null);
219         this.name = name;
220     }
221 
222     /***************************************************************************
223 
224       message that also uses this.name if present
225 
226     ****************************************************************************/
227 
228     static if (is(typeof(Throwable.message)))
229     {
230         public override cstring message () const @trusted nothrow
231         {
232             // The Formatter currently has no annotation, as it would require
233             // extensive work on it (and a new language feature),
234             // but we know it's neither throwing (it doesn't on its own),
235             // nor does it present a non-safe interface.
236             scope (failure) assert(0);
237 
238             if (this.name.length)
239             {
240                 return format("[{}] {}", this.name, this.msg);
241             }
242             else
243             {
244                 return format("{}", this.msg);
245             }
246         }
247     }
248 
249     /**************************************************************************
250 
251         Same as enforceImpl!(TestException) but uses this.name for error message
252         formatting.
253 
254     ***************************************************************************/
255 
256     public void test ( T ) ( T ok, cstring msg = "", string file = __FILE__,
257         int line = __LINE__ )
258     {
259         // uses `enforceImpl` instead of `test` so that pre-constructed
260         // exception instance can be used.
261         if (!msg.length)
262         {
263             msg = "unit test has failed";
264         }
265         enforceImpl(this, ok, idup(msg), file, line);
266     }
267 
268     /**************************************************************************
269 
270         Same as enforceImpl!(op, TestException) but uses this.name for error message
271         formatting.
272 
273     ***************************************************************************/
274 
275     public void test ( string op, T1, T2 ) ( T1 a, T2 b,
276         string file = __FILE__, int line = __LINE__ )
277     {
278         // uses `enforceImpl` instead of `test` so that pre-constructed
279         // exception instance can be used.
280         enforceImpl!(op)(this, a, b, file, line);
281     }
282 }
283 
284 unittest
285 {
286     auto t = new NamedTest("name");
287 
288     t.test(true);
289 
290     try
291     {
292         t.test(false);
293         assert(false);
294     }
295     catch (TestException e)
296     {
297         assert(e.message() == "[name] unit test has failed");
298     }
299 
300     try
301     {
302         t.test!(">")(2, 3);
303         assert(false);
304     }
305     catch (TestException e)
306     {
307         assert(e.message() == "[name] expression '2 > 3' evaluates to false");
308     }
309 }
310 
311 /******************************************************************************
312 
313     Verifies that call to `expr` does not allocate GC memory
314 
315     This is achieved by checking GC usage stats before and after the call.
316 
317     Params:
318         expr = any expression, wrapped in void-returning delegate if necessary
319         file = file where test is invoked
320         line = line where test is invoked
321 
322     Throws:
323         TestException if unexpected allocation happens
324 
325 ******************************************************************************/
326 
327 public void testNoAlloc ( lazy void expr, string file = __FILE__,
328     int line = __LINE__ )
329 {
330     auto before = GC.stats();
331     expr();
332     auto after = GC.stats();
333 
334     enforceImpl!(TestException, bool)(
335         before.usedSize == after.usedSize && before.freeSize == after.freeSize,
336         format("Expression expected to not allocate but GC usage stats have " ~
337                "changed from {} (used) / {} (free) to {} / {}",
338                before.usedSize, before.freeSize, after.usedSize, after.freeSize),
339         file,
340         line
341     );
342 }
343 
344 ///
345 unittest
346 {
347     testNoAlloc({} ());
348 
349     testThrown!(TestException)(
350         testNoAlloc({ auto x = new int; } ())
351     );
352 }
353 
354 unittest
355 {
356     auto t = new NamedTest("struct");
357 
358     struct S { int a; char[2] arr; }
359 
360     try
361     {
362         t.test!("==")(S(1, ['a', 'b']), S(2, ['c', 'd']));
363         assert(false);
364     }
365     catch (TestException e)
366     {
367         assert(e.message() == `[struct] expression '{ a: 1, arr: "ab" } == { a: 2, arr: "cd" }' evaluates to false`);
368     }
369 }
370 
371 unittest
372 {
373     auto t = new NamedTest("typedef");
374 
375     mixin(Typedef!(int, "MyInt"));
376 
377     try
378     {
379         t.test!("==")(cast(MyInt)10, cast(MyInt)20);
380         assert(false);
381     }
382     catch (TestException e)
383     {
384         assert(e.message() == `[typedef] expression '10 == 20' evaluates to false`);
385     }
386 }