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 }