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 *   Check the $(B T) type has properly defined $(B Arbitrary) template. Prints useful user-friendly messages
155 *   at compile time.
156 *
157 *   Good practice is put the template in static assert while defining own instances of $(B Arbitrary) to get
158 *   confidence about instance correctness.
159 */
160 template CheckArbitrary(T)
161 {
162     static assert(__traits(compiles, Arbitrary!T), "Type "~T.stringof~" doesn't have Arbitrary template!");
163     static assert(HasArbitrary!T.HasGenerate!(), "Type "~T.stringof~" doesn't have generate function in Arbitrary template!");
164     static assert(HasArbitrary!T.HasShrink!(), "Type "~T.stringof~" doesn't have shrink function in Arbitrary template!");
165     static assert(HasArbitrary!T.HasSpecialCases!(), "Type "~T.stringof~" doesn't have specialCases function in Arbitrary template!");
166     enum CheckArbitrary = HasArbitrary!T.isFullDefined!();
167 }
168 
169 /**
170 *   Arbitrary for ubyte, byte, ushort, short, uing, int, ulong, long.
171 */
172 template Arbitrary(T)
173     if(isIntegral!T)
174 {
175     static assert(CheckArbitrary!T);
176     
177     auto generate()
178     {
179         return (() => Maybe!T(uniform!"[]"(T.min, T.max))).generator;
180     }
181     
182     auto shrink(T val)
183     {
184         class Shrinker
185         {
186             T saved;
187             
188             this(T firstVal)
189             {
190                 saved = firstVal;
191             }
192             
193             Maybe!T shrink()
194             {
195                 if(saved == 0) return Maybe!T.nothing;
196 
197                 saved = saved/2;
198                 return Maybe!T(saved);
199             }
200         }
201         
202         return (&(new Shrinker(val)).shrink).generator;
203     }
204     
205     T[] specialCases()
206     {
207         return [T.min, 0, T.max];
208     }
209 }
210 unittest
211 {
212     Arbitrary!ubyte.generate;
213     Arbitrary!ubyte.specialCases;
214     assert(Arbitrary!ubyte.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
215     
216     Arbitrary!byte.generate;
217     Arbitrary!byte.specialCases;
218     assert(Arbitrary!byte.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
219     assert(Arbitrary!byte.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0]));
220     
221     Arbitrary!ushort.generate;
222     Arbitrary!ushort.specialCases;
223     assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
224     
225     Arbitrary!short.generate;
226     Arbitrary!short.specialCases;
227     assert(Arbitrary!short.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
228     assert(Arbitrary!short.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0]));
229     
230     Arbitrary!uint.generate;
231     Arbitrary!uint.specialCases;
232     assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
233     
234     Arbitrary!int.generate;
235     Arbitrary!int.specialCases;
236     assert(Arbitrary!int.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
237     assert(Arbitrary!int.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0]));
238     
239     Arbitrary!ulong.generate;
240     Arbitrary!ulong.specialCases;
241     assert(Arbitrary!ushort.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
242     
243     Arbitrary!long.generate;
244     Arbitrary!long.specialCases;
245     assert(Arbitrary!long.shrink(100).take(10).equal([50, 25, 12, 6, 3, 1, 0]));
246     assert(Arbitrary!long.shrink(-100).take(10).equal([-50, -25, -12, -6, -3, -1, 0]));
247 }
248 
249 /**
250 *   Arbitrary for float, double
251 */
252 template Arbitrary(T)
253     if(isFloatingPoint!T)
254 {
255     static assert(CheckArbitrary!T);
256     
257     auto generate()
258     {
259         return (() => Maybe!T(uniform!"[]"(-T.max, T.max))).generator;
260     }
261     
262     auto shrink(T val)
263     {
264         class Shrinker
265         {
266             T saved;
267             
268             this(T firstVal)
269             {
270                 saved = firstVal;
271             }
272             
273             Maybe!T shrink()
274             {
275                 if(approxEqual(saved, 0)) return Maybe!T.nothing;
276                 
277                 saved = saved/2;
278                 return Maybe!T(saved);
279             }
280         }
281         
282         return (&(new Shrinker(val)).shrink).generator;
283     }
284     
285     T[] specialCases()
286     {
287         return [-T.max, 0, T.max, T.nan, T.infinity, T.epsilon, T.min_normal];
288     }
289 }
290 unittest
291 {
292     import std.math;
293     import std.algorithm;
294     
295     Arbitrary!float.generate;
296     Arbitrary!float.specialCases;
297     assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!float.shrink(10.0).take(3).zip([5.0, 2.5, 1.25])));
298     assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!float.shrink(-10.0).take(3).zip([-5.0, -2.5, -1.25])));
299     
300     Arbitrary!double.generate;
301     Arbitrary!double.specialCases;
302     assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!double.shrink(10.0).take(3).zip([5.0, 2.5, 1.25])));
303     assert(reduce!"a && approxEqual(b[0], b[1])"(true, Arbitrary!double.shrink(-10.0).take(3).zip([-5.0, -2.5, -1.25])));
304 }
305 
306 /**
307 *   Arbitrary for bool
308 */
309 template Arbitrary(T)
310     if(is(T == bool))
311 {
312     static assert(CheckArbitrary!T);
313     
314     auto generate()
315     {
316         return [true, false];
317     }
318     
319     auto shrink(T val)
320     {
321         return takeNone!(T[]);
322     }
323     
324     auto specialCases()
325     {
326         return takeNone!(T[]);
327     }
328 }
329 unittest
330 {
331     assert(Arbitrary!bool.generate.equal([true, false]));
332     assert(Arbitrary!bool.shrink(true).empty);
333     assert(Arbitrary!bool.shrink(false).empty);
334 }
335 
336 /**
337 *   Arbitrary template for char, dchar, wchar
338 */
339 template Arbitrary(T)
340     if(isSomeChar!T)
341 {
342     static assert(CheckArbitrary!T);
343     
344     auto generate()
345     {
346         return (() => Maybe!T(cast(T)uniform!"[]"('\u0000', '\U0010FFFF'))).generator;
347     }
348     
349     auto shrink(T val)
350     {
351         return takeNone!(dchar[]);
352     }
353     
354     auto specialCases()
355     {
356         return takeNone!(dchar[]);
357     }
358 }
359 unittest
360 {
361     Arbitrary!char.generate;
362     assert(Arbitrary!char.shrink('a').empty);
363     assert(Arbitrary!char.specialCases().empty);
364     
365     Arbitrary!wchar.generate;
366     assert(Arbitrary!wchar.shrink('a').empty);
367     assert(Arbitrary!wchar.specialCases().empty);
368     
369     Arbitrary!dchar.generate;
370     assert(Arbitrary!dchar.shrink('a').empty);
371     assert(Arbitrary!dchar.specialCases().empty);
372 }
373 
374 /**
375 *   Arbitrary template for strings
376 */
377 template Arbitrary(T)
378     if(isSomeString!T)
379 {
380     static assert(CheckArbitrary!T);
381     
382     auto generate()
383     {
384         return (() => Maybe!T(Arbitrary!(Unqual!(ElementType!T)).generate
385                 .take(uniform!"[]"(ArrayGenSizeMin, ArrayGenSizeMax)).array.idup.to!T)).generator;
386     }
387     
388     auto shrink(T val)
389     {
390         class Shrinker
391         {
392             T saved;
393             
394             this(T startVal)
395             {
396                 saved = startVal;
397             }
398             
399             auto shrink()
400             {
401                 if(saved.length > 0)
402                 {
403                     saved = saved[1..$];
404                     return Maybe!T(saved);
405                 } else return Maybe!T.nothing;
406             }
407         }
408         return (&(new Shrinker(val)).shrink).generator;
409     }
410     
411     T[] specialCases()
412     {
413         return [cast(T)""];
414     }
415 }
416 unittest
417 {
418     Arbitrary!string.generate;
419     assert(Arbitrary!string.shrink("a").take(2).equal([""]));
420     assert(Arbitrary!string.shrink("abc").take(3).equal(["bc", "c", ""]));
421     assert(Arbitrary!string.specialCases().equal([""]));
422     
423     Arbitrary!wstring.generate;
424     assert(Arbitrary!wstring.shrink("a"w).take(2).equal([""w]));
425     assert(Arbitrary!wstring.shrink("abc"w).take(3).equal(["bc"w, "c"w, ""w]));
426     assert(Arbitrary!wstring.specialCases().equal([""w]));
427     
428     Arbitrary!dstring.generate;
429     assert(Arbitrary!dstring.shrink("a"d).take(2).equal([""d]));
430     assert(Arbitrary!dstring.shrink("abc"d).take(3).equal(["bc"d, "c"d, ""d]));
431     assert(Arbitrary!dstring.specialCases().equal([""d]));
432 }
433 
434 /**
435 *   Arbitrary template for char, dchar, wchar
436 */
437 template Arbitrary(T)
438     if(isArray!T && HasArbitrary!(ElementType!T).HasGenerate!() && !isSomeString!T)
439 {
440     static assert(CheckArbitrary!T);
441     
442     auto generate()
443     {
444         return (() => Maybe!T( Arbitrary!(ElementType!T).generate.take(uniform!"[]"(ArrayGenSizeMin,ArrayGenSizeMax)).array )).generator;
445     }
446     
447     auto shrink(T val)
448     {
449         class Shrinker
450         {
451             T saved;
452             
453             this(T startVal)
454             {
455                 saved = startVal;
456             }
457             
458             auto shrink()
459             {
460                 if(saved.length > 0)
461                 {
462                     saved = saved[1..$];
463                     return Maybe!T(saved);
464                 } else return Maybe!T.nothing;
465             }
466         }
467         return (&(new Shrinker(val)).shrink).generator;
468     }
469     
470     auto specialCases()
471     {
472         return [cast(T)[]];
473     }
474 }
475 unittest
476 {
477     Arbitrary!(uint[]).generate;
478     assert(Arbitrary!(uint[]).shrink([1u, 2u, 3u]).take(3).equal([[2u, 3u], [3u], []]));
479     assert(Arbitrary!(uint[]).specialCases().equal([cast(uint[])[]]));
480 }