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