1 module fluentasserts.core.expect;
2 
3 import fluentasserts.core.lifecycle;
4 import fluentasserts.core.evaluation;
5 import fluentasserts.core.results;
6 
7 import fluentasserts.core.serializers;
8 
9 import std.traits;
10 import std..string;
11 import std.uni;
12 import std.conv;
13 
14 ///
15 @safe struct Expect {
16 
17   private {
18     Evaluation evaluation;
19     int refCount;
20   }
21 
22   this(ValueEvaluation value, const string fileName, const size_t line, string prependText = null) @trusted {
23     this.evaluation = new Evaluation();
24 
25     evaluation.id = Lifecycle.instance.beginEvaluation(value);
26     evaluation.currentValue = value;
27     evaluation.message = new MessageResult();
28     evaluation.source = new SourceResult(fileName, line);
29 
30     try {
31       auto sourceValue = evaluation.source.getValue;
32 
33       if(sourceValue == "") {
34         evaluation.message.startWith(evaluation.currentValue.niceValue);
35       } else {
36         evaluation.message.startWith(sourceValue);
37       }
38     } catch(Exception) {
39       evaluation.message.startWith(evaluation.currentValue.strValue);
40     }
41 
42     evaluation.message.addText(" should");
43 
44     if(prependText) {
45       evaluation.message.addText(prependText);
46     }
47   }
48 
49   this(ref return scope Expect another) {
50     this.evaluation = another.evaluation;
51     this.refCount = another.refCount + 1;
52   }
53 
54   ~this() {
55     refCount--;
56 
57     if(refCount < 0) {
58       evaluation.message.addText(" ");
59       evaluation.message.addText(evaluation.operationName.toNiceOperation);
60 
61       if(evaluation.expectedValue.niceValue) {
62         evaluation.message.addText(" ");
63         evaluation.message.addValue(evaluation.expectedValue.niceValue);
64       } else if(evaluation.expectedValue.strValue) {
65         evaluation.message.addText(" ");
66         evaluation.message.addValue(evaluation.expectedValue.strValue);
67       }
68 
69       Lifecycle.instance.endEvaluation(evaluation);
70     }
71   }
72 
73   string msg(const size_t line = __LINE__, const string file = __FILE__) @trusted {
74     if(this.thrown is null) {
75       throw new Exception("There were no thrown exceptions", file, line);
76     }
77 
78     return this.thrown.message.to!string;
79   }
80 
81   Expect withMessage(const size_t line = __LINE__, const string file = __FILE__) {
82     addOperationName("withMessage");
83     return this;
84   }
85 
86   Expect withMessage(string message, const size_t line = __LINE__, const string file = __FILE__) {
87     addOperationName("withMessage");
88     return this.equal(message);
89   }
90 
91   Throwable thrown() {
92     Lifecycle.instance.endEvaluation(evaluation);
93     return evaluation.throwable;
94   }
95 
96   ///
97   Expect to() {
98     return this;
99   }
100 
101   ///
102   Expect be () {
103     evaluation.message.addText(" be");
104     return this;
105   }
106 
107   ///
108   Expect not() {
109     evaluation.isNegated = !evaluation.isNegated;
110     evaluation.message.addText(" not");
111 
112     return this;
113   }
114 
115   ///
116   auto throwAnyException() {
117     return opDispatch!"throwAnyException";
118   }
119 
120   ///
121   Expect throwException(Type)() {
122     this.evaluation.expectedValue.meta["exceptionType"] = fullyQualifiedName!Type;
123     this.evaluation.expectedValue.meta["throwableType"] = fullyQualifiedName!Type;
124 
125     return opDispatch!"throwException"(fullyQualifiedName!Type);
126   }
127 
128   auto because(string reason) {
129     evaluation.message.prependText("Because " ~ reason ~ ", ");
130     return this;
131   }
132 
133   ///
134   auto equal(T)(T value) {
135     return opDispatch!"equal"(value);
136   }
137 
138   ///
139   auto contain(T)(T value) {
140     return opDispatch!"contain"(value);
141   }
142 
143   ///
144   auto greaterThan(T)(T value) {
145     return opDispatch!"greaterThan"(value);
146   }
147 
148   ///
149   auto greaterOrEqualTo(T)(T value) {
150     return opDispatch!"greaterOrEqualTo"(value);
151   }
152 
153   ///
154   auto above(T)(T value) {
155     return opDispatch!"above"(value);
156   }
157   ///
158   auto lessThan(T)(T value) {
159     return opDispatch!"lessThan"(value);
160   }
161 
162   ///
163   auto lessOrEqualTo(T)(T value) {
164     return opDispatch!"lessOrEqualTo"(value);
165   }
166 
167   ///
168   auto below(T)(T value) {
169     return opDispatch!"below"(value);
170   }
171 
172   ///
173   auto startWith(T)(T value) {
174     return opDispatch!"startWith"(value);
175   }
176 
177   ///
178   auto endWith(T)(T value) {
179     return opDispatch!"endWith"(value);
180   }
181 
182   auto containOnly(T)(T value) {
183     return opDispatch!"containOnly"(value);
184   }
185 
186   auto beNull() {
187     return opDispatch!"beNull";
188   }
189 
190   auto instanceOf(Type)() {
191     return opDispatch!"instanceOf"(fullyQualifiedName!Type);
192   }
193 
194   auto approximately(T, U)(T value, U range) {
195     return opDispatch!"approximately"(value, range);
196   }
197 
198   auto between(T, U)(T value, U range) {
199     return opDispatch!"between"(value, range);
200   }
201 
202   auto within(T, U)(T value, U range) {
203     return opDispatch!"within"(value, range);
204   }
205 
206   void inhibit() {
207     this.refCount = int.max;
208   }
209 
210   auto haveExecutionTime() {
211     this.inhibit;
212 
213     auto result = expect(evaluation.currentValue.duration, evaluation.source.file, evaluation.source.line, " have execution time");
214 
215     return result;
216   }
217 
218   void addOperationName(string value) {
219 
220     if(this.evaluation.operationName) {
221       this.evaluation.operationName ~= ".";
222     }
223 
224     this.evaluation.operationName ~= value;
225   }
226 
227   ///
228   Expect opDispatch(string methodName)() {
229     addOperationName(methodName);
230 
231     return this;
232   }
233 
234   ///
235   Expect opDispatch(string methodName, Params...)(Params params) if(Params.length > 0) {
236     addOperationName(methodName);
237 
238     static if(Params.length > 0) {
239       auto expectedValue = params[0].evaluate.evaluation;
240 
241       foreach(key, value; evaluation.expectedValue.meta) {
242         expectedValue.meta[key] = value;
243       }
244 
245       evaluation.expectedValue = expectedValue;
246     }
247 
248     static if(Params.length >= 1) {
249       static foreach (i, Param; Params) {
250         () @trusted { evaluation.expectedValue.meta[i.to!string] = SerializerRegistry.instance.serialize(params[i]); } ();
251       }
252     }
253 
254     return this;
255   }
256 }
257 
258 ///
259 Expect expect(void delegate() callable, const string file = __FILE__, const size_t line = __LINE__, string prependText = null) @trusted {
260   ValueEvaluation value;
261   value.typeNames = [ "callable" ];
262 
263   try {
264     if(callable !is null) {
265       callable();
266     } else {
267       value.typeNames = ["null"];
268     }
269   } catch(Exception e) {
270     value.throwable = e;
271     value.meta["Exception"] = "yes";
272   } catch(Throwable t) {
273     value.throwable = t;
274     value.meta["Throwable"] = "yes";
275   }
276 
277   return Expect(value, file, line, prependText);
278 }
279 
280 ///
281 Expect expect(T)(lazy T testedValue, const string file = __FILE__, const size_t line = __LINE__, string prependText = null) @trusted {
282   return Expect(testedValue.evaluate.evaluation, file, line, prependText);
283 }
284 
285 ///
286 string toNiceOperation(string value) @safe nothrow {
287   string newValue;
288 
289   foreach(index, ch; value) {
290     if(index == 0) {
291       newValue ~= ch.toLower;
292       continue;
293     }
294 
295     if(ch == '.') {
296       newValue ~= ' ';
297       continue;
298     }
299 
300     if(ch.isUpper && value[index - 1].isLower) {
301       newValue ~= ' ';
302       newValue ~= ch.toLower;
303       continue;
304     }
305 
306     newValue ~= ch;
307   }
308 
309   return newValue;
310 }
311 
312 /// toNiceOperation converts to a nice and readable string
313 unittest {
314   expect("".toNiceOperation).to.equal("");
315   expect("a.b".toNiceOperation).to.equal("a b");
316   expect("aB".toNiceOperation).to.equal("a b");
317 }