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