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 private auto shrinkOne(alias F, int index, T)(T values) { 74 import std.stdio; 75 import std.traits; 76 auto orig = values[index]; 77 return shrink!((a) { 78 values[index] = a; 79 return F(values.expand); 80 })(orig); 81 82 } 83 84 @("Verify identity property for int[] succeeds") 85 @safe unittest { 86 int numCalls; 87 bool identity(int[] a) pure { 88 ++numCalls; 89 return a == a; 90 } 91 92 check!identity; 93 numCalls.shouldEqual(100); 94 95 numCalls = 0; 96 check!identity(10); 97 numCalls.shouldEqual(10); 98 } 99 100 @("Verify anti-identity property for int[] fails") 101 @safe unittest { 102 int numCalls; 103 bool antiIdentity(int[] a) { 104 ++numCalls; 105 return a != a; 106 } 107 108 check!antiIdentity.shouldThrow!UnitTestException; 109 // gets called twice due to shrinking 110 numCalls.shouldEqual(2); 111 } 112 113 @("Verify property that sometimes succeeds") 114 @safe unittest { 115 // 2^100 is ~1.26E30, so the chances that no even length array is generated 116 // is small enough to disconsider even if it were truly random 117 // since Gen!int[] is front-loaded, it'll fail deterministically 118 assertExceptionMsg(check!((int[] a) => a.length % 2 == 1), 119 " source/unit_threaded/property/package.d:123 - Property failed with input:\n" ~ 120 " source/unit_threaded/property/package.d:123 - []"); 121 } 122 123 124 @("Explicit Gen") 125 @safe unittest { 126 check!((Gen!(int, 1, 1) a) => a == 1); 127 check!((Gen!(int, 1, 1) a) => a == 2).shouldThrow!UnitTestException; 128 } 129 130 private enum canShrink(T) = __traits(compiles, shrink!((T _) => true)(T.init)); 131 132 T shrink(alias F, T)(T value) { 133 import std.conv: text; 134 assert(!F(value), text("Property did not fail for value ", value)); 135 return shrinkImpl!F(value, [value]); 136 } 137 138 private T shrinkImpl(alias F, T)(T value, T[] values) if(isIntegral!T) { 139 import std.algorithm: canFind, minPos; 140 import std.traits: isSigned; 141 142 if(value < T.max && F(value + 1)) return value; 143 if(value > T.min && F(value - 1)) return value; 144 145 bool try_(T attempt) { 146 if(!F(attempt) && !values.canFind(attempt)) { 147 values ~= attempt; 148 return true; 149 } 150 151 return false; 152 } 153 154 T[] attempts; 155 static if(isSigned!T) attempts ~= -value; 156 attempts ~= value / 2; 157 if(value < T.max / 2) attempts ~= cast(T)(value * 2); 158 if(value < T.max) attempts ~= cast(T)(value + 1); 159 if(value > T.min) attempts ~= cast(T)(value - 1); 160 161 foreach(attempt; attempts) 162 if(try_(attempt)) 163 return shrinkImpl!F(attempt, values); 164 165 auto min = values.minPos[0]; 166 auto max = values.minPos!"a > b"[0]; // maxPos doesn't exist before DMD 2.071.0 167 168 if(!F(min)) return shrinkImpl!F(min, values); 169 if(!F(max)) return shrinkImpl!F(max, values); 170 171 return values[0]; 172 } 173 174 static assert(canShrink!int); 175 176 @("shrink int when already shrunk") 177 @safe pure unittest { 178 assertEqual(0.shrink!(a => a != 0), 0); 179 } 180 181 182 @("shrink int when not already shrunk going up") 183 @safe pure unittest { 184 assertEqual(0.shrink!(a => a > 3), 3); 185 } 186 187 @("shrink int when not already shrunk going down") 188 @safe pure unittest { 189 assertEqual(10.shrink!(a => a < -3), -3); 190 } 191 192 @("shrink int.max") 193 @safe pure unittest { 194 assertEqual(int.max.shrink!(a => a == 0), 1); 195 assertEqual(int.min.shrink!(a => a == 0), -1); 196 assertEqual(int.max.shrink!(a => a < 3), 3); 197 } 198 199 @("shrink unsigneds") 200 @safe pure unittest { 201 import std.meta; 202 foreach(T; AliasSeq!(ubyte, ushort, uint, ulong)) { 203 T value = 3; 204 assertEqual(value.shrink!(a => a == 0), 1); 205 } 206 } 207 208 private T shrinkImpl(alias F, T)(T value, T[] values) if(isArray!T) { 209 if(value == []) return value; 210 211 if(value.length == 1) { 212 T empty; 213 return !F(empty) ? empty : value; 214 } 215 216 auto fst = value[0 .. $ / 2]; 217 auto snd = value[$ / 2 .. $]; 218 if(!F(fst)) return shrinkImpl!F(fst, values); 219 if(!F(snd)) return shrinkImpl!F(snd, values); 220 221 if(F(value[0 .. $ - 1])) return value[0 .. $ - 1]; 222 if(F(value[1 .. $])) return value[1 .. $]; 223 224 if(!F(value[0 .. $ - 1])) return shrinkImpl!F(value[0 .. $ - 1], values); 225 if(!F(value[1 .. $])) return shrinkImpl!F(value[1 .. $], values); 226 return values[0]; 227 } 228 229 @("shrink empty int array") 230 @safe pure unittest { 231 int[] value; 232 assertEqual(value.shrink!(a => a != []), value); 233 } 234 235 @("shrink int array") 236 @safe pure unittest { 237 assertEqual([1, 2, 3].shrink!(a => a == []), [1]); 238 } 239 240 @("shrink string") 241 @safe pure unittest { 242 import std.algorithm: canFind; 243 assertEqual("abcdef".shrink!(a => !a.canFind("e")), "e"); 244 } 245 246 @("shrink one item with check") 247 unittest { 248 assertEqual("ǭĶƶØľĶĄÔ0".shrink!((s) => s.length < 3 || s[2] == 'e'), "ǭ"); 249 } 250 251 @("shrink one item with check") 252 unittest { 253 assertExceptionMsg(check!((int i) => i < 3), 254 " source/unit_threaded/property/package.d:123 - Property failed with input:\n" ~ 255 " source/unit_threaded/property/package.d:123 - 3"); 256 } 257 258 @("string[]") 259 unittest { 260 bool identity(string[] a) pure { 261 return a == a; 262 } 263 check!identity; 264 }