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