1 module fluentasserts.core.results;
2 
3 import std.stdio;
4 import std.file;
5 import std.algorithm;
6 import std.conv;
7 import std.range;
8 import std..string;
9 import std.exception;
10 import std.typecons;
11 
12 import dparse.lexer;
13 import dparse.parser;
14 
15 @safe:
16 
17 /// Glyphs used to display special chars in the results
18 struct ResultGlyphs {
19   static {
20     /// Glyph for the tab char
21     string tab;
22 
23     /// Glyph for the \r char
24     string carriageReturn;
25 
26     /// Glyph for the \n char
27     string newline;
28 
29     /// Glyph for the space char
30     string space;
31 
32     /// Glyph for the \0 char
33     string nullChar;
34 
35     /// Glyph that indicates the error line
36     string sourceIndicator;
37 
38     /// Glyph that sepparates the line number
39     string sourceLineSeparator;
40 
41     /// Glyph for the diff begin indicator
42     string diffBegin;
43 
44     /// Glyph for the diff end indicator
45     string diffEnd;
46 
47     /// Glyph that marks an inserted text in diff
48     string diffInsert;
49 
50     /// Glyph that marks deleted text in diff
51     string diffDelete;
52   }
53 
54   /// Set the default values. The values are
55   static resetDefaults() {
56     version(windows) {
57       ResultGlyphs.tab = `\t`;
58       ResultGlyphs.carriageReturn = `\r`;
59       ResultGlyphs.newline = `\n`;
60       ResultGlyphs.space = ` `;
61       ResultGlyphs.nullChar = `␀`;
62     } else {
63       ResultGlyphs.tab = `¤`;
64       ResultGlyphs.carriageReturn = `←`;
65       ResultGlyphs.newline = `↲`;
66       ResultGlyphs.space = `᛫`;
67       ResultGlyphs.nullChar = `\0`;
68     }
69 
70     ResultGlyphs.sourceIndicator = ">";
71     ResultGlyphs.sourceLineSeparator = ":";
72 
73     ResultGlyphs.diffBegin = "[";
74     ResultGlyphs.diffEnd = "]";
75     ResultGlyphs.diffInsert = "+";
76     ResultGlyphs.diffDelete = "-";
77   }
78 }
79 
80 ///
81 interface ResultPrinter {
82   void primary(string);
83   void info(string);
84   void danger(string);
85   void success(string);
86 
87   void dangerReverse(string);
88   void successReverse(string);
89 }
90 
91 version(unittest) {
92   class MockPrinter : ResultPrinter {
93     string buffer;
94 
95     void primary(string val) {
96       buffer ~= "[primary:" ~ val ~ "]";
97     }
98 
99     void info(string val) {
100       buffer ~= "[info:" ~ val ~ "]";
101     }
102 
103     void danger(string val) {
104       buffer ~= "[danger:" ~ val ~ "]";
105     }
106 
107     void success(string val) {
108       buffer ~= "[success:" ~ val ~ "]";
109     }
110 
111     void dangerReverse(string val) {
112       buffer ~= "[dangerReverse:" ~ val ~ "]";
113     }
114 
115     void successReverse(string val) {
116       buffer ~= "[successReverse:" ~ val ~ "]";
117     }
118   }
119 }
120 
121 struct WhiteIntervals {
122   size_t left;
123   size_t right;
124 }
125 
126 WhiteIntervals getWhiteIntervals(string text) {
127   auto stripText = text.strip;
128 
129   if(stripText == "") {
130     return WhiteIntervals(0, 0);
131   }
132 
133   return WhiteIntervals(text.indexOf(stripText[0]), text.lastIndexOf(stripText[stripText.length - 1]));
134 }
135 
136 /// This is the most simple implementation of a ResultPrinter.
137 /// All the plain data is printed to stdout
138 class DefaultResultPrinter : ResultPrinter {
139   void primary(string text) {
140     write(text);
141   }
142 
143   void info(string text) {
144     write(text);
145   }
146 
147   void danger(string text) {
148     write(text);
149   }
150 
151   void success(string text) {
152     write(text);
153   }
154 
155   void dangerReverse(string text) {
156     write(text);
157   }
158 
159   void successReverse(string text) {
160     write(text);
161   }
162 }
163 
164 interface IResult
165 {
166   string toString();
167   void print(ResultPrinter);
168 }
169 
170 /// A result that prints a simple message to the user
171 class MessageResult : IResult
172 {
173   private
174   {
175     struct Message {
176       bool isValue;
177       string text;
178     }
179 
180     Message[] messages;
181   }
182 
183   this(string message) nothrow
184   {
185     add(false, message);
186   }
187 
188   this() nothrow { }
189 
190   override string toString() {
191     return messages.map!"a.text".join.to!string;
192   }
193 
194   void startWith(string message) @safe nothrow {
195     Message[] newMessages;
196 
197     newMessages ~= Message(false, message);
198     newMessages ~= this.messages;
199 
200     this.messages = newMessages;
201   }
202 
203   void add(bool isValue, string message) nothrow {
204     this.messages ~= Message(isValue, message
205       .replace("\r", ResultGlyphs.carriageReturn)
206       .replace("\n", ResultGlyphs.newline)
207       .replace("\0", ResultGlyphs.nullChar)
208       .replace("\t", ResultGlyphs.tab));
209   }
210 
211   void addValue(string text) @safe nothrow {
212     add(true, text);
213   }
214 
215   void addText(string text) @safe nothrow {
216     if(text == "throwAnyException") {
217       text = "throw any exception";
218     }
219 
220     this.messages ~= Message(false, text);
221   }
222 
223   void prependText(string text) @safe nothrow  {
224     this.messages = Message(false, text) ~ this.messages;
225   }
226 
227   void prependValue(string text) @safe nothrow {
228     this.messages = Message(true, text) ~ this.messages;
229   }
230 
231   void print(ResultPrinter printer)
232   {
233     foreach(message; messages) {
234       if(message.isValue) {
235         printer.info(message.text);
236       } else {
237         printer.primary(message.text);
238       }
239     }
240   }
241 }
242 
243 version (unittest) {
244   import fluentasserts.core.base;
245 }
246 
247 @("Message result should return the message")
248 unittest
249 {
250   auto result = new MessageResult("Message");
251   result.toString.should.equal("Message");
252 }
253 
254 @("Message result should replace the special chars")
255 unittest
256 {
257   auto result = new MessageResult("\t \r\n");
258   result.toString.should.equal(`¤ ←↲`);
259 }
260 
261 @("Message result should replace the special chars with the custom glyphs")
262 unittest
263 {
264   scope(exit) {
265     ResultGlyphs.resetDefaults;
266   }
267 
268   ResultGlyphs.tab = `\t`;
269   ResultGlyphs.carriageReturn = `\r`;
270   ResultGlyphs.newline = `\n`;
271 
272   auto result = new MessageResult("\t \r\n");
273   result.toString.should.equal(`\t \r\n`);
274 }
275 
276 @("Message result should return values as string")
277 unittest
278 {
279   auto result = new MessageResult("text");
280   result.addValue("value");
281   result.addText("text");
282 
283   result.toString.should.equal(`textvaluetext`);
284 }
285 
286 @("Message result should print a string as primary")
287 unittest
288 {
289   auto result = new MessageResult("\t \r\n");
290   auto printer = new MockPrinter;
291   result.print(printer);
292 
293   printer.buffer.should.equal(`[primary:¤ ←↲]`);
294 }
295 
296 @("Message result should print values as info")
297 unittest
298 {
299   auto result = new MessageResult("text");
300   result.addValue("value");
301   result.addText("text");
302 
303   auto printer = new MockPrinter;
304   result.print(printer);
305 
306   printer.buffer.should.equal(`[primary:text][info:value][primary:text]`);
307 }
308 
309 class DiffResult : IResult {
310   import ddmp.diff;
311 
312   protected
313   {
314     string expected;
315     string actual;
316   }
317 
318   this(string expected, string actual)
319   {
320     this.expected = expected.replace("\0", ResultGlyphs.nullChar);
321     this.actual = actual.replace("\0", ResultGlyphs.nullChar);
322   }
323 
324   private string getResult(const Diff d) {
325     final switch(d.operation) {
326         case Operation.DELETE:
327           return ResultGlyphs.diffBegin ~ ResultGlyphs.diffDelete ~ d.text ~ ResultGlyphs.diffEnd;
328         case Operation.INSERT:
329           return ResultGlyphs.diffBegin ~ ResultGlyphs.diffInsert ~ d.text ~ ResultGlyphs.diffEnd;
330         case Operation.EQUAL:
331           return d.text;
332     }
333   }
334 
335   override string toString() @trusted {
336     return "Diff:\n" ~ diff_main(expected, actual).map!(a => getResult(a)).join;
337   }
338 
339   void print(ResultPrinter printer) @trusted {
340     auto result = diff_main(expected, actual);
341     printer.info("Diff:");
342 
343     foreach(diff; result) {
344       if(diff.operation == Operation.EQUAL) {
345         printer.primary(diff.text);
346       }
347 
348       if(diff.operation == Operation.INSERT) {
349         printer.successReverse(diff.text);
350       }
351 
352       if(diff.operation == Operation.DELETE) {
353         printer.dangerReverse(diff.text);
354       }
355     }
356 
357     printer.primary("\n");
358   }
359 }
360 
361 /// DiffResult should find the differences
362 unittest {
363   auto diff = new DiffResult("abc", "asc");
364   diff.toString.should.equal("Diff:\na[-b][+s]c");
365 }
366 
367 /// DiffResult should use the custom glyphs
368 unittest {
369   scope(exit) {
370     ResultGlyphs.resetDefaults;
371   }
372 
373   ResultGlyphs.diffBegin = "{";
374   ResultGlyphs.diffEnd = "}";
375   ResultGlyphs.diffInsert = "!";
376   ResultGlyphs.diffDelete = "?";
377 
378   auto diff = new DiffResult("abc", "asc");
379   diff.toString.should.equal("Diff:\na{?b}{!s}c");
380 }
381 
382 class KeyResult(string key) : IResult {
383 
384   private immutable {
385     string value;
386     size_t indent;
387   }
388 
389   this(string value, size_t indent = 10) {
390     this.value = value.replace("\0", ResultGlyphs.nullChar);
391     this.indent = indent;
392   }
393 
394   bool hasValue() {
395     return value != "";
396   }
397 
398   override string toString()
399   {
400     if(value == "") {
401       return "";
402     }
403 
404     return rightJustify(key ~ ":", indent, ' ') ~ printableValue;
405   }
406 
407   void print(ResultPrinter printer)
408   {
409     if(value == "") {
410       return;
411     }
412 
413     printer.info(rightJustify(key ~ ":", indent, ' '));
414     auto lines = value.split("\n");
415 
416     auto spaces = rightJustify(":", indent, ' ');
417 
418     int index;
419     foreach(line; lines) {
420       if(index > 0) {
421         printer.info(ResultGlyphs.newline);
422         printer.primary("\n");
423         printer.info(spaces);
424       }
425 
426       printLine(line, printer);
427 
428       index++;
429     }
430 
431   }
432 
433   private
434   {
435     struct Message {
436       bool isSpecial;
437       string text;
438     }
439 
440     void printLine(string line, ResultPrinter printer) {
441       Message[] messages;
442 
443       auto whiteIntervals = line.getWhiteIntervals;
444 
445       foreach(size_t index, ch; line) {
446         bool showSpaces = index < whiteIntervals.left || index >= whiteIntervals.right;
447 
448         auto special = isSpecial(ch, showSpaces);
449 
450         if(messages.length == 0 || messages[messages.length - 1].isSpecial != special) {
451           messages ~= Message(special, "");
452         }
453 
454         messages[messages.length - 1].text ~= toVisible(ch, showSpaces);
455       }
456 
457       foreach(message; messages) {
458         if(message.isSpecial) {
459           printer.info(message.text);
460         } else {
461           printer.primary(message.text);
462         }
463       }
464     }
465 
466     bool isSpecial(T)(T ch, bool showSpaces) {
467       if(ch == ' ' && showSpaces) {
468         return true;
469       }
470 
471       if(ch == '\r' || ch == '\t') {
472         return true;
473       }
474 
475       return false;
476     }
477 
478     string toVisible(T)(T ch, bool showSpaces) {
479       if(ch == ' ' && showSpaces) {
480         return ResultGlyphs.space;
481       }
482 
483       if(ch == '\r') {
484         return ResultGlyphs.carriageReturn;
485       }
486 
487       if(ch == '\t') {
488         return ResultGlyphs.tab;
489       }
490 
491       return ch.to!string;
492     }
493 
494     pure string printableValue()
495     {
496       return value.split("\n").join("\\n\n" ~ rightJustify(":", indent, ' '));
497     }
498   }
499 }
500 
501 /// KeyResult should not dispaly spaces between words with special chars
502 unittest {
503   auto result = new KeyResult!"key"(" row1  row2 ");
504   auto printer = new MockPrinter();
505 
506   result.print(printer);
507   printer.buffer.should.equal(`[info:      key:][info:᛫][primary:row1  row2][info:᛫]`);
508 }
509 
510 /// KeyResult should dispaly spaces with special chars on space lines
511 unittest {
512   auto result = new KeyResult!"key"("   ");
513   auto printer = new MockPrinter();
514 
515   result.print(printer);
516   printer.buffer.should.equal(`[info:      key:][info:᛫᛫᛫]`);
517 }
518 
519 /// KeyResult should display no char for empty lines
520 unittest {
521   auto result = new KeyResult!"key"("");
522   auto printer = new MockPrinter();
523 
524   result.print(printer);
525   printer.buffer.should.equal(``);
526 }
527 
528 /// KeyResult should display special characters with different contexts
529 unittest {
530   auto result = new KeyResult!"key"("row1\n \trow2");
531   auto printer = new MockPrinter();
532 
533   result.print(printer);
534 
535   printer.buffer.should.equal(`[info:      key:][primary:row1][info:↲][primary:` ~ "\n" ~ `][info:         :][info:᛫¤][primary:row2]`);
536 }
537 
538 /// KeyResult should display custom glyphs with different contexts
539 unittest {
540   scope(exit) {
541     ResultGlyphs.resetDefaults;
542   }
543 
544   ResultGlyphs.newline = `\n`;
545   ResultGlyphs.tab = `\t`;
546   ResultGlyphs.space = ` `;
547 
548   auto result = new KeyResult!"key"("row1\n \trow2");
549   auto printer = new MockPrinter();
550 
551   result.print(printer);
552 
553   printer.buffer.should.equal(`[info:      key:][primary:row1][info:\n][primary:` ~ "\n" ~ `][info:         :][info: \t][primary:row2]`);
554 }
555 
556 ///
557 class ExpectedActualResult : IResult {
558   protected {
559     string title;
560     KeyResult!"Expected" expected;
561     KeyResult!"Actual" actual;
562   }
563 
564   this(string title, string expected, string actual) nothrow @safe {
565     this.title = title;
566     this(expected, actual);
567   }
568 
569   this(string expected, string actual) nothrow @safe {
570     this.expected = new KeyResult!"Expected"(expected);
571     this.actual = new KeyResult!"Actual"(actual);
572   }
573 
574   override string toString() {
575     auto line1 = expected.toString;
576     auto line2 = actual.toString;
577     string glue;
578     string prefix;
579 
580     if(line1 != "" && line2 != "") {
581       glue = "\n";
582     }
583 
584     if(line1 != "" || line2 != "") {
585       prefix = title == "" ? "\n" : ("\n" ~ title ~ "\n");
586     }
587 
588     return prefix ~ line1 ~ glue ~ line2;
589   }
590 
591   void print(ResultPrinter printer)
592   {
593     auto line1 = expected.toString;
594     auto line2 = actual.toString;
595 
596     if(actual.hasValue || expected.hasValue) {
597       printer.info(title == "" ? "\n" : ("\n" ~ title ~ "\n"));
598     }
599 
600     expected.print(printer);
601     if(actual.hasValue && expected.hasValue) {
602       printer.primary("\n");
603     }
604     actual.print(printer);
605   }
606 }
607 
608 @("ExpectedActual result should be empty when no data is provided")
609 unittest
610 {
611   auto result = new ExpectedActualResult("", "");
612   result.toString.should.equal("");
613 }
614 
615 @("ExpectedActual result should be empty when null data is provided")
616 unittest
617 {
618   auto result = new ExpectedActualResult(null, null);
619   result.toString.should.equal("");
620 }
621 
622 @("ExpectedActual result should show one line of the expected and actual data")
623 unittest
624 {
625   auto result = new ExpectedActualResult("data", "data");
626   result.toString.should.equal(`
627  Expected:data
628    Actual:data`);
629 }
630 
631 @("ExpectedActual result should show one line of the expected and actual data")
632 unittest
633 {
634   auto result = new ExpectedActualResult("data\ndata", "data\ndata");
635   result.toString.should.equal(`
636  Expected:data\n
637          :data
638    Actual:data\n
639          :data`);
640 }
641 
642 /// A result that displays differences between ranges
643 class ExtraMissingResult : IResult
644 {
645   protected
646   {
647     KeyResult!"Extra" extra;
648     KeyResult!"Missing" missing;
649   }
650 
651   this(string extra, string missing)
652   {
653     this.extra = new KeyResult!"Extra"(extra);
654     this.missing = new KeyResult!"Missing"(missing);
655   }
656 
657   override string toString()
658   {
659     auto line1 = extra.toString;
660     auto line2 = missing.toString;
661     string glue;
662     string prefix;
663 
664     if(line1 != "" || line2 != "") {
665       prefix = "\n";
666     }
667 
668     if(line1 != "" && line2 != "") {
669       glue = "\n";
670     }
671 
672     return prefix ~ line1 ~ glue ~ line2;
673   }
674 
675   void print(ResultPrinter printer)
676   {
677     if(extra.hasValue || missing.hasValue) {
678       printer.primary("\n");
679     }
680 
681     extra.print(printer);
682     if(extra.hasValue && missing.hasValue) {
683       printer.primary("\n");
684     }
685     missing.print(printer);
686   }
687 }
688 
689 
690 string toString(const(Token)[] tokens) {
691   string result;
692 
693   foreach(token; tokens.filter!(a => str(a.type) != "comment")) {
694     if(str(token.type) == "whitespace" && token.text == "") {
695       result ~= "\n";
696     } else {
697      result ~= token.text == "" ? str(token.type) : token.text;
698     }
699   }
700 
701   return result;
702 }
703 
704 auto getScope(const(Token)[] tokens, size_t line) nothrow {
705   bool foundScope;
706   bool foundAssert;
707   size_t beginToken;
708   size_t endToken = tokens.length;
709 
710   int paranthesisCount = 0;
711   int scopeLevel;
712   size_t[size_t] paranthesisLevels;
713 
714   foreach(i, token; tokens) {
715     string type = str(token.type);
716 
717     if(type == "{") {
718       paranthesisLevels[paranthesisCount] = i;
719       paranthesisCount++;
720     }
721 
722     if(type == "}") {
723       paranthesisCount--;
724     }
725 
726     if(line == token.line) {
727       foundScope = true;
728     }
729 
730     if(foundScope) {
731       if(token.text == "should" || token.text == "Assert" || type == "assert" || type == ";") {
732         foundAssert = true;
733         scopeLevel = paranthesisCount;
734       }
735 
736       if(type == "}" && paranthesisCount <= scopeLevel) {
737         beginToken = paranthesisLevels[paranthesisCount];
738         endToken = i + 1;
739 
740         break;
741       }
742     }
743   }
744 
745   return const Tuple!(size_t, "begin", size_t, "end")(beginToken, endToken);
746 }
747 
748 /// Get the spec function and scope that contains a lambda
749 unittest {
750   const(Token)[] tokens = [];
751   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
752 
753   auto result = getScope(tokens, 101);
754   auto identifierStart = getPreviousIdentifier(tokens, result.begin);
755 
756   tokens[identifierStart .. result.end].toString.strip.should.equal("it(\"should throw an exception if we request 2 android devices\", {
757       ({
758         auto result = [ device1.idup, device2.idup ].filterBy(RunOptions(\"\", \"android\", 2)).array;
759       }).should.throwException!DeviceException.withMessage.equal(\"You requested 2 `androdid` devices, but there is only 1 healthy.\");
760     }");
761 }
762 
763 /// Get the a method scope and signature
764 unittest {
765   const(Token)[] tokens = [];
766   splitMultilinetokens(fileToDTokens("test/class.d"), tokens);
767 
768   auto result = getScope(tokens, 10);
769   auto identifierStart = getPreviousIdentifier(tokens, result.begin);
770 
771   tokens[identifierStart .. result.end].toString.strip.should.equal("void bar() {
772         assert(false);
773     }");
774 }
775 
776 /// Get the a method scope without assert
777 unittest {
778   const(Token)[] tokens = [];
779   splitMultilinetokens(fileToDTokens("test/class.d"), tokens);
780 
781   auto result = getScope(tokens, 14);
782   auto identifierStart = getPreviousIdentifier(tokens, result.begin);
783 
784   tokens[identifierStart .. result.end].toString.strip.should.equal("void bar2() {
785         enforce(false);
786     }");
787 }
788 
789 size_t getFunctionEnd(const(Token)[] tokens, size_t start) {
790   int paranthesisCount;
791   size_t result = start;
792 
793   // iterate the parameters
794   foreach(i, token; tokens[start .. $]) {
795     string type = str(token.type);
796 
797     if(type == "(") {
798       paranthesisCount++;
799     }
800 
801     if(type == ")") {
802       paranthesisCount--;
803     }
804 
805     if(type == "{" && paranthesisCount == 0) {
806       result = start + i;
807       break;
808     }
809 
810     if(type == ";" && paranthesisCount == 0) {
811       return start + i;
812     }
813   }
814 
815   paranthesisCount = 0;
816   // iterate the scope
817   foreach(i, token; tokens[result .. $]) {
818     string type = str(token.type);
819 
820     if(type == "{") {
821       paranthesisCount++;
822     }
823 
824     if(type == "}") {
825       paranthesisCount--;
826 
827       if(paranthesisCount == 0) {
828         result = result + i;
829         break;
830       }
831     }
832   }
833 
834   return result;
835 }
836 
837 /// Get the end of a spec function with a lambda
838 unittest {
839   const(Token)[] tokens = [];
840   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
841 
842   auto result = getScope(tokens, 101);
843   auto identifierStart = getPreviousIdentifier(tokens, result.begin);
844   auto functionEnd = getFunctionEnd(tokens, identifierStart);
845 
846   tokens[identifierStart .. functionEnd].toString.strip.should.equal("it(\"should throw an exception if we request 2 android devices\", {
847       ({
848         auto result = [ device1.idup, device2.idup ].filterBy(RunOptions(\"\", \"android\", 2)).array;
849       }).should.throwException!DeviceException.withMessage.equal(\"You requested 2 `androdid` devices, but there is only 1 healthy.\");
850     })");
851 }
852 
853 
854 /// Get the end of an unittest function with a lambda
855 unittest {
856   const(Token)[] tokens = [];
857   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
858 
859   auto result = getScope(tokens, 81);
860   auto identifierStart = getPreviousIdentifier(tokens, result.begin);
861   auto functionEnd = getFunctionEnd(tokens, identifierStart) + 1;
862 
863   tokens[identifierStart .. functionEnd].toString.strip.should.equal("unittest {
864   ({
865     ({ }).should.beNull;
866   }).should.throwException!TestException.msg;
867 
868 }");
869 }
870 
871 /// Get tokens from a scope that contains a lambda
872 unittest {
873   const(Token)[] tokens = [];
874   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
875 
876   auto result = getScope(tokens, 81);
877 
878   tokens[result.begin .. result.end].toString.strip.should.equal(`{
879   ({
880     ({ }).should.beNull;
881   }).should.throwException!TestException.msg;
882 
883 }`);
884 }
885 
886 size_t getPreviousIdentifier(const(Token)[] tokens, size_t startIndex) {
887   enforce(startIndex > 0);
888   enforce(startIndex < tokens.length);
889 
890   int paranthesisCount;
891   bool foundIdentifier;
892 
893   foreach(i; 0..startIndex) {
894     auto index = startIndex - i - 1;
895     auto type = str(tokens[index].type);
896 
897     if(type == "(") {
898       paranthesisCount--;
899     }
900 
901     if(type == ")") {
902       paranthesisCount++;
903     }
904 
905     if(paranthesisCount < 0) {
906       return getPreviousIdentifier(tokens, index - 1);
907     }
908 
909     if(paranthesisCount != 0) {
910       continue;
911     }
912 
913     if(type == "unittest") {
914       return index;
915     }
916 
917     if(type == "{" || type == "}") {
918       return index + 1;
919     }
920 
921     if(type == ";") {
922       return index + 1;
923     }
924 
925     if(type == "=") {
926       return index + 1;
927     }
928 
929     if(type == ".") {
930       foundIdentifier = false;
931     }
932 
933     if(type == "identifier" && foundIdentifier) {
934       foundIdentifier = true;
935       continue;
936     }
937 
938     if(foundIdentifier) {
939       return index;
940     }
941   }
942 
943   return 0;
944 }
945 
946 /// Get the the previous unittest identifier from a list of tokens
947 unittest {
948   const(Token)[] tokens = [];
949   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
950 
951   auto scopeResult = getScope(tokens, 81);
952 
953   auto result = getPreviousIdentifier(tokens, scopeResult.begin);
954 
955   tokens[result .. scopeResult.begin].toString.strip.should.equal(`unittest`);
956 }
957 
958 /// Get the the previous paranthesis identifier from a list of tokens
959 unittest {
960   const(Token)[] tokens = [];
961   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
962 
963   auto scopeResult = getScope(tokens, 63);
964 
965   auto end = scopeResult.end - 11;
966 
967   auto result = getPreviousIdentifier(tokens, end);
968 
969   tokens[result .. end].toString.strip.should.equal(`(5, (11))`);
970 }
971 
972 /// Get the the previous function call identifier from a list of tokens
973 unittest {
974   const(Token)[] tokens = [];
975   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
976 
977   auto scopeResult = getScope(tokens, 75);
978 
979   auto end = scopeResult.end - 11;
980 
981   auto result = getPreviousIdentifier(tokens, end);
982 
983   tokens[result .. end].toString.strip.should.equal(`found(4)`);
984 }
985 
986 /// Get the the previous map!"" identifier from a list of tokens
987 unittest {
988   const(Token)[] tokens = [];
989   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
990 
991   auto scopeResult = getScope(tokens, 85);
992 
993   auto end = scopeResult.end - 12;
994   auto result = getPreviousIdentifier(tokens, end);
995 
996   tokens[result .. end].toString.strip.should.equal(`[1, 2, 3].map!"a"`);
997 }
998 
999 size_t getAssertIndex(const(Token)[] tokens, size_t startLine) {
1000   auto assertTokens = tokens
1001     .enumerate
1002     .filter!(a => a[1].text == "Assert")
1003     .filter!(a => a[1].line <= startLine)
1004     .array;
1005 
1006   if(assertTokens.length == 0) {
1007     return 0;
1008   }
1009 
1010   return assertTokens[assertTokens.length - 1].index;
1011 }
1012 
1013 /// Get the index of the Assert structure identifier from a list of tokens
1014 unittest {
1015   const(Token)[] tokens = [];
1016   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
1017 
1018   auto result = getAssertIndex(tokens, 55);
1019 
1020   tokens[result .. result + 4].toString.strip.should.equal(`Assert.equal(`);
1021 }
1022 
1023 auto getParameter(const(Token)[] tokens, size_t startToken) {
1024   size_t paranthesisCount;
1025 
1026   foreach(i; startToken..tokens.length) {
1027     string type = str(tokens[i].type);
1028 
1029     if(type == "(" || type == "[") {
1030       paranthesisCount++;
1031     }
1032 
1033     if(type == ")" || type == "]") {
1034       if(paranthesisCount == 0) {
1035         return i;
1036       }
1037 
1038       paranthesisCount--;
1039     }
1040 
1041     if(paranthesisCount > 0) {
1042       continue;
1043     }
1044 
1045     if(type == ",") {
1046       return i;
1047     }
1048   }
1049 
1050 
1051   return 0;
1052 }
1053 
1054 /// Get the first parameter from a list of tokens
1055 unittest {
1056   const(Token)[] tokens = [];
1057   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
1058 
1059   auto begin = getAssertIndex(tokens, 57) + 4;
1060   auto end = getParameter(tokens, begin);
1061   tokens[begin .. end].toString.strip.should.equal(`(5, (11))`);
1062 }
1063 
1064 /// Get the first list parameter from a list of tokens
1065 unittest {
1066   const(Token)[] tokens = [];
1067   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
1068 
1069   auto begin = getAssertIndex(tokens, 89) + 4;
1070   auto end = getParameter(tokens, begin);
1071   tokens[begin .. end].toString.strip.should.equal(`[ new Value(1), new Value(2) ]`);
1072 }
1073 
1074 /// Get the previous array identifier from a list of tokens
1075 unittest {
1076   const(Token)[] tokens = [];
1077   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
1078 
1079   auto scopeResult = getScope(tokens, 4);
1080   auto end = scopeResult.end - 13;
1081 
1082   auto result = getPreviousIdentifier(tokens, end);
1083 
1084   tokens[result .. end].toString.strip.should.equal(`[1, 2, 3]`);
1085 }
1086 
1087 /// Get the previous array of instances identifier from a list of tokens
1088 unittest {
1089   const(Token)[] tokens = [];
1090   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
1091 
1092   auto scopeResult = getScope(tokens, 90);
1093   auto end = scopeResult.end - 16;
1094 
1095   auto result = getPreviousIdentifier(tokens, end);
1096 
1097   tokens[result .. end].toString.strip.should.equal(`[ new Value(1), new Value(2) ]`);
1098 }
1099 
1100 size_t getShouldIndex(const(Token)[] tokens, size_t startLine) {
1101   auto shouldTokens = tokens
1102     .enumerate
1103     .filter!(a => a[1].text == "should")
1104     .filter!(a => a[1].line <= startLine)
1105     .array;
1106 
1107   if(shouldTokens.length == 0) {
1108     return 0;
1109   }
1110 
1111   return shouldTokens[shouldTokens.length - 1].index;
1112 }
1113 
1114 /// Get the index of the should call
1115 unittest {
1116   const(Token)[] tokens = [];
1117   splitMultilinetokens(fileToDTokens("test/values.d"), tokens);
1118 
1119   auto result = getShouldIndex(tokens, 4);
1120 
1121   auto token = tokens[result];
1122   token.line.should.equal(3);
1123   token.text.should.equal(`should`);
1124   str(token.type).text.should.equal(`identifier`);
1125 }
1126 
1127 /// An alternative to SourceResult that uses
1128 // DParse to get the source code
1129 class SourceResult : IResult
1130 {
1131   static private {
1132     const(Token)[][string] fileTokens;
1133   }
1134 
1135   immutable {
1136     string file;
1137     size_t line;
1138   }
1139 
1140   private const
1141   {
1142     Token[] tokens;
1143   }
1144 
1145   this(string fileName = __FILE__, size_t line = __LINE__, size_t range = 6) nothrow @trusted {
1146     this.file = fileName;
1147     this.line = line;
1148 
1149     if (!fileName.exists)
1150     {
1151       return;
1152     }
1153 
1154     try {
1155       updateFileTokens(fileName);
1156       auto result = getScope(fileTokens[fileName], line);
1157 
1158       auto begin = getPreviousIdentifier(fileTokens[fileName], result.begin);
1159       auto end = getFunctionEnd(fileTokens[fileName], begin) + 1;
1160 
1161       this.tokens = fileTokens[fileName][begin .. end];
1162     } catch (Throwable t) {
1163     }
1164   }
1165 
1166   static void updateFileTokens(string fileName) {
1167     if(fileName !in fileTokens) {
1168       fileTokens[fileName] = [];
1169       splitMultilinetokens(fileToDTokens(fileName), fileTokens[fileName]);
1170     }
1171   }
1172 
1173   string getValue() {
1174     size_t startIndex = 0;
1175     size_t possibleStartIndex = 0;
1176     size_t endIndex = 0;
1177 
1178     size_t lastStartIndex = 0;
1179     size_t lastEndIndex = 0;
1180 
1181     int paranthesisCount = 0;
1182     size_t begin;
1183     size_t end = getShouldIndex(tokens, line);
1184 
1185     if(end != 0) {
1186       begin = tokens.getPreviousIdentifier(end - 1);
1187 
1188       return tokens[begin .. end - 1].toString.strip;
1189     }
1190 
1191     auto beginAssert = getAssertIndex(tokens, line);
1192 
1193     if(beginAssert > 0) {
1194       begin = beginAssert + 4;
1195       end = getParameter(tokens, begin);
1196 
1197       return tokens[begin .. end].toString.strip;
1198     }
1199 
1200     return "";
1201   }
1202 
1203   override string toString() nothrow
1204   {
1205     auto separator = leftJustify("", 20, '-');
1206     string result = "\n" ~ separator ~ "\n" ~ file ~ ":" ~ line.to!string ~ "\n" ~ separator;
1207 
1208     if(tokens.length == 0) {
1209       return result ~ "\n";
1210     }
1211 
1212     size_t line = tokens[0].line - 1;
1213     size_t column = 1;
1214     bool afterErrorLine = false;
1215 
1216     foreach(token; this.tokens.filter!(token => token != tok!"whitespace")) {
1217       string prefix = "";
1218 
1219       foreach(lineNumber; line..token.line) {
1220         if(lineNumber < this.line -1 || afterErrorLine) {
1221           prefix ~= "\n" ~ rightJustify((lineNumber+1).to!string, 6, ' ') ~ ": ";
1222         } else {
1223           prefix ~= "\n>" ~ rightJustify((lineNumber+1).to!string, 5, ' ') ~ ": ";
1224         }
1225       }
1226 
1227       if(token.line != line) {
1228         column = 1;
1229       }
1230 
1231       if(token.column > column) {
1232         prefix ~= ' '.repeat.take(token.column - column).array;
1233       }
1234 
1235       auto stringRepresentation = token.text == "" ? str(token.type) : token.text;
1236 
1237       auto lines = stringRepresentation.split("\n");
1238 
1239       result ~= prefix ~ lines[0];
1240       line = token.line;
1241       column = token.column + stringRepresentation.length;
1242 
1243       if(token.line >= this.line && str(token.type) == ";") {
1244         afterErrorLine = true;
1245       }
1246     }
1247 
1248     return result;
1249   }
1250 
1251   void print(ResultPrinter printer)
1252   {
1253     if(tokens.length == 0) {
1254       return;
1255     }
1256 
1257     printer.primary("\n");
1258     printer.info(file ~ ":" ~ line.to!string);
1259 
1260     size_t line = tokens[0].line - 1;
1261     size_t column = 1;
1262     bool afterErrorLine = false;
1263 
1264     foreach(token; this.tokens.filter!(token => token != tok!"whitespace")) {
1265       foreach(lineNumber; line..token.line) {
1266         printer.primary("\n");
1267 
1268         if(lineNumber < this.line -1 || afterErrorLine) {
1269           printer.primary(rightJustify((lineNumber+1).to!string, 6, ' ') ~ ":");
1270         } else {
1271           printer.dangerReverse(">" ~ rightJustify((lineNumber+1).to!string, 5, ' ') ~ ":");
1272         }
1273       }
1274 
1275       if(token.line != line) {
1276         column = 1;
1277       }
1278 
1279       if(token.column > column) {
1280         printer.primary(' '.repeat.take(token.column - column).array);
1281       }
1282 
1283       auto stringRepresentation = token.text == "" ? str(token.type) : token.text;
1284 
1285       if(token.text == "" && str(token.type) != "whitespace") {
1286         printer.info(str(token.type));
1287       } else if(str(token.type).indexOf("Literal") != -1) {
1288         printer.success(token.text);
1289       } else {
1290         printer.primary(token.text);
1291       }
1292 
1293       line = token.line;
1294       column = token.column + stringRepresentation.length;
1295 
1296       if(token.line >= this.line && str(token.type) == ";") {
1297         afterErrorLine = true;
1298       }
1299     }
1300 
1301     printer.primary("\n");
1302   }
1303 }
1304 
1305 @("TestException should read the code from the file")
1306 unittest
1307 {
1308   auto result = new SourceResult("test/values.d", 26);
1309   auto msg = result.toString;
1310 
1311   msg.should.equal("\n--------------------\ntest/values.d:26\n--------------------\n" ~
1312                    "    23: unittest {\n" ~
1313                    "    24:   /++/\n" ~
1314                    "    25: \n" ~
1315                    ">   26:   [1, 2, 3]\n" ~
1316                    ">   27:     .should\n" ~
1317                    ">   28:     .contain(4);\n" ~
1318                    "    29: }");
1319 }
1320 
1321 @("TestException should print the lines before multiline tokens")
1322 unittest
1323 {
1324   auto result = new SourceResult("test/values.d", 45);
1325   auto msg = result.toString;
1326 
1327   msg.should.equal("\n--------------------\ntest/values.d:45\n--------------------\n" ~
1328                    "    40: unittest {\n" ~
1329                    "    41:   /*\n" ~
1330                    "    42:   Multi line comment\n" ~
1331                    "    43:   */\n" ~
1332                    "    44: \n" ~
1333                    ">   45:   `multi\n" ~
1334                    ">   46:   line\n" ~
1335                    ">   47:   string`\n" ~
1336                    ">   48:     .should\n" ~
1337                    ">   49:     .contain(`multi\n" ~
1338                    ">   50:   line\n" ~
1339                    ">   51:   string`);\n" ~
1340                    "    52: }");
1341 }
1342 
1343 /// Converts a file to D tokens provided by libDParse.
1344 /// All the whitespaces are ignored
1345 const(Token)[] fileToDTokens(string fileName) nothrow @trusted {
1346   try {
1347     auto f = File(fileName);
1348     immutable auto fileSize = f.size();
1349     ubyte[] fileBytes = new ubyte[](fileSize.to!size_t);
1350 
1351     if(f.rawRead(fileBytes).length != fileSize) {
1352       return [];
1353     }
1354 
1355     StringCache cache = StringCache(StringCache.defaultBucketCount);
1356 
1357     LexerConfig config;
1358     config.stringBehavior = StringBehavior.source;
1359     config.fileName = fileName;
1360     config.commentBehavior = CommentBehavior.intern;
1361 
1362     auto lexer = DLexer(fileBytes, config, &cache);
1363     const(Token)[] tokens = lexer.array;
1364 
1365     return tokens.map!(token => const Token(token.type, token.text.idup, token.line, token.column, token.index)).array;
1366   } catch(Throwable) {
1367     return [];
1368   }
1369 }
1370 
1371 @("TestException should ignore missing files")
1372 unittest
1373 {
1374   auto result = new SourceResult("test/missing.txt", 10);
1375   auto msg = result.toString;
1376 
1377   msg.should.equal("\n" ~ `--------------------
1378 test/missing.txt:10
1379 --------------------` ~ "\n");
1380 }
1381 
1382 @("Source reporter should find the tested value on scope start")
1383 unittest
1384 {
1385   auto result = new SourceResult("test/values.d", 4);
1386   result.getValue.should.equal("[1, 2, 3]");
1387 }
1388 
1389 @("Source reporter should find the tested value after a statment")
1390 unittest
1391 {
1392   auto result = new SourceResult("test/values.d", 12);
1393   result.getValue.should.equal("[1, 2, 3]");
1394 }
1395 
1396 @("Source reporter should find the tested value after a */ comment")
1397 unittest
1398 {
1399   auto result = new SourceResult("test/values.d", 20);
1400   result.getValue.should.equal("[1, 2, 3]");
1401 }
1402 
1403 @("Source reporter should find the tested value after a +/ comment")
1404 unittest
1405 {
1406   auto result = new SourceResult("test/values.d", 28);
1407   result.getValue.should.equal("[1, 2, 3]");
1408 }
1409 
1410 @("Source reporter should find the tested value after a // comment")
1411 unittest
1412 {
1413   auto result = new SourceResult("test/values.d", 36);
1414   result.getValue.should.equal("[1, 2, 3]");
1415 }
1416 
1417 @("Source reporter should find the tested value from an assert utility")
1418 unittest
1419 {
1420   auto result = new SourceResult("test/values.d", 55);
1421   result.getValue.should.equal("5");
1422 
1423   result = new SourceResult("test/values.d", 56);
1424   result.getValue.should.equal("(5+1)");
1425 
1426   result = new SourceResult("test/values.d", 57);
1427   result.getValue.should.equal("(5, (11))");
1428 }
1429 
1430 @("Source reporter should get the value from multiple should asserts")
1431 unittest
1432 {
1433   auto result = new SourceResult("test/values.d", 61);
1434   result.getValue.should.equal("5");
1435 
1436   result = new SourceResult("test/values.d", 62);
1437   result.getValue.should.equal("(5+1)");
1438 
1439   result = new SourceResult("test/values.d", 63);
1440   result.getValue.should.equal("(5, (11))");
1441 }
1442 
1443 @("Source reporter should get the value after a scope")
1444 unittest
1445 {
1446   auto result = new SourceResult("test/values.d", 71);
1447   result.getValue.should.equal("found");
1448 }
1449 
1450 @("Source reporter should get a function call value")
1451 unittest
1452 {
1453   auto result = new SourceResult("test/values.d", 75);
1454   result.getValue.should.equal("found(4)");
1455 }
1456 
1457 @("Source reporter should parse nested lambdas")
1458 unittest
1459 {
1460   auto result = new SourceResult("test/values.d", 81);
1461   result.getValue.should.equal("({
1462     ({ }).should.beNull;
1463   })");
1464 }
1465 
1466 /// Source reporter should print the source code
1467 unittest
1468 {
1469   auto result = new SourceResult("test/values.d", 36);
1470   auto printer = new MockPrinter();
1471 
1472   result.print(printer);
1473 
1474 
1475   auto lines = printer.buffer.split("[primary:\n]");
1476 
1477   lines[1].should.equal(`[info:test/values.d:36]`);
1478   lines[2].should.equal(`[primary:    31:][info:unittest][primary: ][info:{]`);
1479   lines[7].should.equal(`[dangerReverse:>   36:][primary:    ][info:.][primary:contain][info:(][success:4][info:)][info:;]`);
1480 }
1481 
1482 /// split multiline tokens in multiple single line tokens with the same type
1483 void splitMultilinetokens(const(Token)[] tokens, ref const(Token)[] result) nothrow @trusted {
1484 
1485   try {
1486     foreach(token; tokens) {
1487       auto pieces = token.text.idup.split("\n");
1488 
1489       if(pieces.length <= 1) {
1490         result ~= const Token(token.type, token.text.dup, token.line, token.column, token.index);
1491       } else {
1492         size_t line = token.line;
1493         size_t column = token.column;
1494 
1495         foreach(textPiece; pieces) {
1496           result ~= const Token(token.type, textPiece, line, column, token.index);
1497           line++;
1498           column = 1;
1499         }
1500       }
1501     }
1502   } catch(Throwable) {}
1503 }
1504 
1505 /// A new line sepparator
1506 class SeparatorResult : IResult {
1507   override string toString() {
1508     return "\n";
1509   }
1510 
1511   void print(ResultPrinter printer) {
1512     printer.primary("\n");
1513   }
1514 }
1515 
1516 class ListInfoResult : IResult {
1517   private {
1518     struct Item {
1519       string singular;
1520       string plural;
1521       string[] valueList;
1522 
1523       string key() {
1524         return valueList.length > 1 ? plural : singular;
1525       }
1526 
1527       MessageResult toMessage(size_t indentation = 0) {
1528         auto printableKey = rightJustify(key ~ ":", indentation, ' ');
1529 
1530         auto result = new MessageResult(printableKey);
1531 
1532         string glue;
1533         foreach(value; valueList) {
1534           result.addText(glue);
1535           result.addValue(value);
1536           glue = ",";
1537         }
1538 
1539         return result;
1540       }
1541     }
1542 
1543     Item[] items;
1544   }
1545 
1546   void add(string key, string value) {
1547     items ~= Item(key, "", [value]);
1548   }
1549 
1550   void add(string singular, string plural, string[] valueList) {
1551     items ~= Item(singular, plural, valueList);
1552   }
1553 
1554   private size_t indentation() {
1555     auto elements = items.filter!"a.valueList.length > 0";
1556 
1557     if(elements.empty) {
1558       return 0;
1559     }
1560 
1561     return elements.map!"a.key".map!"a.length".maxElement + 2;
1562   }
1563 
1564   override string toString() {
1565     auto indent = indentation;
1566     auto elements = items.filter!"a.valueList.length > 0";
1567 
1568     if(elements.empty) {
1569       return "";
1570     }
1571 
1572     return "\n" ~ elements.map!(a => a.toMessage(indent)).map!"a.toString".join("\n");
1573   }
1574 
1575   void print(ResultPrinter printer) {
1576     auto indent = indentation;
1577     auto elements = items.filter!"a.valueList.length > 0";
1578 
1579     if(elements.empty) {
1580       return;
1581     }
1582 
1583     foreach(item; elements) {
1584       printer.primary("\n");
1585       item.toMessage(indent).print(printer);
1586     }
1587   }
1588 }
1589 
1590 /// convert to string the added data to ListInfoResult
1591 unittest {
1592   auto result = new ListInfoResult();
1593 
1594   result.add("a", "1");
1595   result.add("ab", "2");
1596   result.add("abc", "3");
1597 
1598   result.toString.should.equal(`
1599    a:1
1600   ab:2
1601  abc:3`);
1602 }
1603 
1604 /// print the added data to ListInfoResult
1605 unittest {
1606   auto printer = new MockPrinter();
1607   auto result = new ListInfoResult();
1608 
1609   result.add("a", "1");
1610   result.add("ab", "2");
1611   result.add("abc", "3");
1612 
1613   result.print(printer);
1614 
1615   printer.buffer.should.equal(`[primary:
1616 ][primary:   a:][primary:][info:1][primary:
1617 ][primary:  ab:][primary:][info:2][primary:
1618 ][primary: abc:][primary:][info:3]`);
1619 }
1620 
1621 
1622 /// convert to string the added data lists to ListInfoResult
1623 unittest {
1624   auto result = new ListInfoResult();
1625 
1626   result.add("a", "as", ["1", "2","3"]);
1627   result.add("ab", "abs", ["2", "3"]);
1628   result.add("abc", "abcs", ["3"]);
1629   result.add("abcd", "abcds", []);
1630 
1631   result.toString.should.equal(`
1632   as:1,2,3
1633  abs:2,3
1634  abc:3`);
1635 }
1636 
1637 IResult[] toResults(Exception e) nothrow @trusted {
1638   try {
1639     return [ new MessageResult(e.message.to!string) ];
1640   } catch(Exception) {
1641     return [ new MessageResult("Unknown error!") ];
1642   }
1643 }