1 /++ DES TestSuite
2  +
3  + Minimal test suite for easy unittesting
4  +/
5 module des.ts;
6 
7 public import std.exception;
8 
9 import std.traits;
10 import std.meta;
11 import std.math;
12 import std.range;
13 import std.uni;
14 import std.string;
15 import std.stdio : stderr;
16 import std.conv : to;
17 
18 import core.exception : AssertError;
19 
20 /// check equals `a` and `b`
21 bool eq(A,B)( A a, B b )
22 {
23     alias isNum = std.traits.isNumeric;
24     static if( allSatisfy!(isElementArray,A,B) )
25     {
26         if( a.length != b.length ) return false;
27         foreach( i; 0 .. a.length )
28             if( !eq( a[i], b[i] ) ) return false;
29         return true;
30     }
31     else static if( allSatisfy!(isSomeString,A,B) )
32     {
33         if( a.walkLength != b.walkLength ) return false;
34         foreach( x,y; lockstep( a.byGrapheme, b.byGrapheme ) )
35             if( x != y ) return false;
36         return true;
37     }
38     else static if( isSomeObject!A && isSomeObject!B ) return a is b;
39     else static if( allSatisfy!(isNum,A,B) && anySatisfy!(isFloatingPoint,A,B) )
40     {
41         static if( isFloatingPoint!A && isFloatingPoint!B )
42             auto epsilon = fmax( A.epsilon, B.epsilon );
43         else static if( isFloatingPoint!A ) auto epsilon = A.epsilon;
44         else static if( isFloatingPoint!B ) auto epsilon = B.epsilon;
45         else static assert(0, "WTF? not A nor B isn't floating point" );
46 
47         return abs( a - b ) < epsilon;
48     }
49     else static if( is(typeof(a==b)) ) return a == b;
50     else static assert( 0, format( "uncompatible types '%s' and '%s'", nameOf!A, nameOf!B ) );
51 }
52 
53 ///
54 unittest
55 {
56     assert(  eq( 1, 1.0 ) );
57     assert(  eq( [1,2,3], [1.0,2,3] ) );
58     assert(  eq( [1.0f,2,3], [1.0,2,3] ) );
59     assert(  eq( [1,2,3], iota(1,4) ) );
60     assert( !eq( [1.0000001,2,3], [1,2,3] ) );
61     assert(  eq( [[1,2],[3,4]], [[1.0f,2],[3.0f,4]] ) );
62     assert( !eq( [[1,2],[3,4]], [[1.1f,2],[3.0f,4]] ) );
63     assert( !eq( [[1,2],[3,4]], [[1.0f,2],[3.0f]] ) );
64 }
65 
66 ///
67 unittest
68 {
69     // string from Jonathan M Davis presentation
70     auto s1 = `さいごの果実 / ミツバチと科学者`;
71     auto s2 = `さいごの果実 / ミツバチと科学者`w;
72 
73     assert( s1.length != s2.length );
74     assert( !eq( cast(void[])s1, cast(void[])s2 ) );
75     assert( eq( s1, s2 ) );
76 
77     auto s1a = `さいごの果実`;
78     auto s1b = `ミツバチと科学者`;
79 
80     auto s2a = `さいごの果実`w;
81     auto s2b = `ミツバチと科学者`w;
82 
83     assert( eq( [s1a,s1b], [s2a,s2b] ) );
84 
85     assert( !eq( "hello", [1,2,3] ) );
86     assert( eq( " "w, [ 32 ] ) );
87     static assert( !__traits(compiles, eq(["hello"],1)) );
88     static assert( !__traits(compiles, eq(["hello"],[1,2,3])) );
89 }
90 
91 /++ check equals `a` and `b` approx with epsilon
92  + Params:
93  +
94  + a = first value
95  + b = second value
96  + eps = numeric epsilon
97  +/
98 bool eq_approx(A,B,E)( A a, B b, E eps )
99     if( isNumeric!E )
100 {
101     static if( allSatisfy!(isElementArray,A,B) )
102     {
103         if( a.length != b.length ) return false;
104         foreach( i; 0 .. a.length )
105             if( !eq_approx( a[i], b[i], eps ) ) return false;
106         return true;
107     }
108     else static if( allSatisfy!(isNumeric,A,B) ) return abs( a - b ) < eps;
109     else static assert( 0, format( "uncompatible types '%s' and '%s'", nameOf!A, nameOf!B ) );
110 }
111 
112 ///
113 unittest
114 {
115     assert(  eq_approx( [1.1f,2,3], [1,2,3], 0.2 ) );
116     assert( !eq_approx( [1.1f,2,3], [1,2,3], 0.1 ) );
117     assert( !eq_approx( [1.0f,2], [1,2,3], 1 ) );
118     static assert( !__traits(compiles, eq_approx( [[1,2]], [1,2] )) );
119 }
120 
121 /++
122  +
123  + Params:
124  +
125  + a = first value
126  + b = second value
127  + fmt = error message format, must have two string places `'%s'` for `a` and `b`
128  +/
129 void assertEq(A,B)( A a, B b,
130         string fmt="assertEq fails: %s != %s",
131         string file=__FILE__, size_t line=__LINE__ )
132 {
133     if( eq( a, b ) ) return;
134     error( file, line, fmt, toStringForce(a), toStringForce(b) );
135 }
136 
137 /++ throws `AssertError` if `eq( a, b )`
138  +
139  + Params:
140  +
141  + a = first value
142  + b = second value
143  + fmt = error message format, must have two string places `'%s'` for `a` and `b`
144  +/
145 void assertNotEq(A,B)( A a, B b,
146         lazy string fmt="assertNotEq fails: %s == %s",
147         string file=__FILE__, size_t line=__LINE__ )
148 {
149     if( !eq( a, b ) ) return;
150     error( file, line, fmt, toStringForce(a), toStringForce(b) );
151 }
152 
153 /++ throws `AssertError` if `a !is null`
154  +
155  + Params:
156  +
157  + a = value
158  + fmt = error message format, must have one string place `'%s'` for `a`
159  +/
160 void assertNull(A)( A a,
161         lazy string fmt="assertNull fails: %s !is null",
162         string file=__FILE__, size_t line=__LINE__ )
163 {
164     if( a is null ) return;
165     error( file, line, fmt, toStringForce(a) );
166 }
167 
168 /++ throws `AssertError` if `a is null`
169  +
170  + Params:
171  +
172  + a = value
173  + fmt = error message (without format chars)
174  +/
175 void assertNotNull(A)( A a,
176         lazy string fmt="assertNotNull fails: value is null",
177         string file=__FILE__, size_t line=__LINE__ )
178 {
179     if( a !is null ) return;
180     error( file, line, fmt );
181 }
182 
183 /++ throws `AssertError` if `!eq_approx( a, b )`
184  +
185  + Params:
186  +
187  + a = first value
188  + b = second value
189  + eps = epsilon
190  + fmt = error message format, must have two string places `'%s'` for `a` and `b`
191  +/
192 void assertEqApprox(A,B,E)( A a, B b, E eps,
193         lazy string fmt="assertEqApprox fails: %s != %s",
194         string file=__FILE__, size_t line=__LINE__ )
195 {
196     if( eq_approx( a, b, eps ) ) return;
197     error( file, line, fmt, toStringForce(a), toStringForce(b) );
198 }
199 
200 /++ throws `AssertError` if tested value out of range
201  +
202  + for RANGETYPE allows values: `"[]"`, `"(]"`, `"[)"`, `"()"`
203  +
204  +/
205 void assertInRange( string RANGETYPE="[)",MIN,V,MAX,string file=__FILE__,size_t line=__LINE__ )
206                   ( in MIN min_value, in V tested_value, in MAX max_value,
207                     lazy string fmt="assertInRange fails: %s is out of %s" )
208 if( is(typeof(min_value<tested_value)) && is(typeof(tested_value<max_value)) )
209 {
210     static if( !( RANGETYPE == "[]" || RANGETYPE == "()" ||
211                     RANGETYPE == "[)" || RANGETYPE == "(]" ) )
212         static assert( 0, format( "range type must be one of '[]'," ~
213                                 "'()', '(]', '[)', not '%s'", RANGETYPE ) );
214 
215     enum op1 = RANGETYPE[0] == '[' ? "<=" : "<";
216     enum op2 = RANGETYPE[1] == ']' ? "<=" : "<";
217 
218     mixin( format( q{
219         if( min_value %s tested_value && tested_value %s max_value ) return;
220         error( file, line, fmt, toStringForce(tested_value), format( "%s%%s, %%s%s", min_value, max_value ) );
221         }, op1, op2, RANGETYPE[0], RANGETYPE[1] ) );
222 }
223 
224 ///
225 unittest
226 {
227     assertInRange( 0, 1, 2 );
228     assertInRange( 0, 0, 2 );
229     assertInRange!"[]"( 0, 2, 2 );
230     assertInRange!"(]"( 0.0f, 2, 2.0 );
231 }
232 
233 void error(Args...)( string file, size_t line, string fmt, Args args )
234 {
235     string msg;
236     try msg = format( fmt, args );
237     catch( Exception ) msg = "[FMTERR] " ~ toStringForce( args );
238     throw new AssertError( msg, file, line );
239 }
240 
241 /+ not pure because to!string isn't pure +/
242 private string toStringForce(Args...)( in Args args )
243 {
244     static if( Args.length == 1 )
245     {
246         alias T = Args[0];
247         auto val = args[0];
248 
249         static if( isSomeString!T )
250             return "'" ~ to!string(val) ~ "'";
251         else static if( is( typeof( to!string( val ) )) )
252             return to!string( val );
253         else static if( isElementArray!T )
254         {
255             string[] rr;
256             foreach( i; 0 .. val.length )
257                 rr ~= toStringForce( val[i] );
258             return "[ " ~ rr.join(", ") ~ " ]";
259         }
260         else static if( isSomeObject!T )
261         {
262             if( val is null ) return "null";
263             else return to!string( cast(void*)val );
264         }
265         else static if( is( T == typeof(null) ) ) return null;
266         else return val.stringof;
267     }
268     else return toStringForce( args[0] ) ~ ", " ~ toStringForce( args[1..$] );
269 }
270 
271 unittest
272 {
273     assert( eq( toStringForce([0,4]), "[0, 4]" ) );
274     assert( eq( toStringForce(null), "null" ) );
275     assert( eq( toStringForce(0), "0" ) );
276 
277     assert( eq( toStringForce("hello"), "'hello'" ) );
278 
279     Object a = null;
280 
281     assert( eq( toStringForce(a), "null" ) );
282     assert( eq( toStringForce(a,5), "null, 5" ) );
283 
284     assert( eq( to!string(new Object), "object.Object" ) );
285     assert( !eq( toStringForce(new Object), "object.Object" ) );
286 }
287 
288 unittest
289 {
290     static struct fMtr { float[][] data; alias data this; }
291     static struct dMtr { double[][] data; alias data this; }
292 
293     auto a = fMtr( [[1,2,3],[2,3,4]] );
294     assertEq( a, [[1,2,3],[2,3,4]] );
295     assertEq( [[1,2,3],[2,3,4]], a );
296     auto b = fMtr( [[1,2,3],[2,3,4]] );
297     assertEq( a, b );
298     auto c = dMtr( [[1,2,3],[2,3,4]] );
299     assertEq( a, c );
300 }
301 
302 unittest
303 {
304     static struct fMtr { float[][] data; alias data this; }
305     static struct dMtr { double[][] data; alias data this; }
306 
307     auto a = fMtr( [[1,2,3],[2,3,4]] );
308     assertEqApprox( a, [[1,2,3],[2,3,4]], float.epsilon );
309     assertEqApprox( [[1,2,3],[2,3,4]], a, float.epsilon );
310     auto b = fMtr( [[1,2,3],[2,3,4]] );
311     assertEqApprox( a, b, float.epsilon );
312     auto c = dMtr( [[1,2,3],[2,3,4]] );
313     assertEqApprox( a, c, float.epsilon );
314 }
315 
316 unittest
317 {
318     static struct fVec { float[3] data; alias data this; }
319     auto a = fVec([1,2,3]);
320     assertEq( a, [1,2,3] );
321     double[] b = [1,2,3];
322     assertEq( a, b );
323 }
324 
325 unittest
326 {
327     static assert( !__traits(compiles, eq( "hello", 123 ) ) );
328     static struct fVec { float[] data; alias data this; }
329     auto a = fVec([32]);
330     static assert( !__traits(compiles, eq( a, 123 ) ) );
331 }
332 
333 private:
334 
335 template nameOf(T)
336 {
337     static if( __traits(compiles,typeid(T).name) )
338         enum nameOf = typeid(T).name;
339     else enum nameOf = T.stringof;
340 }
341 
342 // not in des.stdx.traits for break dependencies
343 template isElementArray(R)
344 {
345     enum isElementArray = is(typeof(
346     (inout int = 0)
347     {
348         R r = R.init;
349         auto e = r[0];
350         static assert( hasLength!R );
351         static assert( !isNarrowString!R );
352     }));
353 }
354 
355 unittest
356 {
357     static assert(  isElementArray!(int[]) );
358     static assert(  isElementArray!(int[3]) );
359     static assert(  isElementArray!(float[]) );
360 
361     static struct Vec0
362     {
363         float[] data;
364         alias data this;
365     }
366     static assert(  isElementArray!Vec0 );
367 
368     static struct Vec1 { float[] data; }
369     static assert( !isElementArray!Vec1 );
370 
371     static assert( !isElementArray!(string) );
372     static assert( !isElementArray!(wstring) );
373     static assert( !isElementArray!int );
374     static assert( !isElementArray!float );
375     static assert( !isElementArray!(immutable(void)[]) );
376 }
377 
378 template isSomeObject(T)
379 { enum isSomeObject = is( T == class ) || is( T == interface ); }