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 }