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 }