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