1 module unit_threaded.property; 2 3 import std.random: Random; 4 import std.traits: isIntegral, isArray; 5 6 7 version(unittest) import unit_threaded.asserts; 8 9 Random gRandom; 10 11 12 static this() { 13 import std.random: unpredictableSeed; 14 gRandom = Random(unpredictableSeed); 15 } 16 17 18 class PropertyException : Exception 19 { 20 this(in string msg, string file = __FILE__, 21 size_t line = __LINE__, Throwable next = null) @safe pure nothrow 22 { 23 super(msg, file, line, next); 24 } 25 } 26 27 void check(alias F)(int numFuncCalls = 100, 28 in string file = __FILE__, in size_t line = __LINE__) @trusted { 29 30 import unit_threaded.randomized.random: RndValueGen; 31 import unit_threaded.should: UnitTestException; 32 import std.conv: text, to; 33 import std.traits: ReturnType, Parameters, isSomeString; 34 import std.array: join; 35 36 static assert(is(ReturnType!F == bool), 37 text("check only accepts functions that return bool, not ", ReturnType!F.stringof)); 38 39 auto gen = RndValueGen!(Parameters!F)(&gRandom); 40 41 foreach(i; 0 .. numFuncCalls) { 42 bool pass; 43 44 try { 45 gen.genValues; 46 } catch(Throwable t) { 47 throw new PropertyException("Error generating values\n" ~ t.toString, file, line, t); 48 } 49 50 try { 51 pass = F(gen.values); 52 } catch(Throwable t) { 53 throw new PropertyException("Error calling property function\n" ~ t.toString, file, line, t); 54 } 55 56 if(!pass) { 57 string[] input; 58 59 static if(Parameters!F.length == 1 && canShrink!(Parameters!F[0])) { 60 input ~= gen.values[0].value.shrink!F.to!string; 61 static if(isSomeString!(Parameters!F[0])) 62 input[$-1] = `"` ~ input[$-1] ~ `"`; 63 } else 64 foreach(j, ref valueGen; gen.values) { 65 input ~= valueGen.to!string; 66 } 67 68 throw new UnitTestException(["Property failed with input:", input.join(", ")], file, line); 69 } 70 } 71 } 72 73 void checkCustom(alias Generator, alias Predicate) 74 (int numFuncCalls = 100, in string file = __FILE__, in size_t line = __LINE__) @trusted { 75 76 import unit_threaded.should: UnitTestException; 77 import std.conv: to, text; 78 import std.traits: ReturnType; 79 80 static assert(is(ReturnType!Predicate == bool), 81 text("check only accepts functions that return bool, not ", ReturnType!F.stringof)); 82 83 alias Type = ReturnType!Generator; 84 85 foreach(i; 0 .. numFuncCalls) { 86 87 Type object; 88 89 try { 90 object = Generator(); 91 } catch(Throwable t) { 92 throw new PropertyException("Error generating value\n" ~ t.toString, file, line, t); 93 } 94 95 bool pass; 96 97 try { 98 pass = Predicate(object); 99 } catch(Throwable t) { 100 throw new PropertyException("Error calling property function\n" ~ t.toString, file, line, t); 101 } 102 103 if(!pass) { 104 throw new UnitTestException(["Property failed with input:", object.to!string], file, line); 105 } 106 } 107 } 108 109 110 private auto shrinkOne(alias F, int index, T)(T values) { 111 import std.stdio; 112 import std.traits; 113 auto orig = values[index]; 114 return shrink!((a) { 115 values[index] = a; 116 return F(values.expand); 117 })(orig); 118 119 } 120 121 @("Verify identity property for int[] succeeds") 122 @safe unittest { 123 import unit_threaded.should; 124 int numCalls; 125 bool identity(int[] a) pure { 126 ++numCalls; 127 return a == a; 128 } 129 130 check!identity; 131 numCalls.shouldEqual(100); 132 133 numCalls = 0; 134 check!identity(10); 135 numCalls.shouldEqual(10); 136 } 137 138 @("Verify anti-identity property for int[] fails") 139 @safe unittest { 140 import unit_threaded.should; 141 int numCalls; 142 bool antiIdentity(int[] a) { 143 ++numCalls; 144 return a != a; 145 } 146 147 check!antiIdentity.shouldThrow!UnitTestException; 148 // gets called twice due to shrinking 149 numCalls.shouldEqual(2); 150 } 151 152 @("Verify property that sometimes succeeds") 153 @safe unittest { 154 // 2^100 is ~1.26E30, so the chances that no even length array is generated 155 // is small enough to disconsider even if it were truly random 156 // since Gen!int[] is front-loaded, it'll fail deterministically 157 assertExceptionMsg(check!((int[] a) => a.length % 2 == 1), 158 " source/unit_threaded/property.d:123 - Property failed with input:\n" ~ 159 " source/unit_threaded/property.d:123 - []"); 160 } 161 162 163 @("Explicit Gen") 164 @safe unittest { 165 import unit_threaded.randomized.gen; 166 import unit_threaded.should; 167 168 check!((Gen!(int, 1, 1) a) => a == 1); 169 check!((Gen!(int, 1, 1) a) => a == 2).shouldThrow!UnitTestException; 170 } 171 172 private enum canShrink(T) = __traits(compiles, shrink!((T _) => true)(T.init)); 173 174 T shrink(alias F, T)(T value) { 175 import std.conv: text; 176 assert(!F(value), text("Property did not fail for value ", value)); 177 return shrinkImpl!F(value, [value]); 178 } 179 180 private T shrinkImpl(alias F, T)(T value, T[] values) if(isIntegral!T) { 181 import std.algorithm: canFind, minPos; 182 import std.traits: isSigned; 183 184 if(value < T.max && F(value + 1)) return value; 185 if(value > T.min && F(value - 1)) return value; 186 187 bool try_(T attempt) { 188 if(!F(attempt) && !values.canFind(attempt)) { 189 values ~= attempt; 190 return true; 191 } 192 193 return false; 194 } 195 196 T[] attempts; 197 static if(isSigned!T) attempts ~= -value; 198 attempts ~= value / 2; 199 if(value < T.max / 2) attempts ~= cast(T)(value * 2); 200 if(value < T.max) attempts ~= cast(T)(value + 1); 201 if(value > T.min) attempts ~= cast(T)(value - 1); 202 203 foreach(attempt; attempts) 204 if(try_(attempt)) 205 return shrinkImpl!F(attempt, values); 206 207 auto min = values.minPos[0]; 208 auto max = values.minPos!"a > b"[0]; // maxPos doesn't exist before DMD 2.071.0 209 210 if(!F(min)) return shrinkImpl!F(min, values); 211 if(!F(max)) return shrinkImpl!F(max, values); 212 213 return values[0]; 214 } 215 216 static assert(canShrink!int); 217 218 @("shrink int when already shrunk") 219 @safe pure unittest { 220 assertEqual(0.shrink!(a => a != 0), 0); 221 } 222 223 224 @("shrink int when not already shrunk going up") 225 @safe pure unittest { 226 assertEqual(0.shrink!(a => a > 3), 3); 227 } 228 229 @("shrink int when not already shrunk going down") 230 @safe pure unittest { 231 assertEqual(10.shrink!(a => a < -3), -3); 232 } 233 234 @("shrink int.max") 235 @safe pure unittest { 236 assertEqual(int.max.shrink!(a => a == 0), 1); 237 assertEqual(int.min.shrink!(a => a == 0), -1); 238 assertEqual(int.max.shrink!(a => a < 3), 3); 239 } 240 241 @("shrink unsigneds") 242 @safe pure unittest { 243 import std.meta; 244 foreach(T; AliasSeq!(ubyte, ushort, uint, ulong)) { 245 T value = 3; 246 assertEqual(value.shrink!(a => a == 0), 1); 247 } 248 } 249 250 private T shrinkImpl(alias F, T)(T value, T[] values) if(isArray!T) { 251 if(value == []) return value; 252 253 if(value.length == 1) { 254 T empty; 255 return !F(empty) ? empty : value; 256 } 257 258 auto fst = value[0 .. $ / 2]; 259 auto snd = value[$ / 2 .. $]; 260 if(!F(fst)) return shrinkImpl!F(fst, values); 261 if(!F(snd)) return shrinkImpl!F(snd, values); 262 263 if(F(value[0 .. $ - 1])) return value[0 .. $ - 1]; 264 if(F(value[1 .. $])) return value[1 .. $]; 265 266 if(!F(value[0 .. $ - 1])) return shrinkImpl!F(value[0 .. $ - 1], values); 267 if(!F(value[1 .. $])) return shrinkImpl!F(value[1 .. $], values); 268 return values[0]; 269 } 270 271 @("shrink empty int array") 272 @safe pure unittest { 273 int[] value; 274 assertEqual(value.shrink!(a => a != []), value); 275 } 276 277 @("shrink int array") 278 @safe pure unittest { 279 assertEqual([1, 2, 3].shrink!(a => a == []), [1]); 280 } 281 282 @("shrink string") 283 @safe pure unittest { 284 import std.algorithm: canFind; 285 assertEqual("abcdef".shrink!(a => !a.canFind("e")), "e"); 286 } 287 288 @("shrink one item with check") 289 unittest { 290 assertEqual("ǭĶƶØľĶĄÔ0".shrink!((s) => s.length < 3 || s[2] == 'e'), "ǭ"); 291 } 292 293 @("shrink one item with check") 294 unittest { 295 assertExceptionMsg(check!((int i) => i < 3), 296 " source/unit_threaded/property.d:123 - Property failed with input:\n" ~ 297 " source/unit_threaded/property.d:123 - 3"); 298 } 299 300 @("string[]") 301 unittest { 302 bool identity(string[] a) pure { 303 return a == a; 304 } 305 check!identity; 306 }