1 /**
2    Property-based testing.
3  */
4 module unit_threaded.property;
5 
6 import unit_threaded.from;
7 
8 ///
9 class PropertyException : Exception
10 {
11     this(in string msg, string file = __FILE__,
12          size_t line = __LINE__, Throwable next = null) @safe pure nothrow
13     {
14         super(msg, file, line, next);
15     }
16 }
17 
18 /**
19    Check that bool-returning F is true with randomly generated values.
20  */
21 void check(alias F, int numFuncCalls = 100)
22           (in uint seed = from!"std.random".unpredictableSeed,
23            string file = __FILE__,
24            size_t line = __LINE__)
25 {
26     import unit_threaded.randomized.random: RndValueGen;
27     import unit_threaded.exception: UnitTestException;
28     import std.conv: text;
29     import std.traits: ReturnType, Parameters, isSomeString, isSafe;
30     import std.array: join;
31     import std.typecons: Flag, Yes, No;
32     import std.random: Random;
33 
34     static assert(is(ReturnType!F == bool),
35                   text("check only accepts functions that return bool, not ", ReturnType!F.stringof));
36 
37     // `Random` could be put on the stack, but only with -dip1000:
38     // https://github.com/atilaneves/unit-threaded/issues/187
39     // With -dip1000, https://issues.dlang.org/show_bug.cgi?id=20150 manifested here
40     auto gen = RndValueGen!(Parameters!F)(new Random(seed));
41 
42     auto input(Flag!"shrink" shrink = Yes.shrink) {
43 
44         scope string[] ret;
45 
46         static if(Parameters!F.length == 1 && canShrink!(Parameters!F[0])) {
47             auto val = gen.values[0].value;
48             auto shrunk = shrink ? val.shrink!F : val;
49             ret ~= shrunk.text.idup;
50             static if(isSomeString!(Parameters!F[0]))
51                 ret[$-1] = `"` ~ ret[$-1] ~ `"`;
52         } else {
53             foreach(ref valueGen; gen.values) {
54                 ret ~= valueGen.text;
55             }
56         }
57 
58         return ret.join(", ");
59     }
60 
61     foreach(i; 0 .. numFuncCalls) {
62         bool pass;
63 
64         safelyCatchThrowable!(
65             { gen.genValues; },
66             (Throwable t) @trusted { throw new PropertyException("Error generating values\n" ~ t.toString, file, line, t); }
67         );
68 
69         pass = safelyCatchThrowable!(
70             () => F(gen.values),
71             (Throwable t) @trusted {
72                 throw new UnitTestException(
73                     text("Property threw. Seed: ", seed, ". Input: ", input(No.shrink), ". Message: ", t.msg),
74                     file,
75                     line,
76                     t,
77                     );
78             }
79         );
80 
81         if(!pass)
82             throw new UnitTestException(text("Property failed. Seed: ", seed, ". Input: ", input), file, line);
83     }
84 }
85 
86 
87 private auto safelyCatchThrowable(alias Func, alias Handler, A...)(auto ref A args) {
88     import std.traits: isSafe, ReturnType;
89 
90     ReturnType!Func impl() {
91         try
92             return Func(args);
93         catch(Throwable t)
94             Handler(t);
95         assert(0);
96     }
97 
98     static if(isSafe!Func && isSafe!Handler)
99         return () @trusted { return impl; }();
100     else
101         return impl;
102 }
103 
104 
105 /**
106    For values that unit-threaded doesn't know how to generate, test that the Predicate
107    holds, using Generator to come up with new values.
108  */
109 void checkCustom(alias Generator, alias Predicate)
110                 (int numFuncCalls = 100, in string file = __FILE__, in size_t line = __LINE__) @trusted {
111 
112     import unit_threaded.exception: UnitTestException;
113     import std.conv: text;
114     import std.traits: ReturnType;
115 
116     static assert(is(ReturnType!Predicate == bool),
117                   text("check only accepts functions that return bool, not ", ReturnType!F.stringof));
118 
119     alias Type = ReturnType!Generator;
120 
121     foreach(i; 0 .. numFuncCalls) {
122 
123         Type object;
124 
125         try {
126             object = Generator();
127         } catch(Throwable t) {
128             throw new PropertyException("Error generating value\n" ~ t.toString, file, line, t);
129         }
130 
131         bool pass;
132 
133         try {
134             pass = Predicate(object);
135         } catch(Throwable t) {
136             throw new UnitTestException(
137                 text("Property threw. Input: ", object, ". Message: ", t.msg),
138                 file,
139                 line,
140                 t,
141             );
142         }
143 
144         if(!pass) {
145             throw new UnitTestException("Property failed with input:" ~ object.text, file, line);
146         }
147     }
148 }
149 
150 
151 private auto shrinkOne(alias F, int index, T)(T values) {
152     import std.stdio;
153     import std.traits;
154     auto orig = values[index];
155     return shrink!((a) {
156         values[index] = a;
157         return F(values.expand);
158     })(orig);
159 
160 }
161 
162 ///
163 @("Verify identity property for int[] succeeds")
164 @safe unittest {
165 
166     int numCalls;
167     bool identity(int[] a) pure {
168         ++numCalls;
169         return a == a;
170     }
171 
172     check!identity;
173     assert(numCalls == 100);
174 
175     numCalls = 0;
176     check!(identity, 10);
177     assert(numCalls == 10);
178 }
179 
180 ///
181 @("Explicit Gen")
182 @safe unittest {
183     import unit_threaded.randomized.gen;
184     import unit_threaded.exception: UnitTestException;
185     import std.exception: assertThrown;
186 
187     check!((Gen!(int, 1, 1) a) => a == 1);
188     assertThrown!UnitTestException(check!((Gen!(int, 1, 1) a) => a == 2));
189 }
190 
191 private enum canShrink(T) = __traits(compiles, shrink!((T _) => true)(T.init));
192 
193 T shrink(alias F, T)(T value) {
194     import std.conv: text;
195 
196     assert(!F(value), text("Property did not fail for value ", value));
197 
198     T[][] oldParams;
199     return shrinkImpl!F(value, [value], oldParams);
200 }
201 
202 private T shrinkImpl(alias F, T)(in T value, T[] candidates, T[][] oldParams = [])
203     if(from!"std.traits".isIntegral!T)
204 {
205     import std.algorithm: canFind, minPos;
206     import std.traits: isSigned;
207 
208     auto params = value ~ candidates;
209     if(oldParams.canFind(params)) return value;
210     oldParams ~= params;
211 
212     // if it suddenly starts passing we've found our boundary value
213     if(value < T.max && F(value + 1)) return value;
214     if(value > T.min && F(value - 1)) return value;
215 
216     bool stillFails(T attempt) {
217         if(!F(attempt) && !candidates.canFind(attempt)) {
218             candidates ~= attempt;
219             return true;
220         }
221 
222         return false;
223     }
224 
225     T[] attempts;
226     if(value != 0) {
227         static if(isSigned!T) attempts ~= -value;
228         attempts ~= value / 2;
229     }
230     if(value < T.max / 2) attempts ~= cast(T)(value * 2);
231     if(value < T.max) attempts ~= cast(T)(value + 1);
232     if(value > T.min) attempts ~= cast(T)(value - 1);
233 
234     foreach(attempt; attempts)
235         if(stillFails(attempt))
236             return shrinkImpl!F(attempt, candidates, oldParams);
237 
238     const min = candidates.minPos[0];
239     const max = candidates.minPos!"a > b"[0]; // maxPos doesn't exist before DMD 2.071.0
240 
241     if(!F(min)) return shrinkImpl!F(min, candidates, oldParams);
242     if(!F(max)) return shrinkImpl!F(max, candidates, oldParams);
243 
244     return candidates[0];
245 }
246 
247 static assert(canShrink!int);
248 
249 
250 private T shrinkImpl(alias F, T)(T value, T[] candidates, T[][] oldParams = [])
251     if(from!"std.traits".isArray!T)
252 {
253     if(value == []) return value;
254 
255     if(value.length == 1) {
256         T empty;
257         return !F(empty) ? empty : value;
258     }
259 
260     auto fst = value[0 .. $ / 2];
261     auto snd = value[$ / 2 .. $];
262     if(!F(fst)) return shrinkImpl!F(fst, candidates);
263     if(!F(snd)) return shrinkImpl!F(snd, candidates);
264 
265     if(F(value[0 .. $ - 1])) return value[0 .. $ - 1];
266     if(F(value[1 .. $])) return value[1 .. $];
267 
268     if(!F(value[0 .. $ - 1])) return shrinkImpl!F(value[0 .. $ - 1], candidates);
269     if(!F(value[1 .. $])) return shrinkImpl!F(value[1 .. $], candidates);
270     return candidates[0];
271 }