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