1 /**
2 *   Module defines routines for checking constraints in automated mode.
3 *   
4 *   To perform automated check user should define a delegate, that takes
5 *   some arguments and by design should return always true. The $(B checkConstraint)
6 *   function generates arguments via $(B Arbitrary!T) template and checks the
7 *   delegate to be true.
8 *
9 *   If constrained returned false, $(B checkConstraint) function tries to 
10 *   shrink input parameters to find minimum fail case (of course, it shrink
11 *   function of corresponding Arbitrary template is properly defined).
12 *
13 *   Example:
14 *   ---------
15 *   checkConstraint!((int a, int b) => a + b == b + a);
16 *   assertThrown!Error(checkConstraint!((int a, int b) => abs(a) < 100 && abs(b) < 100));
17 *   assertThrown!Error(checkConstraint!((bool a, bool b) => a && !b));
18 *   ---------
19 *
20 *   Copyright: © 2014 Anton Gushcha
21 *   License: Subject to the terms of the MIT license, as written in the included LICENSE file.
22 *   Authors: NCrashed <ncrashed@gmail.com>
23 */
24 module dcheck.constraint;
25 
26 import std.algorithm;
27 import std.array;
28 import std.traits;
29 import std.conv;
30 import std.range;
31 import std.stdio;
32 import dcheck.arbitrary;
33 
34 private template allHasArbitrary(T...)
35 {
36     static if(T.length == 0)
37     {
38         enum allHasArbitrary = true;
39     } else
40     {
41         enum allHasArbitrary = HasArbitrary!(T[0]).isFullDefined!() && allHasArbitrary!(T[1..$]);
42     }
43 }
44 
45 /**
46 *   Checks $(B constraint) to return true value for random generated input parameters
47 *   with $(B Arbitrary) template. 
48 *
49 *   All input arguments of the constraint have to implement $(B Arbitrary) template.
50 *   Argument shrinking is performed if parameter corresponding shrink range is not empty.
51 *
52 *   If constrained returns false for some parameters set, shrinking is performed and
53 *   detailed information about the set is thrown with $(B Error).
54 *
55 *   $(B testCount) parameter defines maximum count of test run. Test can end early if
56 *   there is fail or all possible parameters values are checked.
57 *
58 *   $(B shrinkCount) parameter defines maximum count of shrinking tries. User implementation
59 *   of $(B Arbitrary!T.shrink) function defines speed of minimum fail set search, shrinking
60 *   isn't performed for empty shrinking ranges. 
61 */
62 void checkConstraint(alias constraint)(size_t testsCount = 100, size_t shrinkCount = 100)
63     if(isSomeFunction!constraint && allHasArbitrary!(ParameterTypeTuple!constraint))
64 {
65     // generates string of applying constaint with range values
66     string genApply(string var)
67     {
68         string res = "bool "~var~" = constraint(";
69         foreach(j, T; ParameterTypeTuple!constraint)
70         {
71             res ~= "range"~j.to!string~".front,";
72         }
73         return res~");";
74     }
75     
76     // generates string for declaring range variables
77     string genDeclare()
78     {
79         string res = "";
80         foreach(j, T; ParameterTypeTuple!constraint)
81         {
82             res ~= "auto range"~j.to!string~" = Arbitrary!("~T.stringof~").generate;\n";
83             res ~= "assert(!range"~j.to!string~".empty, \"Generating range is empty at checking start!"
84                 "Check Arbitrary!"~T.stringof~" implementation!\")\n;";
85         }
86         return res;
87     }
88     
89     // generates string for declaring shrink range variables
90     string genShrinkDeclare()
91     {
92         string res = "";
93         foreach(j, T; ParameterTypeTuple!constraint)
94         {
95             res ~= "auto shrink"~j.to!string~" = Arbitrary!("~T.stringof~").shrink(range"~j.to!string~".front);\n";
96             res ~= "ElementType!(typeof(shrink"~j.to!string~")) savedShrink"~j.to!string~" = range"~j.to!string~".front;\n";
97         }
98         return res;
99     }
100     
101     // generates string of applying constaint with range values
102     string genShrinkApply(string var)
103     {
104         string res = "bool "~var~" = constraint(";
105         foreach(j, T; ParameterTypeTuple!constraint)
106         {
107             res ~= "savedShrink"~j.to!string~",";
108         }
109         return res~");";
110     }
111     
112     mixin(genDeclare());
113     
114     // i-th cell holds info about: if i-th range ever be empty
115     // testing is ended when all ranges is cycled or max test count is expired
116     bool[ParameterTypeTuple!constraint.length] flags;
117     // chooses which range is popping now
118     size_t k = 0; 
119     
120     testloop: foreach(calls; 0..testsCount)
121     {
122         mixin(genApply("res"));
123         
124         // catched a bug, start shrink
125         if(!res)
126         {
127             size_t shrinks, shrinkOrder;
128             bool[ParameterTypeTuple!constraint.length] shrinkFlags;
129             mixin(genShrinkDeclare());
130             
131             void printFinalMessage()
132             {
133                 auto builder = appender!string;
134                 builder.put("\n==============================\n");
135                 builder.put(text("Constraint ", fullyQualifiedName!(constraint), " is failed!\n"));
136                 builder.put(text("Calls count: ", calls+1, ". Shrinks count: ", shrinks, "\n"));
137                 builder.put(text("Parameters: \n"));
138                 alias ParameterIdentifierTuple!constraint paramNames;
139                 foreach(j, T; ParameterTypeTuple!constraint)
140                 {
141                     builder.put(text("\t",j,": ", T.stringof, " ",  paramNames[j].stringof, " ",
142                             " = ", mixin("savedShrink"~j.to!string~".to!string"), "\n"));
143                 }
144                 assert(false, builder.data);
145             }
146             
147             shrinkloop: for(;shrinks < shrinkCount; shrinks++)
148             {
149                 // check shrink ranges
150                 foreach(j, T; ParameterTypeTuple!constraint)
151                 {
152                     mixin("if (shrink"~j.to!string~".empty)
153                         {
154                             shrinkFlags["~j.to!string~"] = true;
155                         }
156                     ");
157                 }
158                 if(shrinkFlags.reduce!"a && b")
159                 {
160                     break shrinkloop;
161                 }
162                 
163                 // save values to show them after and
164                 foreach(j, T; ParameterTypeTuple!constraint)
165                 {
166                     mixin("if (!shrink"~j.to!string~".empty)
167                         {
168                             savedShrink"~j.to!string~" = shrink"~j.to!string~".front;
169                             shrink"~j.to!string~".popFront();
170                         }"
171                     );
172                 }
173                 
174                 mixin(genShrinkApply("shrinkedRes"));
175                 
176                 // displaying result
177                 if(shrinkedRes)
178                 {
179                     printFinalMessage();
180                 }
181                 
182                 // update shrinkOrder
183                 shrinkOrder+=1;
184                 if(shrinkOrder >= ParameterTypeTuple!constraint.length)
185                 {
186                     shrinkOrder = 0;
187                 }
188             }
189             
190             // displaying result
191             printFinalMessage();
192         }
193         
194         // updating ranges in order of k
195         foreach(j, T; ParameterTypeTuple!constraint)
196         {
197             if(j == k)
198             {
199                 mixin("range"~j.to!string~".popFront;");
200                 mixin("if (range"~j.to!string~".empty) "
201                     "{
202                         flags["~j.to!string~"] = true;
203                         range"~j.to!string~" = Arbitrary!("~T.stringof~").generate;
204                     }"
205                 );
206             }
207             
208             if(flags.reduce!"a && b")
209             {
210                 break testloop;
211             }
212         }
213         // update k
214         k+=1;
215         if(k >= ParameterTypeTuple!constraint.length)
216         {
217             k = 0;
218         }
219     }
220 }
221 
222 unittest
223 {
224     import std.math;
225     import std.exception;
226     
227     checkConstraint!((int a, int b) => a + b == b + a);
228     assertThrown!Error(checkConstraint!((int a, int b) => abs(a) < 100 && abs(b) < 100));
229     assertThrown!Error(checkConstraint!((bool a, bool b) => a && !b));
230 }