1 module dshould.basic;
2 
3 import std..string : empty;
4 import dshould.ShouldType;
5 public import dshould.ShouldType : should;
6 
7 pure @safe unittest
8 {
9     2.should.be(2);
10     2.should.not.be(5);
11     5.should.equal(5);
12     5.should.not.equal(6);
13     5.should.not.be.equal.greater(6);
14     5.should.be.equal.greater(5);
15     5.should.be.equal.greater(4);
16     5.should.not.be.equal.greater(6);
17     5.should.be.smaller(6);
18 }
19 
20 unittest
21 {
22     (new Object).should.not.be(new Object);
23     (new Object).should.not.equal(new Object);
24     (new Object).should.not.be(null);
25     (cast(Object) null).should.be(null);
26 
27     auto obj = new Object;
28 
29     obj.should.equal(obj);
30     obj.should.be(obj);
31 
32     class SameyClass
33     {
34         override bool opEquals(Object o) { return true; }
35     }
36 
37     (new SameyClass).should.not.be(new SameyClass);
38     (new SameyClass).should.equal(new SameyClass);
39 
40     (cast(void delegate()) null).should.be(null);
41 }
42 
43 auto not(Should)(Should should) pure
44 if (isInstanceOf!(ShouldType, Should))
45 {
46     should.allowOnlyWordsBefore!([], "not");
47 
48     return should.addWord!"not";
49 }
50 
51 auto be(Should)(Should should) pure
52 if (isInstanceOf!(ShouldType, Should))
53 {
54     should.allowOnlyWordsBefore!(["not"], "be");
55 
56     return should.addWord!"be";
57 }
58 
59 void be(Should, T)(Should should, T value, string file = __FILE__, size_t line = __LINE__)
60 if (isInstanceOf!(ShouldType, Should) && !should.hasWord!"approximately")
61 {
62     import std.format : format;
63 
64     should.allowOnlyWordsBefore!(["not"], "be");
65 
66     enum isNullType = is(T == typeof(null));
67     // only types that can have toString need to disambiguate
68     enum isReferenceType = is(T == class) || is(T == interface);
69 
70     with (should.addData!"rhs"(value))
71     {
72         static if (hasWord!"not")
73         {
74             const refInfo = isReferenceType ? "different reference than " : "not ";
75 
76             static if (isNullType)
77             {
78                 check(data.lhs() !is null, format(": expected non-null, but got null"), file, line);
79             }
80             else
81             {
82                 auto lhs = data.lhs();
83                 auto rhs = data.rhs;
84 
85                 check(lhs !is rhs, format(": expected %s%s, but got %s", refInfo, rhs.quote, lhs.quote), file, line);
86             }
87         }
88         else
89         {
90             const refInfo = isReferenceType ? "same reference as " : "";
91             auto lhs = data.lhs();
92             auto rhs = data.rhs;
93 
94             static if (is(T == typeof(null)))
95             {
96                 check(lhs is null, format(": expected null, but got %s", lhs.quote), file, line);
97             }
98             else
99             {
100 
101                 check(lhs is rhs, format(": expected %s%s, but got %s", refInfo, rhs.quote, lhs.quote), file, line);
102             }
103         }
104     }
105 }
106 
107 auto equal(Should)(Should should)
108 if (isInstanceOf!(ShouldType, Should))
109 {
110     should.allowOnlyWordsBefore!(["not", "be", "greater", "smaller"], "equal");
111 
112     return should.addWord!"equal";
113 }
114 
115 void equal(Should, T)(Should should, T value, string file = __FILE__, size_t line = __LINE__)
116 if (isInstanceOf!(ShouldType, Should) && !should.hasWord!"approximately")
117 {
118     should.equal.addData!"rhs"(value).numericCheck(file, line);
119 }
120 
121 auto greater(Should)(Should should)
122 if (isInstanceOf!(ShouldType, Should))
123 {
124     should.allowOnlyWordsBefore!(["not", "be", "equal", "smaller"], "greater");
125     should.requireWord!("be", "greater");
126 
127     return should.addWord!"greater";
128 }
129 
130 void greater(Should, T)(Should should, T value, string file = __FILE__, size_t line = __LINE__)
131 if (isInstanceOf!(ShouldType, Should))
132 {
133     should.greater.addData!"rhs"(value).numericCheck(file, line);
134 }
135 
136 auto smaller(Should)(Should should)
137 if (isInstanceOf!(ShouldType, Should))
138 {
139     should.allowOnlyWordsBefore!(["not", "be", "equal", "greater"], "smaller");
140     should.requireWord!("be", "smaller");
141 
142     return should.addWord!"smaller";
143 }
144 
145 void smaller(Should, T)(Should should, T value, string file = __FILE__, size_t line = __LINE__)
146 if (isInstanceOf!(ShouldType, Should))
147 {
148     should.smaller.addData!"rhs"(value).numericCheck(file, line);
149 }
150 
151 void numericCheck(Should)(Should should, string file, size_t line)
152 if (isInstanceOf!(ShouldType, Should))
153 {
154     import std.format : format;
155 
156     enum notPart = should.hasWord!"not" ? "!" : "";
157     enum equalPart = should.hasWord!"equal" ? "==" : "";
158     enum equalPartShort = should.hasWord!"equal" ? "=" : "";
159     enum smallerPart = should.hasWord!"smaller" ? "<" : "";
160     enum greaterPart = should.hasWord!"greater" ? ">" : "";
161 
162     enum isFloating = __traits(isFloating, typeof(should.data.lhs()));
163 
164     static if (isFloating)
165     {
166         enum combinedWithoutEqual = notPart ~ smallerPart ~ greaterPart;
167     }
168     else
169     {
170         enum combinedWithoutEqual = smallerPart ~ greaterPart;
171     }
172 
173     enum combined = combinedWithoutEqual ~ (combinedWithoutEqual.empty ? equalPart : equalPartShort);
174 
175     static if (isFloating || !should.hasWord!"not")
176     {
177         enum checkString = "%s " ~ combined ~ " %s";
178         enum message = combined ~ " %s";
179     }
180     else
181     {
182         enum checkString = "!(%s " ~ combined ~ " %s)";
183         enum message = "not " ~ combined ~ " %s";
184     }
185 
186     with (should)
187     {
188         auto lhs = data.lhs();
189         auto rhs = data.rhs;
190 
191         check(
192             mixin(format!checkString("lhs", "rhs")),
193             format(": expected value %s, but got %s", message.format(rhs.quote), lhs.quote),
194             file, line
195         );
196     }
197 }
198 
199 /**
200  * This could be in a separate file, say approx.d,
201  * if doing so didn't crash dmd.
202  * see https://issues.dlang.org/show_bug.cgi?id=18839
203  */
204 unittest
205 {
206     5.should.be.approximately(5.1, 0.11);
207     5.should.approximately(0.11).be(5.1);
208     0.should.approximately(1.1).equal(1.0);
209     0.should.approximately(1.1).equal(-1.0);
210     0.should.not.approximately(0.1).equal(1);
211     42.3.should.be.approximately(42.3, 1e-3);
212 }
213 
214 auto approximately(Should)(Should should, double permissibleError)
215 if (isInstanceOf!(ShouldType, Should))
216 {
217     return should.addWord!"approximately".addData!"permissibleError"(permissibleError);
218 }
219 
220 auto approximately(Should)(
221     Should should, double value, double permissibleError,
222     string file = __FILE__, size_t line = __LINE__
223 )
224 if (isInstanceOf!(ShouldType, Should))
225 {
226     static assert(
227         should.hasWord!"be" || should.hasWord!"equal",
228         `bad grammar: expected "be" or "equal" before "approximately"`
229     );
230 
231     should.allowOnlyWordsBefore!(["be", "equal", "not"], "approximately");
232 
233     return should
234         .addWord!"approximately"
235         .addData!"permissibleError"(permissibleError)
236         .addData!"rhs"(value)
237         .approximateCheck(file, line);
238 }
239 
240 void be(Should, T)(Should should, T value, string file = __FILE__, size_t line = __LINE__)
241 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"approximately")
242 {
243     import std.traits : isDynamicArray;
244 
245     static if (isDynamicArray!T)
246     {
247         pragma(msg, "reference comparison of dynamic array: this is probably not what you want.");
248     }
249 
250     should.allowOnlyWordsBefore!(["approximately", "not"], "equal");
251 
252     return should.addData!"rhs"(value).approximateCheck(file, line);
253 }
254 
255 void equal(Should, T)(Should should, T value, string file = __FILE__, size_t line = __LINE__)
256 if (isInstanceOf!(ShouldType, Should) && should.hasWord!"approximately")
257 {
258     should.allowOnlyWordsBefore!(["approximately", "not"], "be");
259 
260     return should.addData!"rhs"(value).approximateCheck(file, line);
261 }
262 
263 void approximateCheck(Should)(Should should, string file, size_t line)
264 if (isInstanceOf!(ShouldType, Should))
265 {
266     import std.format : format;
267     import std.math : abs;
268 
269     with (should)
270     {
271         auto lhs = data.lhs();
272         auto rhs = data.rhs;
273 
274         static if (hasWord!"not")
275         {
276             check(
277                 abs(lhs - rhs) >= data.permissibleError,
278                 format(": expected value outside %s ± %s, but got %s", rhs, data.permissibleError, lhs),
279                 file, line
280             );
281         }
282         else
283         {
284             check(
285                 abs(lhs - rhs) < data.permissibleError,
286                 format(": expected %s ± %s, but got %s", rhs, data.permissibleError, lhs),
287                 file, line
288             );
289         }
290     }
291 }
292 
293 private string quote(T)(T t)
294 {
295     import std.format : format;
296 
297     static if (is(T: string))
298     {
299         return format!`'%s'`(t);
300     }
301     else
302     {
303         return format!`%s`(t);
304     }
305 }