1 module unit_threaded.check;
2 
3 import std.exception;
4 import std.conv;
5 import std.algorithm;
6 import std.traits;
7 
8 public import unit_threaded.attrs;
9 
10 class UnitTestException: Exception {
11     this(in string msgLines[], in string file, in ulong line) {
12         import std.array;
13         super(msgLines.map!(a => getOutputPrefix(file, line) ~ a).join("\n"));
14     }
15 
16 private:
17 
18     string getOutputPrefix(in string file, in ulong line) {
19         return "    " ~ file ~ ":" ~ line.to!string ~ " - ";
20     }
21 }
22 
23 void checkTrue(E)(lazy E condition, in string file = __FILE__, in ulong line = __LINE__) {
24     if(!condition) failEqual(condition, true, file, line);
25 }
26 
27 void checkFalse(E)(lazy E condition, in string file = __FILE__, in ulong line = __LINE__) {
28     if(condition) failEqual(condition, false, file, line);
29 }
30 
31 void checkEqual(T, U)(in T value, in U expected, in string file = __FILE__, in ulong line = __LINE__)
32 if(is(typeof(value != expected) == bool) && !is(T == class)) {
33     if(value != expected) failEqual(value, expected, file, line);
34 }
35 
36 void checkEqual(T)(in T value, in T expected, in string file = __FILE__, in ulong line = __LINE__)
37 if(is(T == class)) {
38     if(value.tupleof != expected.tupleof) failEqual(value, expected, file, line);
39 }
40 
41 
42 void checkNotEqual(T, U)(in T value, in U expected, in string file = __FILE__, in ulong line = __LINE__)
43 if(is(typeof(value == expected) == bool)) {
44     if(value == expected) {
45         auto valueStr = value.to!string;
46         static if(is(T == string)) {
47             valueStr = `"` ~ valueStr ~ `"`;
48         }
49         auto expectedStr = expected.to!string;
50         static if(is(U == string)) {
51             expectedStr = `"` ~ expectedStr ~ `"`;
52         }
53 
54         const msg = "Value " ~ valueStr ~ " is not supposed to be equal to " ~ expectedStr ~ "\n";
55         throw new UnitTestException([msg], file, line);
56     }
57 }
58 
59 void checkNull(T)(in T value, in string file = __FILE__, in ulong line = __LINE__) {
60     if(value !is null) fail("Value is null", file, line);
61 }
62 
63 void checkNotNull(T)(in T value, in string file = __FILE__, in ulong line = __LINE__) {
64     if(value is null) fail("Value is null", file, line);
65 }
66 
67 void checkIn(T, U)(in T value, in U container, in string file = __FILE__, in ulong line = __LINE__)
68     if(isAssociativeArray!U)
69 {
70     if(value !in container) {
71         fail("Value " ~ to!string(value) ~ " not in " ~ to!string(container), file, line);
72     }
73 }
74 
75 void checkIn(T, U)(in T value, in U container, in string file = __FILE__, in ulong line = __LINE__)
76     if(!isAssociativeArray!U)
77 {
78     if(!find(container, value)) {
79         fail("Value " ~ to!string(value) ~ " not in " ~ to!string(container), file, line);
80     }
81 }
82 
83 void checkNotIn(T, U)(in T value, in U container, in string file = __FILE__, in ulong line = __LINE__)
84     if(isAssociativeArray!U)
85 {
86     if(value in container) {
87         fail("Value " ~ to!string(value) ~ " is in " ~ to!string(container), file, line);
88     }
89 }
90 
91 void checkNotIn(T, U)(in T value, in U container, in string file = __FILE__, in ulong line = __LINE__)
92     if(!isAssociativeArray!U)
93 {
94     if(find(container, value).length > 0) {
95         fail("Value " ~ to!string(value) ~ " is in " ~ to!string(container), file, line);
96     }
97 }
98 
99 void checkThrown(T: Throwable = Exception, E)(lazy E expr, in string file = __FILE__, in ulong line = __LINE__) {
100     if(!threw!T(expr)) fail("Expression did not throw", file, line);
101 }
102 
103 void checkNotThrown(T: Throwable = Exception, E)(lazy E expr, in string file = __FILE__, in ulong line = __LINE__) {
104     if(threw!T(expr)) fail("Expression threw", file, line);
105 }
106 
107 private bool threw(T: Throwable, E)(lazy E expr) {
108     try {
109         expr();
110     } catch(T e) {
111         return true;
112     }
113 
114     return false;
115 }
116 
117 
118 void utFail(in string output, in string file, in ulong line) {
119     fail(output, file, line);
120 }
121 
122 private void fail(in string output, in string file, in ulong line) {
123     throw new UnitTestException([output], file, line);
124 }
125 
126 private void failEqual(T, U)(in T value, in U expected, in string file, in ulong line) {
127     static if(isArray!T && !isSomeString!T) {
128         const msg = formatArray("Expected: ", expected) ~ formatArray("     Got: ", value);
129     } else {
130         const msg = ["Expected: " ~ formatValue(expected),
131                      "     Got: " ~ formatValue(value)];
132     }
133 
134     throw new UnitTestException(msg, file, line);
135 }
136 
137 private string[] formatArray(T)(in string prefix, in T value) if(isArray!T) {
138     import std.range;
139     auto defaultLines = [prefix ~ value.to!string];
140 
141     static if(!isArray!(ElementType!T)) return defaultLines;
142     else {
143         const maxElementSize = value.empty ? 0 : value.map!(a => a.length).reduce!max;
144         const tooBigForOneLine = (value.length > 5 && maxElementSize > 5) ||
145             maxElementSize > 10;
146         if(!tooBigForOneLine) return  defaultLines;
147         return [prefix ~ "["] ~ value.map!(a => "              " ~ formatValue(a) ~ ",").array ~ "          ]";
148     }
149 }
150 
151 private auto formatValue(T)(T element) {
152     static if(isSomeString!T) {
153         return `"` ~ element.to!string ~ `"`;
154     } else {
155         return element.to!string;
156     }
157 }
158 
159 
160 private void assertCheck(E)(lazy E expression) {
161     assertNotThrown!UnitTestException(expression);
162 }
163 
164 unittest {
165     assertCheck(checkTrue(true));
166     assertCheck(checkFalse(false));
167 }
168 
169 
170 unittest {
171     assertCheck(checkEqual(true, true));
172     assertCheck(checkEqual(false, false));
173     assertCheck(checkNotEqual(true, false));
174 
175     assertCheck(checkEqual(1, 1));
176     assertCheck(checkNotEqual(1, 2));
177 
178     assertCheck(checkEqual("foo", "foo"));
179     assertCheck(checkNotEqual("f", "b"));
180 
181     assertCheck(checkEqual(1.0, 1.0));
182     assertCheck(checkNotEqual(1.0, 2.0));
183 
184     assertCheck(checkEqual([2, 3], [2, 3]));
185     assertCheck(checkNotEqual([2, 3], [2, 3, 4]));
186 
187     int[] ints = [ 1, 2, 3];
188     byte[] bytes = [ 1, 2, 3];
189     byte[] bytes2 = [ 1, 2, 4];
190     assertCheck(checkEqual(ints, bytes));
191     assertCheck(checkEqual(bytes, ints));
192     assertCheck(checkNotEqual(ints, bytes2));
193 
194     assertCheck(checkEqual([1: 2.0, 2: 4.0], [1: 2.0, 2: 4.0]));
195     assertCheck(checkNotEqual([1: 2.0, 2: 4.0], [1: 2.2, 2: 4.0]));
196     const constIntToInts = [ 1:2, 3: 7, 9: 345];
197     auto intToInts = [ 1:2, 3: 7, 9: 345];
198     assertCheck(checkEqual(intToInts, constIntToInts));
199     assertCheck(checkEqual(constIntToInts, intToInts));
200 }
201 
202 unittest {
203     assertCheck(checkNull(null));
204     class Foo { }
205     assertCheck(checkNotNull(new Foo));
206 }
207 
208 unittest {
209     assertCheck(checkIn(4, [1, 2, 4]));
210     assertCheck(checkNotIn(3.5, [1.1, 2.2, 4.4]));
211     assertCheck(checkIn("foo", ["foo": 1]));
212     assertCheck(checkNotIn(1.0, [2.0: 1, 3.0: 2]));
213 }