1 /**
2 *   Module defines routines for generating testing sets.
3 *   
4 *   To define Arbitrary template for particular type $(B T) (or types) you should follow
5 *   compile-time interface:
6 *   <ul>
7 *       <li>Define $(B generate) function that takes nothing and returns range of $(B T).
8 *           This function is used to generate random sets of testing data. Size of required
9 *           sample isn't passed thus use lazy ranges to generate possible infinite set of
10 *           data.</li>
11 *       <li>Define $(B shrink) function that takes value of $(B T) and returns range of
12 *           truncated variations. This function is used to reduce failing case data to
13 *           minimum possible set. You can return empty array if you like to get large bunch
14 *           of random data.</li>
15 *       <li>Define $(B specialCases) function that takes nothing and returns range of $(B T).
16 *           This function is used to test some special values for particular type like NaN or
17 *           null pointer. You can return empty array if no testing on special cases is required.</li>
18 *   </ul>
19 *
20 *   Usually useful practice is put static assert with $(B CheckArbitrary) template into your implementation
21 *   to actually get confidence that you've done it properly. 
22 *
23 *   Example:
24 *   ---------
25 *   template Arbitrary(T)
26 *       if(isIntegral!T)
27 *   {
28 *       static assert(CheckArbitrary!T);
29 *       
30 *       auto generate()
31 *       {
32 *           return (() => Maybe!T(uniform!"[]"(T.min, T.max))).generator;
33 *       }
34 *       
35 *       auto shrink(T val)
36 *       {
37 *           class Shrinker
38 *           {
39 *               T saved;
40 *               
41 *               this(T firstVal)
42 *               {
43 *                   saved = firstVal;
44 *               }
45 *               
46 *               Maybe!T shrink()
47 *               {
48 *                   if(saved == 0) return Maybe!T.nothing;
49 *                   
50 *                   if(saved > 0) saved--;
51 *                   if(saved < 0) saved++;
52 *                   
53 *                   return Maybe!T(saved);
54 *               }
55 *           }
56 *           
57 *           return (&(new Shrinker(val)).shrink).generator;
58 *       }
59 *       
60 *       T[] specialCases()
61 *       {
62 *           return [T.min, 0, T.max];
63 *       }
64 *   }
65 *   ---------
66 *
67 *   Copyright: © 2014 Anton Gushcha
68 *   License: Subject to the terms of the MIT license, as written in the included LICENSE file.
69 *   Authors: NCrashed <ncrashed@gmail.com>
70 */
71 module dcheck.arbitrary;
72 
73 import std.traits;
74 import std.range;
75 import std.random;
76 import std.conv;
77 import std.math;
78 import dcheck.maybe;
79 import dcheck.generator;
80 
81 /// Minimum size of generated arrays (and strings)
82 enum ArrayGenSizeMin = 1;
83 /// Maximum size of generated arrays (and strings)
84 enum ArrayGenSizeMax = 32;
85  
86 /**
87 *   Checks if $(B T) has Arbitrary template with
88 *   $(B generate), $(B shrink) and $(B specialCases) functions.
89 */
90 template HasArbitrary(T)
91 {
92     template HasGenerate()
93     {
94         static if(__traits(compiles, Arbitrary!T.generate))
95         {
96             alias ParameterTypeTuple!(Arbitrary!T.generate) Params;
97             alias ReturnType!(Arbitrary!T.generate) RetType;
98             
99             enum HasGenerate = 
100                 isInputRange!RetType && is(ElementType!RetType == T)
101                 && Params.length == 0;
102         } else
103         {
104             enum HasGenerate = false;
105         }
106     }
107     
108     template HasShrink()
109     {
110         static if(__traits(compiles, Arbitrary!T.shrink ))
111         {
112             alias ParameterTypeTuple!(Arbitrary!T.shrink) Params;
113             alias ReturnType!(Arbitrary!T.shrink) RetType;
114             
115             enum HasShrink = 
116                 isInputRange!RetType 
117                 && (is(ElementType!RetType == T) || isSomeChar!T && isSomeChar!(ElementType!RetType))
118                 && Params.length == 1
119                 && is(Params[0] == T);                
120         } else
121         {
122             enum HasShrink = false;
123         }
124     }
125     
126     template HasSpecialCases()
127     {
128         static if(__traits(compiles, Arbitrary!T.specialCases))
129         {
130             alias ParameterTypeTuple!(Arbitrary!T.specialCases) Params;
131             alias ReturnType!(Arbitrary!T.specialCases) RetType;
132             
133             enum HasSpecialCases = 
134                 isInputRange!RetType 
135                 && (is(ElementType!RetType == T) || isSomeChar!T && isSomeChar!(ElementType!RetType))
136                 && Params.length == 0;
137         } else
138         {
139             enum HasSpecialCases = false;
140         }
141     }
142     
143     template isFullDefined()
144     {
145         enum isFullDefined = 
146             __traits(compiles, Arbitrary!T) &&
147             HasGenerate!() &&
148             HasShrink!() &&
149             HasSpecialCases!();
150     }
151 }
152 
153 /**
154 *   You can use this to define default shrink implementation.
155 */
156 mixin template DefaultShrink(T)
157 {
158     import std.range;
159     auto shrink(T val)
160     {
161         return takeNone!(T[]);
162     }
163 }
164 
165 /**
166 *   You can use this to define default special cases implementation.
167 */
168 mixin template DefaultSpecialCases(T)
169 {
170     import std.range;
171     auto specialCases()
172     {
173         return takeNone!(T[]);
174     }
175 }
176 
177 /**
178 *   You can use this to define only generate function.
179 */
180 mixin template DefaultShrinkAndSpecialCases(T)
181 {
182     import std.range;
183     
184     auto shrink(T val) 
185     {
186         return takeNone!(T[]);
187     }
188     
189     auto specialCases()
190     {
191         return takeNone!(T[]);
192     }
193 }
194 
195 /**
196 *   Check the $(B T) type has properly defined $(B Arbitrary) template. Prints useful user-friendly messages
197 *   at compile time.
198 *
199 *   Good practice is put the template in static assert while defining own instances of $(B Arbitrary) to get
200 *   confidence about instance correctness.
201 */
202 template CheckArbitrary(T)
203 {
204     // dirty hack to force at least one instance of the template
205     alias t = Arbitrary!T;
206     static assert(__traits(compiles, Arbitrary!T), "Type "~T.stringof~" doesn't have Arbitrary template!");
207     static assert(HasArbitrary!T.HasGenerate!(), "Type "~T.stringof~" doesn't have generate function in Arbitrary template!");
208     static assert(HasArbitrary!T.HasShrink!(), "Type "~T.stringof~" doesn't have shrink function in Arbitrary template!");
209     static assert(HasArbitrary!T.HasSpecialCases!(), "Type "~T.stringof~" doesn't have specialCases function in Arbitrary template!");
210     enum CheckArbitrary = HasArbitrary!T.isFullDefined!();
211 }
212 
213 /**
214 *   Arbitrary for ubyte, byte, ushort, short, uing, int, ulong, long.
215 */
216 template Arbitrary(T)
217     if(isIntegral!T)
218 {
219     static assert(CheckArbitrary!T);
220     
221     auto generate()
222     {
223         return (() => Maybe!T(uniform!"[]"(T.min, T.max))).generator;
224     }
225     
226     auto shrink(T val)
227     {
228         class Shrinker
229         {
230             T saved;
231             
232             this(T firstVal)
233             {
234                 saved = firstVal;
235             }
236             
237             Maybe!T shrink()
238             {
239                 if(saved == 0) return Maybe!T.nothing;
240 
241                 saved = saved/2;
242                 return Maybe!T(saved);
243             }
244         }
245         
246         return (&(new Shrinker(val)).shrink).generator;
247     }
248     
249     T[] specialCases()
250     {
251         return [T.min, 0, T.max];
252     }
253 }
254 unittest
255 {
256     Arbitrary!ubyte.generate;
257     Arbitrary!ubyte.specialCases;
258     assert(Arbitrary!ubyte.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
259     
260     Arbitrary!byte.generate;
261     Arbitrary!byte.specialCases;
262     assert(Arbitrary!byte.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
263     assert(Arbitrary!byte.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0]));
264     
265     Arbitrary!ushort.generate;
266     Arbitrary!ushort.specialCases;
267     assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
268     
269     Arbitrary!short.generate;
270     Arbitrary!short.specialCases;
271     assert(Arbitrary!short.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
272     assert(Arbitrary!short.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0]));
273     
274     Arbitrary!uint.generate;
275     Arbitrary!uint.specialCases;
276     assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
277     
278     Arbitrary!int.generate;
279     Arbitrary!int.specialCases;
280     assert(Arbitrary!int.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
281     assert(Arbitrary!int.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0]));
282     
283     Arbitrary!ulong.generate;
284     Arbitrary!ulong.specialCases;
285     assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
286     
287     Arbitrary!long.generate;
288     Arbitrary!long.specialCases;
289     assert(Arbitrary!long.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
290     assert(Arbitrary!long.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0]));
291 }
292 
293 /**
294 *   Arbitrary for float, double
295 */
296 template Arbitrary(T)
297     if(isFloatingPoint!T)
298 {
299     static assert(CheckArbitrary!T);
300     
301     auto generate()
302     {
303         return (() => Maybe!T(uniform!"[]"(-T.max, T.max))).generator;
304     }
305     
306     auto shrink(T val)
307     {
308         class Shrinker
309         {
310             T saved;
311             
312             this(T firstVal)
313             {
314                 saved = firstVal;
315             }
316             
317             Maybe!T shrink()
318             {
319                 if(approxEqual(saved, 0)) return Maybe!T.nothing;
320                 
321                 saved = saved/2;
322                 return Maybe!T(saved);
323             }
324         }
325         
326         return (&(new Shrinker(val)).shrink).generator;
327     }
328     
329     T[] specialCases()
330     {
331         return [-T.max, 0, T.max, T.nan, T.infinity, T.epsilon, T.min_normal];
332     }
333 }
334 unittest
335 {
336     import std.math;
337     import std.algorithm;
338     
339     Arbitrary!float.generate;
340     Arbitrary!float.specialCases;
341     assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!float.shrink(10.0).take(3).zip([5.0, 2.5, 1.25])));
342     assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!float.shrink(-10.0).take(3).zip([-5.0, -2.5, -1.25])));
343     
344     Arbitrary!double.generate;
345     Arbitrary!double.specialCases;
346     assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!double.shrink(10.0).take(3).zip([5.0, 2.5, 1.25])));
347     assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!double.shrink(-10.0).take(3).zip([-5.0, -2.5, -1.25])));
348 }
349 
350 /**
351 *   Arbitrary for bool
352 */
353 template Arbitrary(T)
354     if(is(T == bool))
355 {
356     static assert(CheckArbitrary!T);
357     
358     auto generate()
359     {
360         return [true, false];
361     }
362     
363     mixin DefaultShrinkAndSpecialCases!T;
364 }
365 unittest
366 {
367     assert(Arbitrary!bool.generate.equal([true, false]));
368     assert(Arbitrary!bool.shrink(true).empty);
369     assert(Arbitrary!bool.shrink(false).empty);
370 }
371 
372 /**
373 *   Arbitrary template for char, dchar, wchar
374 */
375 template Arbitrary(T)
376     if(isSomeChar!T)
377 {
378     static assert(CheckArbitrary!T);
379     
380     auto generate()
381     {
382         enum alphabet = "abcdeABCDE12345áàäéèëÁÀÄÉÈËЯВНЛАڴٸڱ☭ତ⇕";
383         return (() => Maybe!T(cast(T)alphabet[uniform(0, alphabet.length)])).generator;
384     }
385     
386     mixin DefaultShrinkAndSpecialCases!T;
387 }
388 unittest
389 {
390     Arbitrary!char.generate.take(100);
391     assert(Arbitrary!char.shrink('a').empty);
392     assert(Arbitrary!char.specialCases().empty);
393     
394     Arbitrary!wchar.generate.take(100);
395     assert(Arbitrary!wchar.shrink('a').empty);
396     assert(Arbitrary!wchar.specialCases().empty);
397     
398     Arbitrary!dchar.generate.take(100);
399     assert(Arbitrary!dchar.shrink('a').empty);
400     assert(Arbitrary!dchar.specialCases().empty);
401 }
402 
403 /**
404 *   Arbitrary template for strings
405 */
406 template Arbitrary(T)
407     if(isSomeString!T)
408 {
409     static assert(CheckArbitrary!T);
410     
411     auto generate()
412     {
413         return (() => Maybe!T(Arbitrary!(Unqual!(ElementType!T)).generate
414                 .take(uniform!"[]"(ArrayGenSizeMin, ArrayGenSizeMax)).array.idup.to!T)).generator;
415     }
416     
417     auto shrink(T val)
418     {
419         class Shrinker
420         {
421             T saved;
422             
423             this(T startVal)
424             {
425                 saved = startVal;
426             }
427             
428             auto shrink()
429             {
430                 if(saved.length > 0)
431                 {
432                     saved = saved[1..$];
433                     return Maybe!T(saved);
434                 } else return Maybe!T.nothing;
435             }
436         }
437         return (&(new Shrinker(val)).shrink).generator;
438     }
439     
440     T[] specialCases()
441     {
442         return [cast(T)""];
443     }
444 }
445 unittest
446 {
447     Arbitrary!string.generate;
448     assert(Arbitrary!string.shrink("a").take(2).equal([""]));
449     assert(Arbitrary!string.shrink("abc").take(3).equal(["bc", "c", ""]));
450     assert(Arbitrary!string.specialCases().equal([""]));
451     
452     Arbitrary!wstring.generate;
453     assert(Arbitrary!wstring.shrink("a"w).take(2).equal([""w]));
454     assert(Arbitrary!wstring.shrink("abc"w).take(3).equal(["bc"w, "c"w, ""w]));
455     assert(Arbitrary!wstring.specialCases().equal([""w]));
456     
457     Arbitrary!dstring.generate;
458     assert(Arbitrary!dstring.shrink("a"d).take(2).equal([""d]));
459     assert(Arbitrary!dstring.shrink("abc"d).take(3).equal(["bc"d, "c"d, ""d]));
460     assert(Arbitrary!dstring.specialCases().equal([""d]));
461 }
462 
463 /**
464 *   Arbitrary template for arrays
465 */
466 template Arbitrary(T)
467     if(isArray!T && HasArbitrary!(ElementType!T).HasGenerate!() && !isSomeString!T)
468 {
469     static assert(CheckArbitrary!T);
470     
471     auto generate()
472     {
473         return (() => Maybe!T( Arbitrary!(ElementType!T).generate.take(uniform!"[]"(ArrayGenSizeMin,ArrayGenSizeMax)).array )).generator;
474     }
475     
476     auto shrink(T val)
477     {
478         class Shrinker
479         {
480             T saved;
481             
482             this(T startVal)
483             {
484                 saved = startVal;
485             }
486             
487             auto shrink()
488             {
489                 if(saved.length > 0)
490                 {
491                     saved = saved[1..$];
492                     return Maybe!T(saved);
493                 } else return Maybe!T.nothing;
494             }
495         }
496         return (&(new Shrinker(val)).shrink).generator;
497     }
498     
499     auto specialCases()
500     {
501         return [cast(T)[]];
502     }
503 }
504 unittest
505 {
506     Arbitrary!(uint[]).generate;
507     assert(Arbitrary!(uint[]).shrink([1u, 2u, 3u]).take(3).equal([[2u, 3u], [3u], []]));
508     assert(Arbitrary!(uint[]).specialCases().equal([cast(uint[])[]]));
509 }