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