1 /*
2  *             Copyright Andrej Mitrovic 2013.
3  *  Distributed under the Boost Software License, Version 1.0.
4  *     (See accompanying file LICENSE_1_0.txt or copy at
5  *           http://www.boost.org/LICENSE_1_0.txt)
6  */
7 module dchip.util;
8 
9 import core.exception;
10 import std.traits;
11 import std.typetuple;
12 
13 /**
14     See: https://github.com/slembcke/Chipmunk2D/issues/56
15 
16     For two function pointers to be safely casted:
17     Either return type and parameters must match perfectly,
18     or they have to be pointers.
19 */
20 T safeCast(T, S)(S s)
21 {
22     static if (is(Unqual!T == Unqual!S))
23         return cast(T)s;
24     else
25     static if (isSomeFunction!T && isSomeFunction!S)
26     {
27         alias TParams = ParameterTypeTuple!T;
28         alias SParams = ParameterTypeTuple!S;
29 
30         alias TReturn = ReturnType!T;
31         alias SReturn = ReturnType!S;
32 
33         static assert(is(TReturn == SReturn) || isPointer!TReturn && isPointer!SReturn);
34 
35         static assert(TParams.length == SParams.length);
36 
37         static if (is(TParams == SParams))
38         {
39             return cast(T)s;
40         }
41         else
42         {
43             foreach (IDX, _; SParams)
44             {
45                 static assert(is(SParams[IDX] == TParams[IDX])
46                               || isPointer!(SParams[IDX]) && isPointer!(TParams[IDX]));
47             }
48 
49             return cast(T)s;
50         }
51     }
52     else
53     static if (isFloatingPoint!T && isFloatingPoint!S)
54         return cast(T)s;
55     else
56         static assert(0);
57 }
58 
59 /**
60     Return the exception of type $(D Exc) that is
61     expected to be thrown when $(D expr) is evaluated.
62 
63     This is useful to verify the custom exception type
64     holds some interesting state.
65 
66     If no exception is thrown, then a new exception
67     is thrown to notify the user of the missing exception.
68 */
69 Exc getException(Exc, E)(lazy E expr, string file = __FILE__, size_t line = __LINE__)
70 {
71     try
72     {
73         expr();
74         throw new Exception("Error: No exception was thrown.", file, line);
75     }
76     catch (Exc e)
77     {
78         return e;
79     }
80 }
81 
82 ///
83 version (CHIP_ENABLE_UNITTESTS)
84 unittest
85 {
86     assert({ throw new Exception("my message"); }().getException!Exception.msg == "my message");
87 
88     static class MyExc : Exception
89     {
90         this(string file)
91         {
92             this.file = file;
93             super("");
94         }
95 
96         string file;
97     }
98 
99     assert({ throw new MyExc("file.txt"); }().getException!MyExc.file == "file.txt");
100 
101     try
102     {
103         assert(getException!MyExc({ }()).file == "file.txt");
104     }
105     catch (Exception exc)
106     {
107         assert(exc.msg == "Error: No exception was thrown.");
108     }
109 }
110 
111 /**
112     Return the exception message of an exception.
113     If no exception was thrown, then a new exception
114     is thrown to notify the user of the missing exception.
115 */
116 string getExceptionMsg(E)(lazy E expr, string file = __FILE__, size_t line = __LINE__)
117 {
118     import std.exception : collectExceptionMsg;
119 
120     auto result = collectExceptionMsg!Throwable(expr);
121 
122     if (result is null)
123         throw new Exception("Error: No exception was thrown.", file, line);
124 
125     return result;
126 }
127 
128 ///
129 version (CHIP_ENABLE_UNITTESTS)
130 unittest
131 {
132     assert(getExceptionMsg({ throw new Exception("my message"); }()) == "my message");
133     assert(getExceptionMsg({ }()).getExceptionMsg == "Error: No exception was thrown.");
134 }
135 
136 /** Verify that calling $(D expr) throws and contains the exception message $(D msg). */
137 void assertErrorsWith(E)(lazy E expr, string msg, string file = __FILE__, size_t line = __LINE__)
138 {
139     try
140     {
141         expr.getExceptionMsg.assertEqual(msg);
142     }
143     catch (AssertError ae)
144     {
145         ae.file = file;
146         ae.line = line;
147         throw ae;
148     }
149 }
150 
151 ///
152 version (CHIP_ENABLE_UNITTESTS)
153 unittest
154 {
155     require(1 == 2).assertErrorsWith("requirement failed.");
156     require(1 == 2, "%s is not true").assertErrorsWith("%s is not true");
157     require(1 == 2, "%s is not true", "1 == 2").assertErrorsWith("1 == 2 is not true");
158 
159     require(1 == 1).assertErrorsWith("requirement failed.")
160                    .assertErrorsWith("Error: No exception was thrown.");
161 }
162 
163 /**
164     Similar to $(D enforce), except it can take a formatting string as the second argument.
165     $(B Note:) Until Issue 8687 is fixed, $(D file) and $(D line) have to be compile-time
166     arguments, which might create template bloat.
167 */
168 T require(string file = __FILE__, size_t line = __LINE__, T, Args...)
169     (T value, Args args)
170 {
171     if (value)
172         return value;
173 
174     static if (Args.length)
175     {
176         static if (Args.length > 1)
177         {
178             import std..string : format;
179             string msg = format(args[0], args[1 .. $]);
180         }
181         else
182         {
183             import std.conv : text;
184             string msg = text(args);
185         }
186     }
187     else
188         enum msg = "requirement failed.";
189 
190     throw new Exception(msg, file, line);
191 }
192 
193 ///
194 version (CHIP_ENABLE_UNITTESTS)
195 unittest
196 {
197     require(1 == 2).getExceptionMsg.assertEqual("requirement failed.");
198     require(1 == 2, "%s is not true").getExceptionMsg.assertEqual("%s is not true");
199     require(1 == 2, "%s is not true", "1 == 2").getExceptionMsg.assertEqual("1 == 2 is not true");
200 }
201 
202 template assertEquality(bool checkEqual)
203 {
204     import std..string : format;
205 
206     void assertEquality(T1, T2)(T1 lhs, T2 rhs, string file = __FILE__, size_t line = __LINE__)
207         //~ if (is(typeof(lhs == rhs) : bool))  // note: errors are better without this
208     {
209         static if (is(typeof(lhs == rhs) : bool))
210             enum string compare = "lhs == rhs";
211         else
212         static if (is(typeof(equal(lhs, rhs)) : bool))
213             enum string compare = "equal(lhs, rhs)";  // std.algorithm for ranges
214         else
215             static assert(0, format("lhs type '%s' cannot be compared against rhs type '%s'",
216                 __traits(identifier, T1), __traits(identifier, T2)));
217 
218         mixin(format(q{
219             if (%s(%s))
220                 throw new AssertError(
221                     format("(%%s %%s %%s) failed.", lhs.enquote, checkEqual ? "==" : "!=", rhs.enquote),
222                     file, line);
223         }, checkEqual ? "!" : "", compare));
224     }
225 }
226 
227 /**
228     An overload of $(D enforceEx) which allows constructing the exception with the arguments its ctor supports.
229     The ctor's last parameters must be a string (file) and size_t (line).
230 */
231 template enforceEx(E)
232 {
233     T enforceEx(T, string file = __FILE__, size_t line = __LINE__, Args...)(T value, Args args)
234         if (is(typeof(new E(args, file, line))))
235     {
236         if (!value) throw new E(args, file, line);
237         return value;
238     }
239 }
240 
241 ///
242 version (CHIP_ENABLE_UNITTESTS)
243 unittest
244 {
245     static class Exc : Exception
246     {
247         this(int x, string file, int line)
248         {
249             super("", file, line);
250             this.x = x;
251         }
252 
253         int x;
254     }
255 
256     try
257     {
258         enforceEx!Exc(false, 1);
259         assert(0);
260     }
261     catch (Exc ex)
262     {
263         assert(ex.x == 1);
264     }
265 }
266 
267 /** Unittest functions which give out a message with the failing expression. */
268 alias assertEqual = assertEquality!true;
269 
270 /** Common mispelling. */
271 alias assertEquals = assertEqual;
272 
273 /// Ditto
274 alias assertNotEqual = assertEquality!false;
275 
276 ///
277 version (CHIP_ENABLE_UNITTESTS)
278 unittest
279 {
280     assertEqual(1, 1);
281     assertNotEqual(1, 2);
282 
283     assert(assertEqual("foo", "bar").getExceptionMsg == `("foo" == "bar") failed.`);
284     assert(assertNotEqual(1, 1).getExceptionMsg == "(1 != 1) failed.");
285 
286     int x;
287     int[] y;
288     static assert(!__traits(compiles, x.assertEqual(y)));
289 }
290 
291 template assertProp(string prop, bool state)
292 {
293     void assertProp(T)(T arg, string file = __FILE__, size_t line = __LINE__)
294     {
295         import std..string : format;
296 
297         mixin(format(q{
298             if (%sarg.%s)
299             {
300                 throw new AssertError(
301                     format(".%%s is %%s : %%s", prop, !state, arg), file, line);
302             }
303         }, state ? "!" : "", prop));
304     }
305 }
306 
307 /// Assert range isn't empty.
308 alias assertEmpty = assertProp!("empty", true);
309 
310 /// Ditto
311 alias assertNotEmpty = assertProp!("empty", false);
312 
313 ///
314 version (CHIP_ENABLE_UNITTESTS)
315 unittest
316 {
317     // Issue 9588 - format prints context pointer for struct
318     static struct S { int x; bool empty() { return x == 0; } }
319 
320     S s = S(1);
321     assert(assertEmpty(s).getExceptionMsg == ".empty is false : S(1)");
322 
323     s.x = 0;
324     assertEmpty(s);
325 
326     assert(assertNotEmpty(s).getExceptionMsg == ".empty is true : S(0)");
327     s.x = 1;
328     assertNotEmpty(s);
329 }
330 
331 /// Useful template to generate an assert check function
332 template assertOp(string op)
333 {
334     void assertOp(T1, T2)(T1 lhs, T2 rhs,
335                           string file = __FILE__,
336                           size_t line = __LINE__)
337     {
338         import std..string : format;
339 
340         string msg = format("(%s %s %s) failed.", lhs, op, rhs);
341 
342         mixin(format(q{
343             if (!(lhs %s rhs)) throw new AssertError(msg, file, line);
344         }, op));
345     }
346 }
347 
348 ///
349 version (CHIP_ENABLE_UNITTESTS)
350 unittest
351 {
352     alias assertEqual = assertOp!"==";
353     alias assertNotEqual = assertOp!"!=";
354     alias assertGreaterThan = assertOp!">";
355     alias assertGreaterThanOrEqual = assertOp!">=";
356 
357     assertEqual(1, 1);
358     assertNotEqual(1, 2);
359     assertGreaterThan(2, 1);
360     assertGreaterThanOrEqual(2, 2);
361 }
362 
363 /**
364     Return string representation of argument.
365     If argument is already a string or a
366     character, enquote it to make it more readable.
367 */
368 string enquote(T)(T arg)
369 {
370     import std.conv : to;
371     import std.range : isInputRange, ElementEncodingType;
372     import std..string : format;
373     import std.traits : isSomeString, isSomeChar;
374 
375     static if (isSomeString!T)
376         return format(`"%s"`, arg);
377     else
378     static if (isSomeChar!T)
379         return format("'%s'", arg);
380     else
381     static if (isInputRange!T && is(ElementEncodingType!T == dchar))
382         return format(`"%s"`, to!string(arg));
383     else
384         return to!string(arg);
385 }
386 
387 ///
388 version (CHIP_ENABLE_UNITTESTS)
389 unittest
390 {
391     assert(enquote(0) == "0");
392     assert(enquote(enquote(0)) == `"0"`);
393     assert(enquote("foo") == `"foo"`);
394     assert(enquote('a') == "'a'");
395 }
396 
397 /**
398     Export all enum members as aliases. This allows enums to be used as types
399     and allows its members to be used as if they're defined in module scope.
400 */
401 package mixin template _ExportEnumMembers(E) if (is(E == enum))
402 {
403     mixin(_makeEnumAliases!(E)());
404 }
405 
406 /// ditto
407 package string _makeEnumAliases(E)() if (is(E == enum))
408 {
409     import std.array;
410     import std..string;
411 
412     enum enumName = __traits(identifier, E);
413     Appender!(string[]) result;
414 
415     foreach (string member; __traits(allMembers, E))
416         result ~= format("alias %s = %s.%s;", member, enumName, member);
417 
418     return result.data.join("\n");
419 }
420 
421 ///
422 version (CHIP_ENABLE_UNITTESTS)
423 unittest
424 {
425     enum enum_type_t
426     {
427         foo,
428         bar,
429     }
430 
431     mixin _ExportEnumMembers!enum_type_t;
432 
433     enum_type_t e1 = enum_type_t.foo;  // ok
434     enum_type_t e2 = bar;    // ok
435 }