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