fluentasserts.core.results 564/615(91%) line coverage

      
10
20
30
40
50
60
70
80
90
100
110
120
130
140
150
160
170
180
190
200
210
220
230
240
250
260
270
280
290
300
310
320
330
340
350
360
370
380
390
400
410
420
430
440
450
460
470
480
490
500
510
520
530
540
550
560
570
580
590
600
610
620
634
644
654
664
674
680
690
704
714
720
734
744
754
764
770
780
790
800
810
820
830
840
850
860
870
880
890
900
910
920
930
940
950
9644
970
980
990
10030
1010
1020
1030
1040
1050
1060
1070
1084
1090
1100
1110
1121
1130
1140
1150
1160
1170
1180
1190
1200
1210
1220
1230
1240
1250
1260
1276
1280
1296
1301
1310
1320
1335
1340
1350
1360
1370
1380
1390
1403
1410
1420
1430
1440
1450
1460
1470
1480
1490
1500
1510
1520
1530
1540
1550
1560
1570
1580
1590
1600
1610
1620
1630
1640
1650
1660
1670
1680
1690
1700
1710
1720
1730
1740
1750
1760
1770
1780
1790
1800
1810
1820
18317
1840
18517
1860
1870
1883522
1890
1900
1911071
1920
1930
1940
1953522
1960
1973522
1983522
1990
2003522
2010
2020
2030
2044484
2050
2060
2070
2080
2090
2100
2110
2124467
2130
2140
2150
21619994
2170
2180
2190
22019994
2210
2220
2230
2247
2250
2260
2270
2280
2290
2300
2310
2320
23354
23413
2354
2360
2379
2380
2390
2400
2410
2420
2430
2440
2450
2460
2470
2480
2490
2501
2512
2520
2530
2540
2550
2560
2571
2582
2590
2600
2610
2620
2630
2640
2651
2660
2670
2681
2691
2701
2710
2721
2732
2740
2750
2760
2770
2780
2791
2801
2811
2820
2832
2840
2850
2860
2870
2880
2891
2901
2911
2920
2932
2940
2950
2960
2970
2980
2991
3001
3011
3020
3031
3041
3050
3062
3070
3080
3090
3100
3110
3120
3130
3140
3150
3160
3170
318108
3190
320108
321108
3220
3230
3240
325738
326152
327152
328212
329212
330374
331374
3320
3330
3340
3350
336946
3370
3380
3390
3400
3410
3420
3430
3440
3450
3460
3470
3480
3490
3500
3510
3520
3530
3540
3550
3560
3570
3580
3590
3600
3610
3620
3631
3642
3650
3660
3670
3680
3690
3701
3710
3720
3731
3741
3751
3761
3770
3781
3792
3800
3810
3820
3830
3840
3850
3860
3870
3880
3891153
3901153
3911153
3920
3930
3940
3950
3960
3970
3980
3990
4002240
40194
4020
4030
4042146
4050
4060
4070
4080
4095
4101
4110
4120
4134
4144
4150
4164
4170
4184
41930
4206
4212
4222
4232
4240
4250
4266
4270
4286
4290
4300
4310
4320
4330
4340
4350
4360
4370
4380
4390
4400
4416
4420
4436
4440
445158
44665
4470
44835
4490
45064
45110
4520
4530
45435
4550
4560
45748
45810
4595
4600
4615
4620
4630
4640
4650
4660
46744
4687
4690
4700
47156
4722
4730
4740
47526
4760
4770
4780
47944
4807
4810
4820
48328
4840
4850
4860
48728
4882
4890
4900
49126
4920
4930
4940
4950
4962146
4970
4980
4990
5000
5010
5020
5031
5041
5050
5061
5072
5080
5090
5100
5110
5121
5131
5140
5151
5162
5170
5180
5190
5200
5211
5221
5230
5241
5252
5260
5270
5280
5290
5301
5311
5320
5331
5340
5352
5360
5370
5380
5390
5400
5411
5420
5430
5441
5451
5461
5470
5481
5491
5500
5511
5520
5532
5540
5550
5560
5570
5580
5590
5600
5610
5620
5630
5640
5650
5660
5670
5680
5690
5700
571546
572546
573546
5740
5750
5760
5771064
5781064
5791064
5801064
5810
5822080
5831016
5840
5850
5861112
5872124
5880
5890
5901064
5910
5920
5930
5940
5950
5960
5970
5980
5990
6000
6010
6020
6030
6040
6050
6060
6070
6080
6090
6100
6110
6120
6131
6142
6150
6160
6170
6180
6190
6201
6212
6220
6230
6240
6250
6260
6271
6282
6290
6300
6310
6320
6330
6340
6350
6361
6372
6380
6390
6400
6410
6420
6430
6440
6450
6460
6470
6480
6490
6500
6510
6520
65328
6540
65528
65628
6570
6580
6590
6600
66156
66256
66356
66456
6650
666100
66756
6680
6690
67068
67112
6720
6730
67456
6750
6760
6770
6780
6790
6800
6810
6820
6830
6840
6850
6860
6870
6880
6890
6900
6910
6920
6932093
6940
695119693
69635351
6973121
6980
69951510
7000
7010
7020
7032093
7040
7050
7060
7073552
7083552
7093552
7103552
7110
7123552
7133552
7143552
7150
71618887164
7174720903
7180
7194720903
72088768
72188768
7220
7230
7244720903
72579286
7260
7270
7284720903
72981392
7300
7310
7324720903
733745869
73412677
73512677
7360
7370
738194825
7393552
7403552
7410
7423552
7430
7440
7450
7460
7473552
7480
7490
7500
7510
7521
7531
7540
7551
7561
7570
7582
7590
7600
7610
7620
7630
7640
7650
7660
7671
7681
7690
7701
7711
7720
7732
7740
7750
7760
7770
7780
7790
7801
7811
7820
7831
7841
7850
7862
7870
7880
7890
7900
7910
7923542
7933542
7940
7950
7961308238
797326174
7980
799326174
80021271
8010
8020
803326174
80421271
8050
8060
807331472
808544
809544
8100
8110
812338594
8132998
8140
8150
8160
817544
8180
819204888
82051086
8210
82251086
8231072
8240
8250
82651086
8271072
8280
8291072
830544
831544
8320
8330
8340
8350
836544
8370
8380
8390
8400
8411
8421
8430
8441
8451
8461
8470
8482
8490
8500
8510
8520
8530
8540
8550
8560
8570
8581
8591
8600
8611
8621
8631
8640
8652
8660
8670
8680
8690
8700
8710
8720
8730
8740
8751
8761
8770
8781
8790
8802
8810
8820
8830
8840
8850
8860
8870
8880
8898615
8908615
8910
8928615
8938615
8940
895182285
89657890
89757890
8980
89957890
9006072
9010
9020
90357890
9043073
9050
9060
90757890
9082999
9090
9100
91154891
91217898
9130
9140
91536993
916288
9170
9180
91973142
920438
9210
9220
92336267
9243804
9250
9260
92732463
9281086
9290
9300
93131377
9322099
9330
9340
93535001
9360
9370
9380
9390
94031377
9410
9420
9430
9440
9450
9460
9470
9480
9490
9501
9511
9520
9531
9540
9551
9560
9572
9580
9590
9600
9610
9621
9631
9640
9651
9660
9671
9680
9691
9700
9712
9720
9730
9740
9750
9761
9771
9780
9791
9800
9811
9820
9831
9840
9852
9860
9870
9880
9890
9901
9911
9920
9931
9940
9951
9961
9970
9982
9990
10000
10010
10021474
10030
100465353
1005125
10060
10070
10081474
10091458
10100
10110
101216
10130
10140
10150
10160
10171
10181
10190
10201
10210
10222
10230
10240
10250
102615
10270
1028264
102983
10300
1031159
103210
10330
10340
1035157
103612
10372
10380
10390
104010
10410
10420
104381
104451
10450
10460
104730
104813
10490
10500
10510
10520
10530
10540
10550
10560
10570
10581
10591
10600
10611
10621
10632
10640
10650
10660
10670
10681
10691
10700
10711
10721
10732
10740
10750
10760
10770
10781
10791
10800
10811
10821
10830
10841
10850
10862
10870
10880
10890
10900
10911
10921
10930
10941
10951
10960
10971
10980
10992
11000
11010
11020
11033537
11040
1105377129
11069694
11070
11080
11093537
11101471
11110
11120
11132066
11140
11150
11160
11170
11181
11191
11200
11211
11220
11231
11242
11252
11262
11270
11280
11290
11300
11310
11320
11330
11340
11350
11360
11370
11380
11390
11400
11410
11420
11430
11440
11450
11460
11473540
11483542
11493542
11500
11513542
11520
11532
11540
11550
11560
11573540
11583540
11590
11603540
11613540
11620
11633540
11640
11650
11660
11670
11680
11693540
117026
117126
11720
11730
11740
11750
11763536
11773536
11783536
11790
11803536
11813536
11820
11833536
11843536
11853536
11860
11873536
11882065
11890
11902065
11910
11920
11931471
11940
11951471
119613
119713
11980
119913
12000
12010
12021458
12030
12040
12050
12060
12071064
12081064
12090
12101064
12113
12120
12130
12141061
12151061
12161061
12170
1218131679
121930910
12200
1221102549
12225423
12232196
12240
12251077
12260
12270
12280
122930910
12303223
12310
12320
123330910
12343709
12350
12360
123761820
12380
123930910
12400
124130910
124230910
124330910
12440
124559424
12462117
12470
12480
12490
12501061
12510
12520
12530
12540
12551
12560
12570
12580
12591
12601
12610
12621
12631
12641
12650
126692
126778
12687
12690
12709
12716
12720
12731
12740
12750
12760
127719
12786
12790
12800
128119
12827
12830
12840
128538
12860
128731
128812
12897
12904
12910
12923
12930
12940
129519
129619
12970
129826
12991
13000
13010
13020
13031
13040
13050
13060
13070
13080
13090
13101
13111
13120
13132
13140
13150
13160
13170
13180
13190
13200
13210
13220
13230
13240
13250
13261
13271
13280
13292
13300
13310
13320
13330
13340
13350
13360
13370
13380
13390
13400
13410
13420
13430
13440
13450
13460
13470
13480
134984
135042
135142
13520
135342
13540
13550
13560
135784
13580
135942
136042
136142
136242
13630
136442
136542
13660
136765635
13680
13690
13700
13710
13720
13730
13740
13750
13761
13771
13780
13792
13800
13810
13820
13830
13840
13850
13860
13871
13882
13890
13900
13910
13920
13930
13941
13952
13960
13970
13980
13990
14000
14011
14022
14030
14040
14050
14060
14070
14081
14092
14100
14110
14120
14130
14140
14151
14162
14170
14180
14190
14200
14210
14221
14232
14240
14251
14262
14270
14281
14292
14300
14310
14320
14330
14340
14351
14362
14370
14381
14392
14400
14411
14422
14430
14440
14450
14460
14470
14481
14492
14500
14510
14520
14530
14540
14551
14562
14570
14580
14590
14600
14610
14621
14632
14640
14650
14660
14670
14680
14690
14700
14711
14721
14730
14741
14750
14760
14771
14780
14792
14802
14812
14820
14830
14840
14850
14860
14870
1488196905
148965593
14900
149165593
149258892
14930
14946701
14956701
14960
149766348
149815415
149915415
150015415
15010
15020
15030
15040
15050
15060
15070
15080
15090
15100
15110
15120
15130
15140
15150
15160
15170
15180
15190
15200
15210
15220
15230
15240
15250
152646
15270
15280
15290
15309
15310
15329
15330
15349
153563
153612
153712
153812
15390
15400
15419
15420
15430
15440
15450
15460
15470
15480
15496
15500
15510
15520
15534
15540
15550
15560
15573
15580
15593
15600
15610
15620
15633
15640
15650
15660
15672
15682
15690
15702
15710
15720
15730
15748
15750
15760
15770
15781
15791
15800
15811
15820
15830
15840
158511
15863
15873
15880
15890
15900
15910
15920
15930
15941
15950
15961
15971
15981
15990
16002
16010
16020
16030
16040
16050
16060
16070
16081
16091
16100
16111
16121
16131
16140
16151
16160
16172
16180
16190
16200
16210
16220
16230
16240
16250
16261
16270
16281
16291
16301
16311
16320
16332
16340
16350
16360
16370
module fluentasserts.core.results; import std.stdio; import std.file; import std.algorithm; import std.conv; import std.range; import std.string; import std.exception; import std.typecons; import dparse.lexer; import dparse.parser; @safe: /// Glyphs used to display special chars in the results struct ResultGlyphs { static { /// Glyph for the tab char string tab; /// Glyph for the \r char string carriageReturn; /// Glyph for the \n char string newline; /// Glyph for the space char string space; /// Glyph for the \0 char string nullChar; /// Glyph that indicates the error line string sourceIndicator; /// Glyph that sepparates the line number string sourceLineSeparator; /// Glyph for the diff begin indicator string diffBegin; /// Glyph for the diff end indicator string diffEnd; /// Glyph that marks an inserted text in diff string diffInsert; /// Glyph that marks deleted text in diff string diffDelete; } /// Set the default values. The values are static resetDefaults() { version(windows) { ResultGlyphs.tab = `\t`; ResultGlyphs.carriageReturn = `\r`; ResultGlyphs.newline = `\n`; ResultGlyphs.space = ` `; ResultGlyphs.nullChar = `␀`; } else { ResultGlyphs.tab = `¤`; ResultGlyphs.carriageReturn = `←`; ResultGlyphs.newline = `↲`; ResultGlyphs.space = `᛫`; ResultGlyphs.nullChar = `\0`; } ResultGlyphs.sourceIndicator = ">"; ResultGlyphs.sourceLineSeparator = ":"; ResultGlyphs.diffBegin = "["; ResultGlyphs.diffEnd = "]"; ResultGlyphs.diffInsert = "+"; ResultGlyphs.diffDelete = "-"; } } /// interface ResultPrinter { void primary(string); void info(string); void danger(string); void success(string); void dangerReverse(string); void successReverse(string); } version(unittest) { class MockPrinter : ResultPrinter { string buffer; void primary(string val) { buffer ~= "[primary:" ~ val ~ "]"; } void info(string val) { buffer ~= "[info:" ~ val ~ "]"; } void danger(string val) { buffer ~= "[danger:" ~ val ~ "]"; } void success(string val) { buffer ~= "[success:" ~ val ~ "]"; } void dangerReverse(string val) { buffer ~= "[dangerReverse:" ~ val ~ "]"; } void successReverse(string val) { buffer ~= "[successReverse:" ~ val ~ "]"; } } } struct WhiteIntervals { size_t left; size_t right; } WhiteIntervals getWhiteIntervals(string text) { auto stripText = text.strip; if(stripText == "") { return WhiteIntervals(0, 0); } return WhiteIntervals(text.indexOf(stripText[0]), text.lastIndexOf(stripText[stripText.length - 1])); } /// This is the most simple implementation of a ResultPrinter. /// All the plain data is printed to stdout class DefaultResultPrinter : ResultPrinter { void primary(string text) { write(text); } void info(string text) { write(text); } void danger(string text) { write(text); } void success(string text) { write(text); } void dangerReverse(string text) { write(text); } void successReverse(string text) { write(text); } } interface IResult { string toString(); void print(ResultPrinter); } /// A result that prints a simple message to the user class MessageResult : IResult { private { struct Message { bool isValue; string text; } Message[] messages; } this(string message) nothrow { add(false, message); } this() nothrow { } override string toString() { return messages.map!"a.text".join.to!string; } void startWith(string message) @safe nothrow { Message[] newMessages; newMessages ~= Message(false, message); newMessages ~= this.messages; this.messages = newMessages; } void add(bool isValue, string message) nothrow { this.messages ~= Message(isValue, message .replace("\r", ResultGlyphs.carriageReturn) .replace("\n", ResultGlyphs.newline) .replace("\0", ResultGlyphs.nullChar) .replace("\t", ResultGlyphs.tab)); } void addValue(string text) @safe nothrow { add(true, text); } void addText(string text) @safe nothrow { if(text == "throwAnyException") { text = "throw any exception"; } this.messages ~= Message(false, text); } void prependText(string text) @safe nothrow { this.messages = Message(false, text) ~ this.messages; } void prependValue(string text) @safe nothrow { this.messages = Message(true, text) ~ this.messages; } void print(ResultPrinter printer) { foreach(message; messages) { if(message.isValue) { printer.info(message.text); } else { printer.primary(message.text); } } } } version (unittest) { import fluentasserts.core.base; } @("Message result should return the message") unittest { auto result = new MessageResult("Message"); result.toString.should.equal("Message"); } @("Message result should replace the special chars") unittest { auto result = new MessageResult("\t \r\n"); result.toString.should.equal(`¤ ←↲`); } @("Message result should replace the special chars with the custom glyphs") unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.tab = `\t`; ResultGlyphs.carriageReturn = `\r`; ResultGlyphs.newline = `\n`; auto result = new MessageResult("\t \r\n"); result.toString.should.equal(`\t \r\n`); } @("Message result should return values as string") unittest { auto result = new MessageResult("text"); result.addValue("value"); result.addText("text"); result.toString.should.equal(`textvaluetext`); } @("Message result should print a string as primary") unittest { auto result = new MessageResult("\t \r\n"); auto printer = new MockPrinter; result.print(printer); printer.buffer.should.equal(`[primary:¤ ←↲]`); } @("Message result should print values as info") unittest { auto result = new MessageResult("text"); result.addValue("value"); result.addText("text"); auto printer = new MockPrinter; result.print(printer); printer.buffer.should.equal(`[primary:text][info:value][primary:text]`); } class DiffResult : IResult { import ddmp.diff; protected { string expected; string actual; } this(string expected, string actual) { this.expected = expected.replace("\0", ResultGlyphs.nullChar); this.actual = actual.replace("\0", ResultGlyphs.nullChar); } private string getResult(const Diff d) { final switch(d.operation) { case Operation.DELETE: return ResultGlyphs.diffBegin ~ ResultGlyphs.diffDelete ~ d.text ~ ResultGlyphs.diffEnd; case Operation.INSERT: return ResultGlyphs.diffBegin ~ ResultGlyphs.diffInsert ~ d.text ~ ResultGlyphs.diffEnd; case Operation.EQUAL: return d.text; } } override string toString() @trusted { return "Diff:\n" ~ diff_main(expected, actual).map!(a => getResult(a)).join; } void print(ResultPrinter printer) @trusted { auto result = diff_main(expected, actual); printer.info("Diff:"); foreach(diff; result) { if(diff.operation == Operation.EQUAL) { printer.primary(diff.text); } if(diff.operation == Operation.INSERT) { printer.successReverse(diff.text); } if(diff.operation == Operation.DELETE) { printer.dangerReverse(diff.text); } } printer.primary("\n"); } } /// DiffResult should find the differences unittest { auto diff = new DiffResult("abc", "asc"); diff.toString.should.equal("Diff:\na[-b][+s]c"); } /// DiffResult should use the custom glyphs unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.diffBegin = "{"; ResultGlyphs.diffEnd = "}"; ResultGlyphs.diffInsert = "!"; ResultGlyphs.diffDelete = "?"; auto diff = new DiffResult("abc", "asc"); diff.toString.should.equal("Diff:\na{?b}{!s}c"); } class KeyResult(string key) : IResult { private immutable { string value; size_t indent; } this(string value, size_t indent = 10) { this.value = value.replace("\0", ResultGlyphs.nullChar); this.indent = indent; } bool hasValue() { return value != ""; } override string toString() { if(value == "") { return ""; } return rightJustify(key ~ ":", indent, ' ') ~ printableValue; } void print(ResultPrinter printer) { if(value == "") { return; } printer.info(rightJustify(key ~ ":", indent, ' ')); auto lines = value.split("\n"); auto spaces = rightJustify(":", indent, ' '); int index; foreach(line; lines) { if(index > 0) { printer.info(ResultGlyphs.newline); printer.primary("\n"); printer.info(spaces); } printLine(line, printer); index++; } } private { struct Message { bool isSpecial; string text; } void printLine(string line, ResultPrinter printer) { Message[] messages; auto whiteIntervals = line.getWhiteIntervals; foreach(size_t index, ch; line) { bool showSpaces = index < whiteIntervals.left || index >= whiteIntervals.right; auto special = isSpecial(ch, showSpaces); if(messages.length == 0 || messages[messages.length - 1].isSpecial != special) { messages ~= Message(special, ""); } messages[messages.length - 1].text ~= toVisible(ch, showSpaces); } foreach(message; messages) { if(message.isSpecial) { printer.info(message.text); } else { printer.primary(message.text); } } } bool isSpecial(T)(T ch, bool showSpaces) { if(ch == ' ' && showSpaces) { return true; } if(ch == '\r' || ch == '\t') { return true; } return false; } string toVisible(T)(T ch, bool showSpaces) { if(ch == ' ' && showSpaces) { return ResultGlyphs.space; } if(ch == '\r') { return ResultGlyphs.carriageReturn; } if(ch == '\t') { return ResultGlyphs.tab; } return ch.to!string; } pure string printableValue() { return value.split("\n").join("\\n\n" ~ rightJustify(":", indent, ' ')); } } } /// KeyResult should not dispaly spaces between words with special chars unittest { auto result = new KeyResult!"key"(" row1 row2 "); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][info:᛫][primary:row1 row2][info:᛫]`); } /// KeyResult should dispaly spaces with special chars on space lines unittest { auto result = new KeyResult!"key"(" "); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][info:᛫᛫᛫]`); } /// KeyResult should display no char for empty lines unittest { auto result = new KeyResult!"key"(""); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(``); } /// KeyResult should display special characters with different contexts unittest { auto result = new KeyResult!"key"("row1\n \trow2"); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][primary:row1][info:↲][primary:` ~ "\n" ~ `][info: :][info:᛫¤][primary:row2]`); } /// KeyResult should display custom glyphs with different contexts unittest { scope(exit) { ResultGlyphs.resetDefaults; } ResultGlyphs.newline = `\n`; ResultGlyphs.tab = `\t`; ResultGlyphs.space = ` `; auto result = new KeyResult!"key"("row1\n \trow2"); auto printer = new MockPrinter(); result.print(printer); printer.buffer.should.equal(`[info: key:][primary:row1][info:\n][primary:` ~ "\n" ~ `][info: :][info: \t][primary:row2]`); } /// class ExpectedActualResult : IResult { protected { string title; KeyResult!"Expected" expected; KeyResult!"Actual" actual; } this(string title, string expected, string actual) nothrow @safe { this.title = title; this(expected, actual); } this(string expected, string actual) nothrow @safe { this.expected = new KeyResult!"Expected"(expected); this.actual = new KeyResult!"Actual"(actual); } override string toString() { auto line1 = expected.toString; auto line2 = actual.toString; string glue; string prefix; if(line1 != "" && line2 != "") { glue = "\n"; } if(line1 != "" || line2 != "") { prefix = title == "" ? "\n" : ("\n" ~ title ~ "\n"); } return prefix ~ line1 ~ glue ~ line2; } void print(ResultPrinter printer) { auto line1 = expected.toString; auto line2 = actual.toString; if(actual.hasValue || expected.hasValue) { printer.info(title == "" ? "\n" : ("\n" ~ title ~ "\n")); } expected.print(printer); if(actual.hasValue && expected.hasValue) { printer.primary("\n"); } actual.print(printer); } } @("ExpectedActual result should be empty when no data is provided") unittest { auto result = new ExpectedActualResult("", ""); result.toString.should.equal(""); } @("ExpectedActual result should be empty when null data is provided") unittest { auto result = new ExpectedActualResult(null, null); result.toString.should.equal(""); } @("ExpectedActual result should show one line of the expected and actual data") unittest { auto result = new ExpectedActualResult("data", "data"); result.toString.should.equal(` Expected:data Actual:data`); } @("ExpectedActual result should show one line of the expected and actual data") unittest { auto result = new ExpectedActualResult("data\ndata", "data\ndata"); result.toString.should.equal(` Expected:data\n :data Actual:data\n :data`); } /// A result that displays differences between ranges class ExtraMissingResult : IResult { protected { KeyResult!"Extra" extra; KeyResult!"Missing" missing; } this(string extra, string missing) { this.extra = new KeyResult!"Extra"(extra); this.missing = new KeyResult!"Missing"(missing); } override string toString() { auto line1 = extra.toString; auto line2 = missing.toString; string glue; string prefix; if(line1 != "" || line2 != "") { prefix = "\n"; } if(line1 != "" && line2 != "") { glue = "\n"; } return prefix ~ line1 ~ glue ~ line2; } void print(ResultPrinter printer) { if(extra.hasValue || missing.hasValue) { printer.primary("\n"); } extra.print(printer); if(extra.hasValue && missing.hasValue) { printer.primary("\n"); } missing.print(printer); } } string toString(const(Token)[] tokens) { string result; foreach(token; tokens.filter!(a => str(a.type) != "comment")) { if(str(token.type) == "whitespace" && token.text == "") { result ~= "\n"; } else { result ~= token.text == "" ? str(token.type) : token.text; } } return result; } auto getScope(const(Token)[] tokens, size_t line) nothrow { bool foundScope; bool foundAssert; size_t beginToken; size_t endToken = tokens.length; int paranthesisCount = 0; int scopeLevel; size_t[size_t] paranthesisLevels; foreach(i, token; tokens) { string type = str(token.type); if(type == "{") { paranthesisLevels[paranthesisCount] = i; paranthesisCount++; } if(type == "}") { paranthesisCount--; } if(line == token.line) { foundScope = true; } if(foundScope) { if(token.text == "should" || token.text == "Assert" || type == "assert" || type == ";") { foundAssert = true; scopeLevel = paranthesisCount; } if(type == "}" && paranthesisCount <= scopeLevel) { beginToken = paranthesisLevels[paranthesisCount]; endToken = i + 1; break; } } } return const Tuple!(size_t, "begin", size_t, "end")(beginToken, endToken); } /// Get the spec function and scope that contains a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 101); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("it(\"should throw an exception if we request 2 android devices\", { ({ auto result = [ device1.idup, device2.idup ].filterBy(RunOptions(\"\", \"android\", 2)).array; }).should.throwException!DeviceException.withMessage.equal(\"You requested 2 `androdid` devices, but there is only 1 healthy.\"); }"); } /// Get the a method scope and signature unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/class.d"), tokens); auto result = getScope(tokens, 10); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("void bar() { assert(false); }"); } /// Get the a method scope without assert unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/class.d"), tokens); auto result = getScope(tokens, 14); auto identifierStart = getPreviousIdentifier(tokens, result.begin); tokens[identifierStart .. result.end].toString.strip.should.equal("void bar2() { enforce(false); }"); } size_t getFunctionEnd(const(Token)[] tokens, size_t start) { int paranthesisCount; size_t result = start; // iterate the parameters foreach(i, token; tokens[start .. $]) { string type = str(token.type); if(type == "(") { paranthesisCount++; } if(type == ")") { paranthesisCount--; } if(type == "{" && paranthesisCount == 0) { result = start + i; break; } if(type == ";" && paranthesisCount == 0) { return start + i; } } paranthesisCount = 0; // iterate the scope foreach(i, token; tokens[result .. $]) { string type = str(token.type); if(type == "{") { paranthesisCount++; } if(type == "}") { paranthesisCount--; if(paranthesisCount == 0) { result = result + i; break; } } } return result; } /// Get the end of a spec function with a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 101); auto identifierStart = getPreviousIdentifier(tokens, result.begin); auto functionEnd = getFunctionEnd(tokens, identifierStart); tokens[identifierStart .. functionEnd].toString.strip.should.equal("it(\"should throw an exception if we request 2 android devices\", { ({ auto result = [ device1.idup, device2.idup ].filterBy(RunOptions(\"\", \"android\", 2)).array; }).should.throwException!DeviceException.withMessage.equal(\"You requested 2 `androdid` devices, but there is only 1 healthy.\"); })"); } /// Get the end of an unittest function with a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 81); auto identifierStart = getPreviousIdentifier(tokens, result.begin); auto functionEnd = getFunctionEnd(tokens, identifierStart) + 1; tokens[identifierStart .. functionEnd].toString.strip.should.equal("unittest { ({ ({ }).should.beNull; }).should.throwException!TestException.msg; }"); } /// Get tokens from a scope that contains a lambda unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getScope(tokens, 81); tokens[result.begin .. result.end].toString.strip.should.equal(`{ ({ ({ }).should.beNull; }).should.throwException!TestException.msg; }`); } size_t getPreviousIdentifier(const(Token)[] tokens, size_t startIndex) { enforce(startIndex > 0); enforce(startIndex < tokens.length); int paranthesisCount; bool foundIdentifier; foreach(i; 0..startIndex) { auto index = startIndex - i - 1; auto type = str(tokens[index].type); if(type == "(") { paranthesisCount--; } if(type == ")") { paranthesisCount++; } if(paranthesisCount < 0) { return getPreviousIdentifier(tokens, index - 1); } if(paranthesisCount != 0) { continue; } if(type == "unittest") { return index; } if(type == "{" || type == "}") { return index + 1; } if(type == ";") { return index + 1; } if(type == "=") { return index + 1; } if(type == ".") { foundIdentifier = false; } if(type == "identifier" && foundIdentifier) { foundIdentifier = true; continue; } if(foundIdentifier) { return index; } } return 0; } /// Get the the previous unittest identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 81); auto result = getPreviousIdentifier(tokens, scopeResult.begin); tokens[result .. scopeResult.begin].toString.strip.should.equal(`unittest`); } /// Get the the previous paranthesis identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 63); auto end = scopeResult.end - 11; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`(5, (11))`); } /// Get the the previous function call identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 75); auto end = scopeResult.end - 11; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`found(4)`); } /// Get the the previous map!"" identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 85); auto end = scopeResult.end - 12; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[1, 2, 3].map!"a"`); } size_t getAssertIndex(const(Token)[] tokens, size_t startLine) { auto assertTokens = tokens .enumerate .filter!(a => a[1].text == "Assert") .filter!(a => a[1].line <= startLine) .array; if(assertTokens.length == 0) { return 0; } return assertTokens[assertTokens.length - 1].index; } /// Get the index of the Assert structure identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getAssertIndex(tokens, 55); tokens[result .. result + 4].toString.strip.should.equal(`Assert.equal(`); } auto getParameter(const(Token)[] tokens, size_t startToken) { size_t paranthesisCount; foreach(i; startToken..tokens.length) { string type = str(tokens[i].type); if(type == "(" || type == "[") { paranthesisCount++; } if(type == ")" || type == "]") { if(paranthesisCount == 0) { return i; } paranthesisCount--; } if(paranthesisCount > 0) { continue; } if(type == ",") { return i; } } return 0; } /// Get the first parameter from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto begin = getAssertIndex(tokens, 57) + 4; auto end = getParameter(tokens, begin); tokens[begin .. end].toString.strip.should.equal(`(5, (11))`); } /// Get the first list parameter from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto begin = getAssertIndex(tokens, 89) + 4; auto end = getParameter(tokens, begin); tokens[begin .. end].toString.strip.should.equal(`[ new Value(1), new Value(2) ]`); } /// Get the previous array identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 4); auto end = scopeResult.end - 13; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[1, 2, 3]`); } /// Get the previous array of instances identifier from a list of tokens unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto scopeResult = getScope(tokens, 90); auto end = scopeResult.end - 16; auto result = getPreviousIdentifier(tokens, end); tokens[result .. end].toString.strip.should.equal(`[ new Value(1), new Value(2) ]`); } size_t getShouldIndex(const(Token)[] tokens, size_t startLine) { auto shouldTokens = tokens .enumerate .filter!(a => a[1].text == "should") .filter!(a => a[1].line <= startLine) .array; if(shouldTokens.length == 0) { return 0; } return shouldTokens[shouldTokens.length - 1].index; } /// Get the index of the should call unittest { const(Token)[] tokens = []; splitMultilinetokens(fileToDTokens("test/values.d"), tokens); auto result = getShouldIndex(tokens, 4); auto token = tokens[result]; token.line.should.equal(3); token.text.should.equal(`should`); str(token.type).text.should.equal(`identifier`); } /// An alternative to SourceResult that uses // DParse to get the source code class SourceResult : IResult { static private { const(Token)[][string] fileTokens; } immutable { string file; size_t line; } private const { Token[] tokens; } this(string fileName = __FILE__, size_t line = __LINE__, size_t range = 6) nothrow @trusted { this.file = fileName; this.line = line; if (!fileName.exists) { return; } try { updateFileTokens(fileName); auto result = getScope(fileTokens[fileName], line); auto begin = getPreviousIdentifier(fileTokens[fileName], result.begin); auto end = getFunctionEnd(fileTokens[fileName], begin) + 1; this.tokens = fileTokens[fileName][begin .. end]; } catch (Throwable t) { } } static void updateFileTokens(string fileName) { if(fileName !in fileTokens) { fileTokens[fileName] = []; splitMultilinetokens(fileToDTokens(fileName), fileTokens[fileName]); } } string getValue() { size_t startIndex = 0; size_t possibleStartIndex = 0; size_t endIndex = 0; size_t lastStartIndex = 0; size_t lastEndIndex = 0; int paranthesisCount = 0; size_t begin; size_t end = getShouldIndex(tokens, line); if(end != 0) { begin = tokens.getPreviousIdentifier(end - 1); return tokens[begin .. end - 1].toString.strip; } auto beginAssert = getAssertIndex(tokens, line); if(beginAssert > 0) { begin = beginAssert + 4; end = getParameter(tokens, begin); return tokens[begin .. end].toString.strip; } return ""; } override string toString() nothrow { auto separator = leftJustify("", 20, '-'); string result = "\n" ~ separator ~ "\n" ~ file ~ ":" ~ line.to!string ~ "\n" ~ separator; if(tokens.length == 0) { return result ~ "\n"; } size_t line = tokens[0].line - 1; size_t column = 1; bool afterErrorLine = false; foreach(token; this.tokens.filter!(token => token != tok!"whitespace")) { string prefix = ""; foreach(lineNumber; line..token.line) { if(lineNumber < this.line -1 || afterErrorLine) { prefix ~= "\n" ~ rightJustify((lineNumber+1).to!string, 6, ' ') ~ ": "; } else { prefix ~= "\n>" ~ rightJustify((lineNumber+1).to!string, 5, ' ') ~ ": "; } } if(token.line != line) { column = 1; } if(token.column > column) { prefix ~= ' '.repeat.take(token.column - column).array; } auto stringRepresentation = token.text == "" ? str(token.type) : token.text; auto lines = stringRepresentation.split("\n"); result ~= prefix ~ lines[0]; line = token.line; column = token.column + stringRepresentation.length; if(token.line >= this.line && str(token.type) == ";") { afterErrorLine = true; } } return result; } void print(ResultPrinter printer) { if(tokens.length == 0) { return; } printer.primary("\n"); printer.info(file ~ ":" ~ line.to!string); size_t line = tokens[0].line - 1; size_t column = 1; bool afterErrorLine = false; foreach(token; this.tokens.filter!(token => token != tok!"whitespace")) { foreach(lineNumber; line..token.line) { printer.primary("\n"); if(lineNumber < this.line -1 || afterErrorLine) { printer.primary(rightJustify((lineNumber+1).to!string, 6, ' ') ~ ":"); } else { printer.dangerReverse(">" ~ rightJustify((lineNumber+1).to!string, 5, ' ') ~ ":"); } } if(token.line != line) { column = 1; } if(token.column > column) { printer.primary(' '.repeat.take(token.column - column).array); } auto stringRepresentation = token.text == "" ? str(token.type) : token.text; if(token.text == "" && str(token.type) != "whitespace") { printer.info(str(token.type)); } else if(str(token.type).indexOf("Literal") != -1) { printer.success(token.text); } else { printer.primary(token.text); } line = token.line; column = token.column + stringRepresentation.length; if(token.line >= this.line && str(token.type) == ";") { afterErrorLine = true; } } printer.primary("\n"); } } @("TestException should read the code from the file") unittest { auto result = new SourceResult("test/values.d", 26); auto msg = result.toString; msg.should.equal("\n--------------------\ntest/values.d:26\n--------------------\n" ~ " 23: unittest {\n" ~ " 24: /++/\n" ~ " 25: \n" ~ "> 26: [1, 2, 3]\n" ~ "> 27: .should\n" ~ "> 28: .contain(4);\n" ~ " 29: }"); } @("TestException should print the lines before multiline tokens") unittest { auto result = new SourceResult("test/values.d", 45); auto msg = result.toString; msg.should.equal("\n--------------------\ntest/values.d:45\n--------------------\n" ~ " 40: unittest {\n" ~ " 41: /*\n" ~ " 42: Multi line comment\n" ~ " 43: */\n" ~ " 44: \n" ~ "> 45: `multi\n" ~ "> 46: line\n" ~ "> 47: string`\n" ~ "> 48: .should\n" ~ "> 49: .contain(`multi\n" ~ "> 50: line\n" ~ "> 51: string`);\n" ~ " 52: }"); } /// Converts a file to D tokens provided by libDParse. /// All the whitespaces are ignored const(Token)[] fileToDTokens(string fileName) nothrow @trusted { try { auto f = File(fileName); immutable auto fileSize = f.size(); ubyte[] fileBytes = new ubyte[](fileSize.to!size_t); if(f.rawRead(fileBytes).length != fileSize) { return []; } StringCache cache = StringCache(StringCache.defaultBucketCount); LexerConfig config; config.stringBehavior = StringBehavior.source; config.fileName = fileName; config.commentBehavior = CommentBehavior.intern; auto lexer = DLexer(fileBytes, config, &cache); const(Token)[] tokens = lexer.array; return tokens.map!(token => const Token(token.type, token.text.idup, token.line, token.column, token.index)).array; } catch(Throwable) { return []; } } @("TestException should ignore missing files") unittest { auto result = new SourceResult("test/missing.txt", 10); auto msg = result.toString; msg.should.equal("\n" ~ `-------------------- test/missing.txt:10 --------------------` ~ "\n"); } @("Source reporter should find the tested value on scope start") unittest { auto result = new SourceResult("test/values.d", 4); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a statment") unittest { auto result = new SourceResult("test/values.d", 12); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a */ comment") unittest { auto result = new SourceResult("test/values.d", 20); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a +/ comment") unittest { auto result = new SourceResult("test/values.d", 28); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value after a // comment") unittest { auto result = new SourceResult("test/values.d", 36); result.getValue.should.equal("[1, 2, 3]"); } @("Source reporter should find the tested value from an assert utility") unittest { auto result = new SourceResult("test/values.d", 55); result.getValue.should.equal("5"); result = new SourceResult("test/values.d", 56); result.getValue.should.equal("(5+1)"); result = new SourceResult("test/values.d", 57); result.getValue.should.equal("(5, (11))"); } @("Source reporter should get the value from multiple should asserts") unittest { auto result = new SourceResult("test/values.d", 61); result.getValue.should.equal("5"); result = new SourceResult("test/values.d", 62); result.getValue.should.equal("(5+1)"); result = new SourceResult("test/values.d", 63); result.getValue.should.equal("(5, (11))"); } @("Source reporter should get the value after a scope") unittest { auto result = new SourceResult("test/values.d", 71); result.getValue.should.equal("found"); } @("Source reporter should get a function call value") unittest { auto result = new SourceResult("test/values.d", 75); result.getValue.should.equal("found(4)"); } @("Source reporter should parse nested lambdas") unittest { auto result = new SourceResult("test/values.d", 81); result.getValue.should.equal("({ ({ }).should.beNull; })"); } /// Source reporter should print the source code unittest { auto result = new SourceResult("test/values.d", 36); auto printer = new MockPrinter(); result.print(printer); auto lines = printer.buffer.split("[primary:\n]"); lines[1].should.equal(`[info:test/values.d:36]`); lines[2].should.equal(`[primary: 31:][info:unittest][primary: ][info:{]`); lines[7].should.equal(`[dangerReverse:> 36:][primary: ][info:.][primary:contain][info:(][success:4][info:)][info:;]`); } /// split multiline tokens in multiple single line tokens with the same type void splitMultilinetokens(const(Token)[] tokens, ref const(Token)[] result) nothrow @trusted { try { foreach(token; tokens) { auto pieces = token.text.idup.split("\n"); if(pieces.length <= 1) { result ~= const Token(token.type, token.text.dup, token.line, token.column, token.index); } else { size_t line = token.line; size_t column = token.column; foreach(textPiece; pieces) { result ~= const Token(token.type, textPiece, line, column, token.index); line++; column = 1; } } } } catch(Throwable) {} } /// A new line sepparator class SeparatorResult : IResult { override string toString() { return "\n"; } void print(ResultPrinter printer) { printer.primary("\n"); } } class ListInfoResult : IResult { private { struct Item { string singular; string plural; string[] valueList; string key() { return valueList.length > 1 ? plural : singular; } MessageResult toMessage(size_t indentation = 0) { auto printableKey = rightJustify(key ~ ":", indentation, ' '); auto result = new MessageResult(printableKey); string glue; foreach(value; valueList) { result.addText(glue); result.addValue(value); glue = ","; } return result; } } Item[] items; } void add(string key, string value) { items ~= Item(key, "", [value]); } void add(string singular, string plural, string[] valueList) { items ~= Item(singular, plural, valueList); } private size_t indentation() { auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return 0; } return elements.map!"a.key".map!"a.length".maxElement + 2; } override string toString() { auto indent = indentation; auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return ""; } return "\n" ~ elements.map!(a => a.toMessage(indent)).map!"a.toString".join("\n"); } void print(ResultPrinter printer) { auto indent = indentation; auto elements = items.filter!"a.valueList.length > 0"; if(elements.empty) { return; } foreach(item; elements) { printer.primary("\n"); item.toMessage(indent).print(printer); } } } /// convert to string the added data to ListInfoResult unittest { auto result = new ListInfoResult(); result.add("a", "1"); result.add("ab", "2"); result.add("abc", "3"); result.toString.should.equal(` a:1 ab:2 abc:3`); } /// print the added data to ListInfoResult unittest { auto printer = new MockPrinter(); auto result = new ListInfoResult(); result.add("a", "1"); result.add("ab", "2"); result.add("abc", "3"); result.print(printer); printer.buffer.should.equal(`[primary: ][primary: a:][primary:][info:1][primary: ][primary: ab:][primary:][info:2][primary: ][primary: abc:][primary:][info:3]`); } /// convert to string the added data lists to ListInfoResult unittest { auto result = new ListInfoResult(); result.add("a", "as", ["1", "2","3"]); result.add("ab", "abs", ["2", "3"]); result.add("abc", "abcs", ["3"]); result.add("abcd", "abcds", []); result.toString.should.equal(` as:1,2,3 abs:2,3 abc:3`); }