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 }