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 }