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 public 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, string error)
67     {
68         string res = "try {";
69         res ~= var~" = constraint(";
70         foreach(j, T; ParameterTypeTuple!constraint)
71         {
72             res ~= "range"~j.to!string~".front,";
73         }
74         res ~= ");
75         } 
76         catch( Throwable th )
77         {
78             "~var~" = false;
79             "~error~" = th;
80         }";
81         return res;
82     }
83     
84     // generates string for declaring range variables
85     string genDeclare()
86     {
87         string res = "";
88         foreach(j, T; ParameterTypeTuple!constraint)
89         {
90             res ~= "auto range"~j.to!string~" = chain(Arbitrary!("~T.stringof~").generate, Arbitrary!("~T.stringof~").specialCases);\n";
91             res ~= "assert(!range"~j.to!string~".empty, \"Generating range is empty at checking start!"
92                 "Check Arbitrary!"~T.stringof~" implementation!\")\n;";
93         }
94         return res;
95     }
96     
97     // generates string for declaring shrink range variables
98     string genShrinkDeclare()
99     {
100         string res = "";
101         foreach(j, T; ParameterTypeTuple!constraint)
102         {
103             res ~= "auto shrink"~j.to!string~" = Arbitrary!("~T.stringof~").shrink(range"~j.to!string~".front);\n";
104             res ~= "ElementType!(typeof(shrink"~j.to!string~")) savedShrink"~j.to!string~" = range"~j.to!string~".front;\n";
105         }
106         return res;
107     }
108     
109     // generates string of applying constaint with range values
110     string genShrinkApply(string var)
111     {
112         string res = "bool "~var~" = constraint(";
113         foreach(j, T; ParameterTypeTuple!constraint)
114         {
115             res ~= "savedShrink"~j.to!string~",";
116         }
117         return res~");";
118     }
119     
120     mixin(genDeclare());
121     
122     // i-th cell holds info about: if i-th range ever be empty
123     // testing is ended when all ranges is cycled or max test count is expired
124     bool[ParameterTypeTuple!constraint.length] flags;
125     // chooses which range is popping now
126     size_t k = 0; 
127     
128     testloop: foreach(calls; 0..testsCount)
129     {
130         Throwable savedError = null;
131         bool res = false;
132         
133         mixin(genApply("res", "savedError"));
134 
135         // catched a bug, start shrink
136         if(!res)
137         {
138             size_t shrinks, shrinkOrder;
139             bool[ParameterTypeTuple!constraint.length] shrinkFlags;
140             mixin(genShrinkDeclare());
141             
142             void printFinalMessage()
143             {
144                 auto builder = appender!string;
145                 
146                 if(savedError !is null)
147                 {
148                     builder.put(savedError.toString);
149                 }
150                 
151                 builder.put("\n==============================\n");
152                 static if(__traits(compiles, fullyQualifiedName!(constraint)))
153                     builder.put(text("Constraint ", fullyQualifiedName!(constraint), " is failed!\n"));
154                 else
155                     builder.put(text("Constraint ", constraint.stringof, " is failed!\n"));
156                 builder.put(text("Calls count: ", calls+1, ". Shrinks count: ", shrinks, "\n"));
157                 builder.put(text("Parameters: \n"));
158                 alias ParameterIdentifierTuple!constraint paramNames;
159                 foreach(j, T; ParameterTypeTuple!constraint)
160                 {
161                     builder.put(text("\t",j,": ", T.stringof, " ",  paramNames[j].stringof, " ",
162                             " = ", mixin("savedShrink"~j.to!string~".to!string"), "\n"));
163                 }
164                 assert(false, builder.data);
165             }
166             
167             shrinkloop: for(;shrinks < shrinkCount; shrinks++)
168             {
169                 // check shrink ranges
170                 foreach(j, T; ParameterTypeTuple!constraint)
171                 {
172                     mixin("if (shrink"~j.to!string~".empty)
173                         {
174                             shrinkFlags["~j.to!string~"] = true;
175                         }
176                     ");
177                 }
178                 if(shrinkFlags.reduce!"a && b")
179                 {
180                     break shrinkloop;
181                 }
182                 
183                 // save values to show them after and
184                 foreach(j, T; ParameterTypeTuple!constraint)
185                 {
186                     mixin("if (!shrink"~j.to!string~".empty)
187                         {
188                             savedShrink"~j.to!string~" = shrink"~j.to!string~".front;
189                             shrink"~j.to!string~".popFront();
190                         }"
191                     );
192                 }
193                 
194                 mixin(genShrinkApply("shrinkedRes"));
195                 
196                 // displaying result
197                 if(shrinkedRes)
198                 {
199                     printFinalMessage();
200                 }
201                 
202                 // update shrinkOrder
203                 shrinkOrder+=1;
204                 if(shrinkOrder >= ParameterTypeTuple!constraint.length)
205                 {
206                     shrinkOrder = 0;
207                 }
208             }
209             
210             // displaying result
211             printFinalMessage();
212         }
213         
214         // updating ranges in order of k
215         foreach(j, T; ParameterTypeTuple!constraint)
216         {
217             if(j == k)
218             {
219                 mixin("range"~j.to!string~".popFront;");
220                 mixin("if (range"~j.to!string~".empty) "
221                     "{
222                         flags["~j.to!string~"] = true;
223                         range"~j.to!string~" = chain(Arbitrary!("~T.stringof~").generate, Arbitrary!("~T.stringof~").specialCases);
224                     }"
225                 );
226             }
227             
228             if(flags.reduce!"a && b")
229             {
230                 break testloop;
231             }
232         }
233         // update k
234         k+=1;
235         if(k >= ParameterTypeTuple!constraint.length)
236         {
237             k = 0;
238         }
239     }
240 }
241 
242 unittest
243 {
244     import std.math;
245     import std.exception;
246     
247     checkConstraint!((int a, int b) => a + b == b + a);
248     assertThrown!Error(checkConstraint!((int a, int b) => abs(a) < 100 && abs(b) < 100));
249     assertThrown!Error(checkConstraint!((bool a, bool b) => a && !b));
250 }