1 /** 2 Property-based testing. 3 */ 4 module unit_threaded.property; 5 6 7 template from(string moduleName) { 8 mixin("import from = " ~ moduleName ~ ";"); 9 } 10 11 12 /// 13 class PropertyException : Exception 14 { 15 this(in string msg, string file = __FILE__, 16 size_t line = __LINE__, Throwable next = null) @safe pure nothrow 17 { 18 super(msg, file, line, next); 19 } 20 } 21 22 /** 23 Check that bool-returning F is true with randomly generated values. 24 */ 25 void check(alias F, int numFuncCalls = 100) 26 (in uint seed = from!"std.random".unpredictableSeed, 27 in string file = __FILE__, 28 in size_t line = __LINE__) 29 @trusted 30 { 31 32 import unit_threaded.randomized.random: RndValueGen; 33 import unit_threaded.should: UnitTestException; 34 import std.conv: text; 35 import std.traits: ReturnType, Parameters, isSomeString; 36 import std.array: join; 37 import std.typecons: Flag, Yes, No; 38 import std.random: Random; 39 40 static assert(is(ReturnType!F == bool), 41 text("check only accepts functions that return bool, not ", ReturnType!F.stringof)); 42 43 auto random = Random(seed); 44 auto gen = RndValueGen!(Parameters!F)(&random); 45 46 auto input(Flag!"shrink" shrink = Yes.shrink) { 47 string[] ret; 48 static if(Parameters!F.length == 1 && canShrink!(Parameters!F[0])) { 49 auto val = gen.values[0].value; 50 auto shrunk = shrink ? val.shrink!F : val; 51 ret ~= shrunk.text; 52 static if(isSomeString!(Parameters!F[0])) 53 ret[$-1] = `"` ~ ret[$-1] ~ `"`; 54 } else 55 foreach(ref valueGen; gen.values) { 56 ret ~= valueGen.text; 57 } 58 return ret.join(", "); 59 } 60 61 foreach(i; 0 .. numFuncCalls) { 62 bool pass; 63 64 try { 65 gen.genValues; 66 } catch(Throwable t) { 67 throw new PropertyException("Error generating values\n" ~ t.toString, file, line, t); 68 } 69 70 try { 71 pass = F(gen.values); 72 } catch(Throwable t) { 73 // trying to shrink when an exeption is thrown is too much of a bother code-wise 74 throw new UnitTestException( 75 text("Property threw. Seed: ", seed, ". Input: ", input(No.shrink), ". Message: ", t.msg), 76 file, 77 line, 78 t, 79 ); 80 } 81 82 if(!pass) { 83 throw new UnitTestException(text("Property failed. Seed: ", seed, ". Input: ", input), file, line); 84 } 85 } 86 } 87 88 /** 89 For values that unit-threaded doesn't know how to generate, test that the Predicate 90 holds, using Generator to come up with new values. 91 */ 92 void checkCustom(alias Generator, alias Predicate) 93 (int numFuncCalls = 100, in string file = __FILE__, in size_t line = __LINE__) @trusted { 94 95 import unit_threaded.should: UnitTestException; 96 import std.conv: text; 97 import std.traits: ReturnType; 98 99 static assert(is(ReturnType!Predicate == bool), 100 text("check only accepts functions that return bool, not ", ReturnType!F.stringof)); 101 102 alias Type = ReturnType!Generator; 103 104 foreach(i; 0 .. numFuncCalls) { 105 106 Type object; 107 108 try { 109 object = Generator(); 110 } catch(Throwable t) { 111 throw new PropertyException("Error generating value\n" ~ t.toString, file, line, t); 112 } 113 114 bool pass; 115 116 try { 117 pass = Predicate(object); 118 } catch(Throwable t) { 119 throw new UnitTestException( 120 text("Property threw. Input: ", object, ". Message: ", t.msg), 121 file, 122 line, 123 t, 124 ); 125 } 126 127 if(!pass) { 128 throw new UnitTestException("Property failed with input:" ~ object.text, file, line); 129 } 130 } 131 } 132 133 134 private auto shrinkOne(alias F, int index, T)(T values) { 135 import std.stdio; 136 import std.traits; 137 auto orig = values[index]; 138 return shrink!((a) { 139 values[index] = a; 140 return F(values.expand); 141 })(orig); 142 143 } 144 145 /// 146 @("Verify identity property for int[] succeeds") 147 @safe unittest { 148 149 int numCalls; 150 bool identity(int[] a) pure { 151 ++numCalls; 152 return a == a; 153 } 154 155 check!identity; 156 assert(numCalls == 100); 157 158 numCalls = 0; 159 check!(identity, 10); 160 assert(numCalls == 10); 161 } 162 163 /// 164 @("Explicit Gen") 165 @safe unittest { 166 import unit_threaded.randomized.gen; 167 import unit_threaded.should: UnitTestException; 168 import std.exception: assertThrown; 169 170 check!((Gen!(int, 1, 1) a) => a == 1); 171 assertThrown!UnitTestException(check!((Gen!(int, 1, 1) a) => a == 2)); 172 } 173 174 private enum canShrink(T) = __traits(compiles, shrink!((T _) => true)(T.init)); 175 176 T shrink(alias F, T)(T value) { 177 import std.conv: text; 178 179 assert(!F(value), text("Property did not fail for value ", value)); 180 181 T[][] oldParams; 182 return shrinkImpl!F(value, [value], oldParams); 183 } 184 185 private T shrinkImpl(alias F, T)(in T value, T[] candidates, T[][] oldParams = []) 186 if(from!"std.traits".isIntegral!T) 187 { 188 import std.algorithm: canFind, minPos; 189 import std.traits: isSigned; 190 191 auto params = value ~ candidates; 192 if(oldParams.canFind(params)) return value; 193 oldParams ~= params; 194 195 // if it suddenly starts passing we've found our boundary value 196 if(value < T.max && F(value + 1)) return value; 197 if(value > T.min && F(value - 1)) return value; 198 199 bool stillFails(T attempt) { 200 if(!F(attempt) && !candidates.canFind(attempt)) { 201 candidates ~= attempt; 202 return true; 203 } 204 205 return false; 206 } 207 208 T[] attempts; 209 if(value != 0) { 210 static if(isSigned!T) attempts ~= -value; 211 attempts ~= value / 2; 212 } 213 if(value < T.max / 2) attempts ~= cast(T)(value * 2); 214 if(value < T.max) attempts ~= cast(T)(value + 1); 215 if(value > T.min) attempts ~= cast(T)(value - 1); 216 217 foreach(attempt; attempts) 218 if(stillFails(attempt)) 219 return shrinkImpl!F(attempt, candidates, oldParams); 220 221 const min = candidates.minPos[0]; 222 const max = candidates.minPos!"a > b"[0]; // maxPos doesn't exist before DMD 2.071.0 223 224 if(!F(min)) return shrinkImpl!F(min, candidates, oldParams); 225 if(!F(max)) return shrinkImpl!F(max, candidates, oldParams); 226 227 return candidates[0]; 228 } 229 230 static assert(canShrink!int); 231 232 233 private T shrinkImpl(alias F, T)(T value, T[] candidates, T[][] oldParams = []) 234 if(from!"std.traits".isArray!T) 235 { 236 if(value == []) return value; 237 238 if(value.length == 1) { 239 T empty; 240 return !F(empty) ? empty : value; 241 } 242 243 auto fst = value[0 .. $ / 2]; 244 auto snd = value[$ / 2 .. $]; 245 if(!F(fst)) return shrinkImpl!F(fst, candidates); 246 if(!F(snd)) return shrinkImpl!F(snd, candidates); 247 248 if(F(value[0 .. $ - 1])) return value[0 .. $ - 1]; 249 if(F(value[1 .. $])) return value[1 .. $]; 250 251 if(!F(value[0 .. $ - 1])) return shrinkImpl!F(value[0 .. $ - 1], candidates); 252 if(!F(value[1 .. $])) return shrinkImpl!F(value[1 .. $], candidates); 253 return candidates[0]; 254 }