1 /++ DES TestSuite
2  +
3  + Minimal test suite for easy unittesting
4  +/
5 module des.ts;
6 
7 import std.traits;
8 import std.typetuple;
9 import std.math;
10 
11 import std.stdio;
12 import std.string;
13 import std.exception;
14 import std.conv : to;
15 import std.format : FormatException;
16 import core.exception : AssertError;
17 
18 private
19 {
20     debug enum __DEBUG__ = true;
21     else  enum __DEBUG__ = false;
22 
23     version(unittest) enum __UNITTEST__ = true;
24     else              enum __UNITTEST__ = false;
25 
26     version( des_ts_always_assert )
27         enum __ALWAYS_ASSERT__ = true;
28     else
29     {
30         enum __ALWAYS_ASSERT__ = false;
31 
32         static if( !__DEBUG__ && !__UNITTEST__ )
33             pragma(msg, "## Warning: des.ts not use asserts, use 'version=des_ts_always_assert'");
34     }
35 
36     enum __USE_ASSERT__ = __UNITTEST__ || __DEBUG__ || __ALWAYS_ASSERT__;
37 }
38 
39 /// check equals `a` and `b`
40 bool eq(A,B)( in A a, in B b ) pure
41 {
42     static if( allSatisfy!(isLikeArray,A,B) )
43     {
44         if( a.length != b.length ) return false;
45 
46         foreach( i; 0 .. a.length )
47             if( !eq( a[i], b[i] ) ) return false;
48 
49         return true;
50     }
51     else static if( isSomeObject!A && isSomeObject!B ) return a is b;
52     else static if( allSatisfy!(isNumeric,A,B) && anySatisfy!(isFloatingPoint,A,B) )
53     {
54         static if( isFloatingPoint!A && isFloatingPoint!B )
55             auto epsilon = fmax( A.epsilon, B.epsilon );
56         else static if( isFloatingPoint!A ) auto epsilon = A.epsilon;
57         else static if( isFloatingPoint!B ) auto epsilon = B.epsilon;
58         else static assert(0, "WTF? not A nor B isn't floating point" );
59 
60         return abs( a - b ) < epsilon;
61     }
62     else return a == b;
63 }
64 
65 ///
66 unittest
67 {
68     assert(  eq( 1, 1.0 ) );
69     assert(  eq( "hello", "hello"w ) );
70     assert( !eq( cast(void[])"hello", cast(void[])"hello"w ) );
71     assert(  eq( cast(void[])"hello", cast(void[])"hello" ) );
72     assert(  eq( cast(void[])"hello", "hello" ) );
73     assert( !eq( cast(void[])"hello", "hello"w ) );
74     assert(  eq( [[1,2],[3,4]], [[1.0f,2],[3.0f,4]] ) );
75     assert( !eq( [[1,2],[3,4]], [[1.1f,2],[3.0f,4]] ) );
76     assert( !eq( [[1,2],[3,4]], [[1.0f,2],[3.0f]] ) );
77     assert(  eq( [1,2,3], [1.0,2,3] ) );
78     assert(  eq( [1.0f,2,3], [1.0,2,3] ) );
79     assert(  eq( [1,2,3], [1,2,3] ) );
80     assert( !eq( [1.0000001,2,3], [1,2,3] ) );
81     assert(  eq( ["hello","world"], ["hello","world"] ) );
82     assert( !eq( "hello", [1,2,3] ) );
83     static assert( !__traits(compiles, eq(["hello"],1)) );
84     static assert( !__traits(compiles, eq(["hello"],[1,2,3])) );
85 }
86 
87 /++ check equals `a` and `b` approx with epsilon
88  + Params:
89  +
90  + a = first value
91  + b = second value
92  + eps = numeric epsilon
93  +/
94 bool eq_approx(A,B,E)( in A a, in B b, in E eps ) pure
95     if( isNumeric!E && ( allSatisfy!(isNumeric,A,B) || allSatisfy!(isLikeArray,A,B) ) )
96 {
97     static if( allSatisfy!(isLikeArray,A,B) )
98     {
99         if( a.length != b.length ) return false;
100         foreach( i; 0 .. a.length )
101             if( !eq_approx( a[i], b[i], eps ) ) return false;
102         return true;
103     }
104     else return abs( a - b ) < eps;
105 }
106 
107 ///
108 unittest
109 {
110     assert(  eq_approx( [1.1f,2,3], [1,2,3], 0.2 ) );
111     assert( !eq_approx( [1.1f,2,3], [1,2,3], 0.1 ) );
112     assert( !eq_approx( [1.0f,2], [1,2,3], 1 ) );
113 }
114 
115 private template isLikeArray(T)
116 {
117     enum isLikeArray = !is( Unqual!T == void[] ) &&
118                           is( typeof(T.init[0]) ) &&
119                          !is( Unqual!(typeof(T.init[0])) == void ) &&
120                           is( typeof( T.init.length ) == size_t );
121 }
122 
123 unittest
124 {
125     static assert(  isLikeArray!(int[]) );
126     static assert(  isLikeArray!(float[]) );
127     static assert(  isLikeArray!(string) );
128     static assert( !isLikeArray!int );
129     static assert( !isLikeArray!float );
130     static assert( !isLikeArray!(immutable(void)[]) );
131 }
132 
133 private template isSomeObject(T)
134 {
135     enum isSomeObject = is( T == class ) || is( T == interface );
136 }
137 
138 /++ try call delegate
139  +
140  + Params:
141  +
142  + fnc = called delegate
143  + throw_unexpected = if `true` when catch exception with type != `E` throw it out, if `false` ignore it
144  +
145  + Returns:
146  + `true` if is catched exception of type `E`, `false` otherwise
147  +/
148 bool mustExcept(E:Throwable=Exception)( void delegate() fnc, bool throw_unexpected=true )
149 in { assert( fnc !is null, "delegate is null" ); } body
150 {
151     static if( !is( E == Throwable ) )
152     {
153         try fnc();
154         catch( E e ) return true;
155         catch( Throwable t )
156             if( throw_unexpected ) throw t;
157         return false;
158     }
159     else
160     {
161         try fnc();
162         catch( Throwable t ) return true;
163         return false;
164     }
165 }
166 
167 ///
168 unittest
169 {
170     assert(  mustExcept!Exception( { throw new Exception("test"); } ) );
171     assert( !mustExcept!Exception( { throw new Throwable("test"); }, false ) );
172     assert(  mustExcept( { throw new Exception("test"); } ) );
173     assert( !mustExcept( { throw new Throwable("test"); }, false ) );
174     assert(  mustExcept!Throwable( { throw new Exception("test"); } ) );
175     assert(  mustExcept!Throwable( { throw new Throwable("test"); } ) );
176     assert( !mustExcept!Exception({ auto a = 4; }) );
177 }
178 
179 ///
180 unittest
181 {
182     static class A {}
183     static assert( !__traits(compiles, mustExcept!A({})) );
184 
185     static class TestExceptionA : Exception
186     { this() @safe pure nothrow { super( "" ); } }
187     static class TestExceptionB : Exception
188     { this() @safe pure nothrow { super( "" ); } }
189     static class TestExceptionC : TestExceptionA
190     { this() @safe pure nothrow { super(); } }
191 
192     assert(  mustExcept!Exception({ throw new TestExceptionA; }) );
193     assert(  mustExcept!Exception({ throw new TestExceptionB; }) );
194 
195     assert(  mustExcept!TestExceptionA({ throw new TestExceptionA; }) );
196     assert(  mustExcept!TestExceptionA({ throw new TestExceptionC; }) );
197     assert(  mustExcept!TestExceptionB({ throw new TestExceptionB; }) );
198 
199     assert( !mustExcept!TestExceptionB( { throw new TestExceptionA; }, false ) );
200     assert( !mustExcept!TestExceptionA( { throw new TestExceptionB; }, false ) );
201 
202     auto test_b_catched = false;
203     try mustExcept!TestExceptionA({ throw new TestExceptionB; });
204     catch( TestExceptionB ) test_b_catched = true;
205     assert( test_b_catched );
206 }
207 
208 /++ throws `AssertError` if `!eq( a, b )`
209  +
210  + Params:
211  +
212  + a = first value
213  + b = second value
214  + fmt = error message format, must have two string places `'%s'` for `a` and `b`
215  +/
216 void assertEq(A,B,string file=__FILE__,size_t line=__LINE__)( in A a, in B b, lazy string fmt=null )
217 if( is( typeof( eq(a,b) ) ) )
218 {
219     static if( __USE_ASSERT__ )
220         enforce( eq( a, b ), newError( file, line,
221                     ( fmt.length > 0 ? fmt : "assertEq fails: %s != %s" ),
222                     toStringForce(a), toStringForce(b) ) );
223 }
224 
225 ///
226 unittest
227 {
228     assert(  mustExcept!AssertError({ assertEq( 1, 2 ); }) );
229     assert(  mustExcept!AssertError({ assertEq( [1,2], [2,3] ); }) );
230     assert( !mustExcept!AssertError({ assertEq( [1,2], [1,2] ); }) );
231 }
232 
233 /++ throws `AssertError` if `eq( a, b )`
234  +
235  + Params:
236  +
237  + a = first value
238  + b = second value
239  + fmt = error message format, must have two string places `'%s'` for `a` and `b`
240  +/
241 void assertNotEq(A,B,string file=__FILE__,size_t line=__LINE__)( in A a, in B b, lazy string fmt=null )
242 if( is( typeof( eq(a,b) ) ) )
243 {
244     static if( __USE_ASSERT__ )
245         enforce( !eq( a, b ), newError( file, line,
246                     ( fmt.length > 0 ? fmt : "assertNotEq fails: %s == %s" ),
247                     toStringForce(a), toStringForce(b) ) );
248 }
249 
250 /++ throws `AssertError` if `a !is null`
251  +
252  + Params:
253  +
254  + a = value
255  + fmt = error message format, must have one string place `'%s'` for `a`
256  +/
257 void assertNull(A,string file=__FILE__,size_t line=__LINE__)( in A a, lazy string fmt=null )
258 {
259     static if( __USE_ASSERT__ )
260         enforce( a is null, newError( file, line,
261                     ( fmt.length > 0 ? fmt : "assertNull fails: %s !is null" ),
262                     toStringForce(a) ) );
263 }
264 
265 /++ throws `AssertError` if `a is null`
266  +
267  + Params:
268  +
269  + a = value
270  + fmt = error message format, must have one string place `'%s'` for `a`
271  +/
272 void assertNotNull(A,string file=__FILE__,size_t line=__LINE__)( in A a, lazy string fmt=null )
273 {
274     static if( __USE_ASSERT__ )
275         enforce( a !is null, newError( file, line,
276                     ( fmt.length > 0 ? fmt : "assertNotNull fails: %s is null" ),
277                     toStringForce(a) ) );
278 }
279 
280 /+ not pure because using stderr and toStringForce isn't pure +/
281 auto newError(Args...)( string file, size_t line, string fmt, Args args )
282 {
283     string msg;
284     try msg = format( fmt, args );
285     catch( Exception )
286     {
287         stderr.writefln( "bad error format: '%s' (%s:%d)", fmt, file, line );
288         msg = toStringForce( args );
289     }
290     return new AssertError( msg, file, line );
291 }
292 
293 /+ not pure because to!string isn't pure +/
294 string toStringForce(Args...)( in Args args )
295 {
296     static if( Args.length == 1 )
297     {
298         alias T = Args[0];
299         auto val = args[0];
300 
301         static if( is( typeof( to!string( val ) )) )
302             return to!string( val );
303         else static if( isLikeArray!T )
304         {
305             string[] rr;
306             foreach( i; 0 .. val.length )
307                 rr ~= toStringForce( val[i] );
308             return "[ " ~ rr.join(", ") ~ " ]";
309         }
310         else static if( isSomeObject!T )
311         {
312             if( val is null ) return "null";
313             else return to!string( cast(void*)val );
314         }
315         else static if( is( T == typeof(null) ) ) return null;
316         else return val.stringof;
317     }
318     else return toStringForce( args[0] ) ~ ", " ~ toStringForce( args[1..$] );
319 }
320 
321 unittest
322 {
323     assert( eq( toStringForce([0,4]), "[0, 4]" ) );
324     assert( eq( toStringForce(null), "null" ) );
325     assert( eq( toStringForce(0), "0" ) );
326 
327     Object a = null;
328 
329     assert( eq( toStringForce(a), "null" ) );
330     assert( eq( toStringForce(a,5), "null, 5" ) );
331 
332     assert( eq( to!string(new Object), "object.Object" ) );
333     assert( !eq( toStringForce(new Object), "object.Object" ) );
334 }