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 ( istring msg, istring 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     istring 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 ( istring op, T1, T2 ) ( T1 a,
83     T2 b, istring 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, istring 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 istring name;
209 
210     /**************************************************************************
211 
212         Constructor
213 
214     ***************************************************************************/
215 
216     this(istring 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
231         {
232             if (this.name.length)
233             {
234                 return format("[{}] {}", this.name, this.msg);
235             }
236             else
237             {
238                 return format("{}", this.msg);
239             }
240         }
241     }
242 
243     /**************************************************************************
244 
245         Same as enforceImpl!(TestException) but uses this.name for error message
246         formatting.
247 
248     ***************************************************************************/
249 
250     public void test ( T ) ( T ok, cstring msg = "", istring file = __FILE__,
251         int line = __LINE__ )
252     {
253         // uses `enforceImpl` instead of `test` so that pre-constructed
254         // exception instance can be used.
255         if (!msg.length)
256         {
257             msg = "unit test has failed";
258         }
259         enforceImpl(this, ok, idup(msg), file, line);
260     }
261 
262     /**************************************************************************
263 
264         Same as enforceImpl!(op, TestException) but uses this.name for error message
265         formatting.
266 
267     ***************************************************************************/
268 
269     public void test ( istring op, T1, T2 ) ( T1 a, T2 b,
270         istring file = __FILE__, int line = __LINE__ )
271     {
272         // uses `enforceImpl` instead of `test` so that pre-constructed
273         // exception instance can be used.
274         enforceImpl!(op)(this, a, b, file, line);
275     }
276 }
277 
278 unittest
279 {
280     auto t = new NamedTest("name");
281 
282     t.test(true);
283 
284     try
285     {
286         t.test(false);
287         assert(false);
288     }
289     catch (TestException e)
290     {
291         assert(e.message() == "[name] unit test has failed");
292     }
293 
294     try
295     {
296         t.test!(">")(2, 3);
297         assert(false);
298     }
299     catch (TestException e)
300     {
301         assert(e.message() == "[name] expression '2 > 3' evaluates to false");
302     }
303 }
304 
305 /******************************************************************************
306 
307     Verifies that call to `expr` does not allocate GC memory
308 
309     This is achieved by checking GC usage stats before and after the call.
310 
311     Params:
312         expr = any expression, wrapped in void-returning delegate if necessary
313         file = file where test is invoked
314         line = line where test is invoked
315 
316     Throws:
317         TestException if unexpected allocation happens
318 
319 ******************************************************************************/
320 
321 public void testNoAlloc ( lazy void expr, istring file = __FILE__,
322     int line = __LINE__ )
323 {
324     size_t used1, free1;
325     ocean.transition.gc_usage(used1, free1);
326 
327     expr();
328 
329     size_t used2, free2;
330     ocean.transition.gc_usage(used2, free2);
331 
332     enforceImpl!(TestException, bool)(
333         used1 == used2 && free1 == free2,
334         format("Expression expected to not allocate but GC usage stats have " ~
335                "changed from {} (used) / {} (free) to {} / {}",
336                used1, free1, used2, free2),
337         file,
338         line
339     );
340 }
341 
342 ///
343 unittest
344 {
345     testNoAlloc({} ());
346 
347     testThrown!(TestException)(
348         testNoAlloc({ auto x = new int; } ())
349     );
350 }
351 
352 unittest
353 {
354     auto t = new NamedTest("struct");
355 
356     struct S { int a; char[2] arr; }
357 
358     try
359     {
360         t.test!("==")(S(1, ['a', 'b']), S(2, ['c', 'd']));
361         assert(false);
362     }
363     catch (TestException e)
364     {
365         assert(e.message() == `[struct] expression '{ a: 1, arr: "ab" } == { a: 2, arr: "cd" }' evaluates to false`);
366     }
367 }
368 
369 unittest
370 {
371     auto t = new NamedTest("typedef");
372 
373     mixin(Typedef!(int, "MyInt"));
374 
375     try
376     {
377         t.test!("==")(cast(MyInt)10, cast(MyInt)20);
378         assert(false);
379     }
380     catch (TestException e)
381     {
382         assert(e.message() == `[typedef] expression '10 == 20' evaluates to false`);
383     }
384 }