1 module fluentasserts.core.base;
2 
3 public import fluentasserts.core.array;
4 public import fluentasserts.core..string;
5 public import fluentasserts.core.objects;
6 public import fluentasserts.core.basetype;
7 public import fluentasserts.core.callable;
8 public import fluentasserts.core.results;
9 public import fluentasserts.core.lifecycle;
10 public import fluentasserts.core.expect;
11 public import fluentasserts.core.evaluation;
12 
13 import std.traits;
14 import std.stdio;
15 import std.algorithm;
16 import std.array;
17 import std.range;
18 import std.conv;
19 import std..string;
20 import std.file;
21 import std.datetime;
22 import std.range.primitives;
23 import std.typecons;
24 
25 @safe:
26 
27 struct Result {
28   bool willThrow;
29   IResult[] results;
30 
31   MessageResult message;
32 
33   string file;
34   size_t line;
35 
36   private string reason;
37 
38   auto because(string reason) {
39     this.reason = "Because " ~ reason ~ ", ";
40     return this;
41   }
42 
43   void perform() {
44     if(!willThrow) {
45       return;
46     }
47 
48     version(DisableMessageResult) {
49       IResult[] localResults = this.results;
50     } else {
51       IResult[] localResults = message ~ this.results;
52     }
53 
54     version(DisableSourceResult) {} else {
55       auto sourceResult = new SourceResult(file, line);
56       message.prependValue(sourceResult.getValue);
57       message.prependText(reason);
58 
59       localResults ~= sourceResult;
60     }
61 
62     throw new TestException(localResults, file, line);
63   }
64 
65   ~this() {
66     this.perform;
67   }
68 
69   static Result success() {
70     return Result(false);
71   }
72 }
73 
74 struct Message {
75   bool isValue;
76   string text;
77 }
78 
79 mixin template DisabledShouldThrowableCommons() {
80   auto throwSomething(string file = __FILE__, size_t line = __LINE__) {
81     static assert("`throwSomething` does not work for arrays and ranges");
82   }
83 
84   auto throwAnyException(const string file = __FILE__, const size_t line = __LINE__) {
85     static assert("`throwAnyException` does not work for arrays and ranges");
86   }
87 
88   auto throwException(T)(const string file = __FILE__, const size_t line = __LINE__) {
89     static assert("`throwException` does not work for arrays and ranges");
90   }
91 }
92 
93 mixin template ShouldThrowableCommons() {
94   auto throwSomething(string file = __FILE__, size_t line = __LINE__) {
95     addMessage(" throw ");
96     addValue("something");
97     beginCheck;
98 
99     return throwException!Throwable(file, line);
100   }
101 
102   auto throwAnyException(const string file = __FILE__, const size_t line = __LINE__) {
103     addMessage(" throw ");
104     addValue("any exception");
105     beginCheck;
106 
107     return throwException!Exception(file, line);
108   }
109 
110   auto throwException(T)(const string file = __FILE__, const size_t line = __LINE__) {
111     addMessage(" throw a `");
112     addValue(T.stringof);
113     addMessage("`");
114 
115     return ThrowableProxy!T(valueEvaluation.throwable, expectedValue, messages, file, line);
116   }
117 
118   private {
119     ThrowableProxy!T throwExceptionImplementation(T)(Throwable t, string file = __FILE__, size_t line = __LINE__) {
120       addMessage(" throw a `");
121       addValue(T.stringof);
122       addMessage("`");
123 
124       bool rightType = true;
125       if(t !is null) {
126         T castedThrowable = cast(T) t;
127         rightType = castedThrowable !is null;
128       }
129 
130       return ThrowableProxy!T(t, expectedValue, rightType, messages, file, line);
131     }
132   }
133 }
134 
135 mixin template ShouldCommons()
136 {
137   import std..string;
138   import fluentasserts.core.results;
139 
140   private ValueEvaluation valueEvaluation;
141   private bool isNegation;
142 
143   private void validateException() {
144     if(valueEvaluation.throwable !is null) {
145       throw valueEvaluation.throwable;
146     }
147   }
148 
149   auto be() {
150     addMessage(" be");
151     return this;
152   }
153 
154   auto should() {
155     return this;
156   }
157 
158   auto not() {
159     addMessage(" not");
160     expectedValue = !expectedValue;
161     isNegation = !isNegation;
162 
163     return this;
164   }
165 
166   auto forceMessage(string message) {
167     messages = [];
168 
169     addMessage(message);
170 
171     return this;
172   }
173 
174   auto forceMessage(Message[] messages) {
175     this.messages = messages;
176 
177     return this;
178   }
179 
180   private {
181     Message[] messages;
182     ulong mesageCheckIndex;
183 
184     bool expectedValue = true;
185 
186     void addMessage(string msg) {
187       if(mesageCheckIndex != 0) {
188         return;
189       }
190 
191       messages ~= Message(false, msg);
192     }
193 
194     void addValue(string msg) {
195       if(mesageCheckIndex != 0) {
196         return;
197       }
198 
199       messages ~= Message(true, msg);
200     }
201 
202     void addValue(EquableValue msg) {
203       if(mesageCheckIndex != 0) {
204         return;
205       }
206 
207       messages ~= Message(true, msg.getSerialized);
208     }
209 
210     void beginCheck() {
211       if(mesageCheckIndex != 0) {
212         return;
213       }
214 
215       mesageCheckIndex = messages.length;
216     }
217 
218     Result simpleResult(bool value, Message[] msg, string file, size_t line) {
219       return result(value, msg, [ ], file, line);
220     }
221 
222     Result result(bool value, Message[] msg, IResult res, string file, size_t line) {
223       return result(value, msg, [ res ], file, line);
224     }
225 
226     Result result(bool value, IResult res, string file, size_t line) {
227       return result(value, [], [ res ], file, line);
228     }
229 
230     Result result(bool value, Message[] msg, IResult[] res, const string file, const size_t line) {
231       if(res.length == 0 && msg.length == 0) {
232         return Result(false);
233       }
234 
235       auto finalMessage = new MessageResult(" should");
236 
237       messages ~= Message(false, ".");
238 
239       if(msg.length > 0) {
240         messages ~= Message(false, " ") ~ msg;
241       }
242 
243       foreach(message; messages) {
244         if(message.isValue) {
245           finalMessage.addValue(message.text);
246         } else {
247           finalMessage.addText(message.text);
248         }
249       }
250 
251       return Result(expectedValue != value, res, finalMessage, file, line);
252     }
253   }
254 }
255 
256 version(Have_unit_threaded) {
257   import unit_threaded.should;
258   alias ReferenceException = UnitTestException;
259 } else {
260   alias ReferenceException = Exception;
261 }
262 
263 class TestException : ReferenceException {
264   private {
265     IResult[] results;
266   }
267 
268   this(IResult[] results, string fileName, size_t line, Throwable next = null) {
269     auto msg = results.map!"a.toString".filter!"a != ``".join("\n") ~ '\n';
270     this.results = results;
271 
272     super(msg, fileName, line, next);
273   }
274 
275   void print(ResultPrinter printer) {
276     foreach(result; results) {
277       result.print(printer);
278       printer.primary("\n");
279     }
280   }
281 }
282 
283 /// Test Exception should separate the results by a new line
284 unittest {
285   import std.stdio;
286   IResult[] results = [
287     cast(IResult) new MessageResult("message"),
288     cast(IResult) new SourceResult("test/missing.txt", 10),
289     cast(IResult) new DiffResult("a", "b"),
290     cast(IResult) new ExpectedActualResult("a", "b"),
291     cast(IResult) new ExtraMissingResult("a", "b") ];
292 
293   auto exception = new TestException(results, "unknown", 0);
294 
295   exception.msg.should.equal(`message
296 
297 --------------------
298 test/missing.txt:10
299 --------------------
300 
301 Diff:
302 [-a][+b]
303 
304  Expected:a
305    Actual:b
306 
307     Extra:a
308   Missing:b
309 `);
310 }
311 
312 @("TestException should concatenate all the Result strings")
313 unittest {
314   class TestResult : IResult {
315     override string toString() {
316       return "message";
317     }
318 
319     void print(ResultPrinter) {}
320   }
321 
322   auto exception = new TestException([ new TestResult, new TestResult, new TestResult], "", 0);
323 
324   exception.msg.should.equal("message\nmessage\nmessage\n");
325 }
326 
327 @("TestException should call all the result print methods on print")
328 unittest {
329   int count;
330 
331   class TestResult : IResult {
332     override string toString() {
333       return "";
334     }
335 
336     void print(ResultPrinter) {
337       count++;
338     }
339   }
340 
341   auto exception = new TestException([ new TestResult, new TestResult, new TestResult], "", 0);
342   exception.print(new DefaultResultPrinter);
343 
344   count.should.equal(3);
345 }
346 
347 struct ThrowableProxy(T : Throwable) {
348   import fluentasserts.core.results;
349 
350   private const {
351     bool expectedValue;
352     const string _file;
353     size_t _line;
354   }
355 
356   private {
357     Message[] messages;
358     string reason;
359     bool check;
360     Throwable thrown;
361     T thrownTyped;
362   }
363 
364   this(Throwable thrown, bool expectedValue, Message[] messages, const string file, size_t line) {
365     this.expectedValue = expectedValue;
366     this._file = file;
367     this._line = line;
368     this.thrown = thrown;
369     this.thrownTyped = cast(T) thrown;
370     this.messages = messages;
371     this.check = true;
372   }
373 
374   ~this() {
375     checkException;
376   }
377 
378   auto msg() {
379     checkException;
380     check = false;
381 
382     return thrown.msg.dup.to!string.strip;
383   }
384 
385   auto original() {
386     checkException;
387     check = false;
388 
389     return thrownTyped;
390   }
391 
392   auto file() {
393     checkException;
394     check = false;
395 
396     return thrown.file;
397   }
398 
399   auto info() {
400     checkException;
401     check = false;
402 
403     return thrown.info;
404   }
405 
406   auto line() {
407     checkException;
408     check = false;
409 
410     return thrown.line;
411   }
412 
413   auto next() {
414     checkException;
415     check = false;
416 
417     return thrown.next;
418   }
419 
420   auto withMessage() {
421     auto s = ShouldString(msg);
422     check = false;
423 
424     return s.forceMessage(messages ~ Message(false, " with message"));
425   }
426 
427   auto withMessage(string expectedMessage) {
428     auto s = ShouldString(msg);
429     check = false;
430 
431     return s.forceMessage(messages ~ Message(false, " with message")).equal(expectedMessage);
432   }
433 
434   private void checkException() {
435     if(!check) {
436       return;
437     }
438 
439     bool hasException = thrown !is null;
440     bool hasTypedException = thrownTyped !is null;
441 
442     if(hasException == expectedValue && hasTypedException == expectedValue) {
443       return;
444     }
445 
446     auto sourceResult = new SourceResult(_file, _line);
447     auto message = new MessageResult("");
448 
449     if(reason != "") {
450       message.addText("Because " ~ reason ~ ", ");
451     }
452 
453     message.addText(sourceResult.getValue ~ " should");
454 
455     foreach(msg; messages) {
456       if(msg.isValue) {
457         message.addValue(msg.text);
458       } else {
459         message.addText(msg.text);
460       }
461     }
462 
463     message.addText(".");
464 
465     if(thrown is null) {
466       message.addText(" Nothing was thrown.");
467     } else {
468       message.addText(" An exception of type `");
469       message.addValue(thrown.classinfo.name);
470       message.addText("` saying `");
471       message.addValue(thrown.msg);
472       message.addText("` was thrown.");
473     }
474 
475     throw new TestException([ cast(IResult) message ], _file, _line);
476   }
477 
478   auto because(string reason) {
479     this.reason = reason;
480 
481     return this;
482   }
483 }
484 
485 auto should(T)(lazy T testData, const string file = __FILE__, const size_t line = __LINE__) @trusted {
486   static if(is(T == void)) {
487     auto callable = ({ testData; });
488     return expect(callable, file, line);
489   } else {
490     return expect(testData, file, line);
491   }
492 }
493 
494 @("because")
495 unittest {
496   auto msg = ({
497     true.should.equal(false).because("of test reasons");
498   }).should.throwException!TestException.msg;
499 
500   msg.split("\n")[0].should.equal("Because of test reasons, true should equal false.");
501 }
502 
503 struct Assert {
504   static void opDispatch(string s, T, U)(T actual, U expected, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
505   {
506     auto sh = expect(actual);
507 
508     static if(s[0..3] == "not") {
509       sh.not;
510       enum assertName = s[3..4].toLower ~ s[4..$];
511     } else {
512       enum assertName = s;
513     }
514 
515     static if(assertName == "greaterThan" ||
516               assertName == "lessThan" ||
517               assertName == "above" ||
518               assertName == "below" ||
519               assertName == "between" ||
520               assertName == "within" ||
521               assertName == "approximately") {
522       sh.be;
523     }
524 
525     mixin("auto result = sh." ~ assertName ~ "(expected);");
526 
527     if(reason != "") {
528       result.because(reason);
529     }
530   }
531 
532   static void between(T, U)(T actual, U begin, U end, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
533   {
534     auto s = expect(actual, file, line).to.be.between(begin, end);
535 
536     if(reason != "") {
537       s.because(reason);
538     }
539   }
540 
541   static void notBetween(T, U)(T actual, U begin, U end, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
542   {
543     auto s = expect(actual, file, line).not.to.be.between(begin, end);
544 
545     if(reason != "") {
546       s.because(reason);
547     }
548   }
549 
550   static void within(T, U)(T actual, U begin, U end, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
551   {
552     auto s = expect(actual, file, line).to.be.between(begin, end);
553 
554     if(reason != "") {
555       s.because(reason);
556     }
557   }
558 
559   static void notWithin(T, U)(T actual, U begin, U end, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
560   {
561     auto s = expect(actual, file, line).not.to.be.between(begin, end);
562 
563     if(reason != "") {
564       s.because(reason);
565     }
566   }
567 
568   static void approximately(T, U, V)(T actual, U expected, V delta, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
569   {
570     auto s = expect(actual, file, line).to.be.approximately(expected, delta);
571 
572     if(reason != "") {
573       s.because(reason);
574     }
575   }
576 
577   static void notApproximately(T, U, V)(T actual, U expected, V delta, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
578   {
579     auto s = expect(actual, file, line).not.to.be.approximately(expected, delta);
580 
581     if(reason != "") {
582       s.because(reason);
583     }
584   }
585 
586   static void beNull(T)(T actual, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
587   {
588     auto s = expect(actual, file, line).to.beNull;
589 
590     if(reason != "") {
591       s.because(reason);
592     }
593   }
594 
595   static void notNull(T)(T actual, string reason = "", const string file = __FILE__, const size_t line = __LINE__)
596   {
597     auto s = expect(actual, file, line).not.to.beNull;
598 
599     if(reason != "") {
600       s.because(reason);
601     }
602   }
603 }
604 
605 /// Assert should work for base types
606 unittest {
607   Assert.equal(1, 1, "they are the same value");
608   Assert.notEqual(1, 2, "they are not the same value");
609 
610   Assert.greaterThan(1, 0);
611   Assert.notGreaterThan(0, 1);
612 
613   Assert.lessThan(0, 1);
614   Assert.notLessThan(1, 0);
615 
616   Assert.above(1, 0);
617   Assert.notAbove(0, 1);
618 
619   Assert.below(0, 1);
620   Assert.notBelow(1, 0);
621 
622   Assert.between(1, 0, 2);
623   Assert.notBetween(3, 0, 2);
624 
625   Assert.within(1, 0, 2);
626   Assert.notWithin(3, 0, 2);
627 
628   Assert.approximately(1.5f, 1, 0.6f);
629   Assert.notApproximately(1.5f, 1, 0.2f);
630 }
631 
632 /// Assert should work for objects
633 unittest {
634   Object o = null;
635   Assert.beNull(o, "it's a null");
636   Assert.notNull(new Object, "it's not a null");
637 }
638 
639 /// Assert should work for strings
640 unittest {
641   Assert.equal("abcd", "abcd");
642   Assert.notEqual("abcd", "abwcd");
643 
644   Assert.contain("abcd", "bc");
645   Assert.notContain("abcd", 'e');
646 
647   Assert.startWith("abcd", "ab");
648   Assert.notStartWith("abcd", "bc");
649 
650   Assert.startWith("abcd", 'a');
651   Assert.notStartWith("abcd", 'b');
652 
653   Assert.endWith("abcd", "cd");
654   Assert.notEndWith("abcd", "bc");
655 
656   Assert.endWith("abcd", 'd');
657   Assert.notEndWith("abcd", 'c');
658 }
659 
660 /// Assert should work for ranges
661 unittest {
662   Assert.equal([1, 2, 3], [1, 2, 3]);
663   Assert.notEqual([1, 2, 3], [1, 1, 3]);
664 
665   Assert.contain([1, 2, 3], 3);
666   Assert.notContain([1, 2, 3], [5, 6]);
667 
668   Assert.containOnly([1, 2, 3], [3, 2, 1]);
669   Assert.notContainOnly([1, 2, 3], [3, 1]);
670 }
671 
672 void fluentHandler(string file, size_t line, string msg) nothrow {
673   import core.exception;
674 
675   auto message = new MessageResult("Assert failed. " ~ msg);
676   auto source = new SourceResult(file, line);
677 
678   throw new AssertError(message.toString ~ "\n\n" ~ source.toString, file, line);
679 }
680 
681 void setupFluentHandler() {
682   import core.exception;
683   core.exception.assertHandler = &fluentHandler;
684 }
685 
686 /// It should call the fluent handler
687 @trusted
688 unittest {
689   import core.exception;
690 
691   setupFluentHandler;
692   scope(exit) core.exception.assertHandler = null;
693 
694   bool thrown = false;
695 
696   try {
697     assert(false, "What?");
698   } catch(Throwable t) {
699     thrown = true;
700     t.msg.should.startWith("Assert failed. What?\n");
701   }
702 
703   thrown.should.equal(true);
704 }