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 }