1 module unit_threaded.check;
2 
3 import std.exception;
4 import std.conv;
5 import std.algorithm;
6 import std.traits;
7 import std.range;
8 
9 public import unit_threaded.attrs;
10 
11 @safe:
12 
13 class UnitTestException: Exception {
14     this(in string[] msgLines, in string file, in ulong line) {
15         import std.array;
16         super(msgLines.map!(a => getOutputPrefix(file, line) ~ a).join("\n"));
17     }
18 
19 private:
20 
21     string getOutputPrefix(in string file, in ulong line) const {
22         return "    " ~ file ~ ":" ~ line.to!string ~ " - ";
23     }
24 }
25 
26 void checkTrue(E)(lazy E condition, in string file = __FILE__, in ulong line = __LINE__) {
27     if(!condition) failEqual(condition, true, file, line);
28 }
29 
30 void checkFalse(E)(lazy E condition, in string file = __FILE__, in ulong line = __LINE__) {
31     if(condition) failEqual(condition, false, file, line);
32 }
33 
34 
35 void checkEqual(T, U)(in T value, in U expected, in string file = __FILE__, in ulong line = __LINE__)
36   if(is(typeof(value != expected) == bool) && !is(T == class) && !is(T == interface)) {
37     if(value != expected) failEqual(value, expected, file, line);
38 }
39 
40 void checkEqual(T)(in T value, in T expected, in string file = __FILE__, in ulong line = __LINE__)
41 if(is(T == class)) {
42     if(value.tupleof != expected.tupleof) failEqual(value, expected, file, line);
43 }
44 
45 void checkEqual(T)(in T value, in T expected, in string file = __FILE__, in ulong line = __LINE__)
46   if(is(T == interface)) {
47 
48     //not allowed to compare interfaces or take their addresses in @safe code
49     () @trusted { if(value != expected) failEqual(value, expected, file, line); }();
50 }
51 
52 
53 
54 //@trusted because of object.opEquals
55 void checkNotEqual(T, U)(in T value, in U expected, in string file = __FILE__, in ulong line = __LINE__) @trusted
56   if(is(typeof(value == expected) == bool)) {
57     if(value == expected) {
58         auto valueStr = value.to!string;
59         static if(is(T == string)) {
60             valueStr = `"` ~ valueStr ~ `"`;
61         }
62         auto expectedStr = expected.to!string;
63         static if(is(U == string)) {
64             expectedStr = `"` ~ expectedStr ~ `"`;
65         }
66 
67         const msg = "Value " ~ valueStr ~ " is not supposed to be equal to " ~ expectedStr ~ "\n";
68         throw new UnitTestException([msg], file, line);
69     }
70 }
71 
72 void checkNull(T)(in T value, in string file = __FILE__, in ulong line = __LINE__) {
73     if(value !is null) fail("Value is null", file, line);
74 }
75 
76 void checkNotNull(T)(in T value, in string file = __FILE__, in ulong line = __LINE__) {
77     if(value is null) fail("Value is null", file, line);
78 }
79 
80 void checkIn(T, U)(in T value, in U container, in string file = __FILE__, in ulong line = __LINE__)
81     if(isAssociativeArray!U)
82 {
83     if(value !in container) {
84         fail("Value " ~ to!string(value) ~ " not in " ~ to!string(container), file, line);
85     }
86 }
87 
88 void checkIn(T, U)(in T value, in U container, in string file = __FILE__, in ulong line = __LINE__)
89     if(!isAssociativeArray!U)
90 {
91     if(!find(container, value)) {
92         fail("Value " ~ to!string(value) ~ " not in " ~ to!string(container), file, line);
93     }
94 }
95 
96 void checkNotIn(T, U)(in T value, in U container, in string file = __FILE__, in ulong line = __LINE__)
97     if(isAssociativeArray!U)
98 {
99     if(value in container) {
100         fail("Value " ~ to!string(value) ~ " is in " ~ to!string(container), file, line);
101     }
102 }
103 
104 void checkNotIn(T, U)(in T value, in U container, in string file = __FILE__, in ulong line = __LINE__)
105     if(!isAssociativeArray!U)
106 {
107     if(find(container, value).length > 0) {
108         fail("Value " ~ to!string(value) ~ " is in " ~ to!string(container), file, line);
109     }
110 }
111 
112 void checkThrown(T: Throwable = Exception, E)(lazy E expr, in string file = __FILE__, in ulong line = __LINE__) {
113     if(!threw!T(expr)) fail("Expression did not throw", file, line);
114 }
115 
116 void checkNotThrown(T: Throwable = Exception, E)(lazy E expr, in string file = __FILE__, in ulong line = __LINE__) {
117     if(threw!T(expr)) fail("Expression threw", file, line);
118 }
119 
120 private bool threw(T: Throwable, E)(lazy E expr) {
121     try {
122         expr();
123     } catch(T e) {
124         return true;
125     }
126 
127     return false;
128 }
129 
130 
131 void utFail(in string output, in string file, in ulong line) {
132     fail(output, file, line);
133 }
134 
135 private void fail(in string output, in string file, in ulong line) {
136     throw new UnitTestException([output], file, line);
137 }
138 
139 private void failEqual(T, U)(in T value, in U expected, in string file, in ulong line) {
140     static if(isArray!T && !isSomeString!T) {
141         const msg = formatArray("Expected: ", expected) ~ formatArray("     Got: ", value);
142     } else {
143         const msg = ["Expected: " ~ formatValue(expected),
144                      "     Got: " ~ formatValue(value)];
145     }
146 
147     throw new UnitTestException(msg, file, line);
148 }
149 
150 private string[] formatArray(T)(in string prefix, in T value) if(isArray!T) {
151     import std.range;
152     auto defaultLines = [prefix ~ value.to!string];
153 
154     static if(!isArray!(ElementType!T)) return defaultLines;
155     else {
156         const maxElementSize = value.empty ? 0 : value.map!(a => a.length).reduce!max;
157         const tooBigForOneLine = (value.length > 5 && maxElementSize > 5) ||
158             maxElementSize > 10;
159         if(!tooBigForOneLine) return  defaultLines;
160         return [prefix ~ "["] ~ value.map!(a => "              " ~ formatValue(a) ~ ",").array ~ "          ]";
161     }
162 }
163 
164 private auto formatValue(T)(T element) {
165     static if(isSomeString!T) {
166         return `"` ~ element.to!string ~ `"`;
167     } else {
168         return () @trusted { return element.to!string; }();
169     }
170 }
171 
172 
173 private void assertCheck(E)(lazy E expression) {
174     assertNotThrown!UnitTestException(expression);
175 }
176 
177 private void assertFail(E)(lazy E expression) {
178     assertThrown!UnitTestException(expression);
179 }
180 
181 
182 unittest {
183     assertCheck(checkTrue(true));
184     assertCheck(checkFalse(false));
185 }
186 
187 
188 unittest {
189     assertCheck(checkEqual(true, true));
190     assertCheck(checkEqual(false, false));
191     assertCheck(checkNotEqual(true, false));
192 
193     assertCheck(checkEqual(1, 1));
194     assertCheck(checkNotEqual(1, 2));
195 
196     assertCheck(checkEqual("foo", "foo"));
197     assertCheck(checkNotEqual("f", "b"));
198 
199     assertCheck(checkEqual(1.0, 1.0));
200     assertCheck(checkNotEqual(1.0, 2.0));
201 
202     assertCheck(checkEqual([2, 3], [2, 3]));
203     assertCheck(checkNotEqual([2, 3], [2, 3, 4]));
204 
205     int[] ints = [ 1, 2, 3];
206     byte[] bytes = [ 1, 2, 3];
207     byte[] bytes2 = [ 1, 2, 4];
208     assertCheck(checkEqual(ints, bytes));
209     assertCheck(checkEqual(bytes, ints));
210     assertCheck(checkNotEqual(ints, bytes2));
211 
212     assertCheck(checkEqual([1: 2.0, 2: 4.0], [1: 2.0, 2: 4.0]));
213     assertCheck(checkNotEqual([1: 2.0, 2: 4.0], [1: 2.2, 2: 4.0]));
214     const constIntToInts = [ 1:2, 3: 7, 9: 345];
215     auto intToInts = [ 1:2, 3: 7, 9: 345];
216     assertCheck(checkEqual(intToInts, constIntToInts));
217     assertCheck(checkEqual(constIntToInts, intToInts));
218 }
219 
220 unittest {
221     assertCheck(checkNull(null));
222     class Foo { }
223     assertCheck(checkNotNull(new Foo));
224 }
225 
226 unittest {
227     assertCheck(checkIn(4, [1, 2, 4]));
228     assertCheck(checkNotIn(3.5, [1.1, 2.2, 4.4]));
229     assertCheck(checkIn("foo", ["foo": 1]));
230     assertCheck(checkNotIn(1.0, [2.0: 1, 3.0: 2]));
231 }
232 
233 
234 void checkEmpty(R)(R rng, in string file = __FILE__, in ulong line = __LINE__) if(isInputRange!R) {
235     if(!rng.empty) fail("Range not empty", file, line);
236 }
237 
238 void checkEmpty(T)(in T aa, in string file = __FILE__, in ulong line = __LINE__) if(isAssociativeArray!T) {
239     //keys is @system
240     () @trusted { if(!aa.keys.empty) fail("AA not empty", file, line); }();
241 }
242 
243 
244 void checkNotEmpty(R)(R rng, in string file = __FILE__, in ulong line = __LINE__) if(isInputRange!R) {
245     if(rng.empty) fail("Range empty", file, line);
246 }
247 
248 
249 void checkNotEmpty(T)(in T aa, in string file = __FILE__, in ulong line = __LINE__) if(isAssociativeArray!T) {
250     //keys is @system
251     () @trusted { if(aa.keys.empty) fail("AA empty", file, line); }();
252 }
253 
254 
255 unittest {
256     int[] ints;
257     string[] strings;
258     string[string] aa;
259 
260     assertCheck(checkEmpty(ints));
261     assertCheck(checkEmpty(strings));
262     assertCheck(checkEmpty(aa));
263 
264     assertFail(checkNotEmpty(ints));
265     assertFail(checkNotEmpty(strings));
266     assertFail(checkNotEmpty(aa));
267 
268 
269     ints ~= 1;
270     strings ~= "foo";
271     aa["foo"] = "bar";
272 
273     assertCheck(checkNotEmpty(ints));
274     assertCheck(checkNotEmpty(strings));
275     assertCheck(checkNotEmpty(aa));
276 
277     assertFail(checkEmpty(ints));
278     assertFail(checkEmpty(strings));
279     assertFail(checkEmpty(aa));
280 }
281 
282 
283 void checkGreaterThan(T, U)(in T t, in U u, in string file = __FILE__, in ulong line = __LINE__) {
284     if(t <= u) fail(text(t, " is not > ", u), file, line);
285 }
286 
287 void checkSmallerThan(T, U)(in T t, in U u, in string file = __FILE__, in ulong line = __LINE__) {
288     if(t >= u) fail(text(t, " is not < ", u), file, line);
289 }
290 
291 unittest {
292     assertCheck(checkGreaterThan(7, 5));
293     assertFail(checkGreaterThan(5, 7));
294     assertFail(checkGreaterThan(7, 7));
295 
296     assertCheck(checkSmallerThan(5, 7));
297     assertFail(checkSmallerThan(7, 5));
298     assertFail(checkSmallerThan(7, 7));
299 }