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 }