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