trial.discovery.unit 233/235(99%) 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
40463231
410
42120017
430
440
45343214
460
4737895
480
490
50609515
510
523876
530
540
55601451
560
574074
580
590
60297369
610
620
630
640
650
661
670
680
690
700
710
720
730
740
750
760
770
780
790
800
810
820
830
840
851
860
872
882
892
902
912
922
932
940
952
962
972
982
992
1000
1010
1020
1030
1040
1050
1060
1070
1080
1090
11045832
1110
11232328
1130
11436798
1150
1160
1179902
1180
1190
12035930
1210
1221937
1231937
1240
12510618
1268681
1270
12831706
1290
1308681
1319591
1320
13374
1340
1350
1360
13712555
13810618
1390
14019299
1418681
1420
1430
1441937
1450
1460
14733993
1480
1490
1500
1510
1523599
1530
154346
1550
1560
1570
1580
159417328
1600
161208664
1620
16343657
1640
1650
166165007
1670
168143143
1690
1700
17142508
1720
17319991
1740
1750
1761873
1770
1780
1790
1800
1810
1822
1832
1842
1852
1862
1872
1882
1890
1900
1910
1920
193302
1940
195463526
196463224
197254258
1980
199302
200302
2010
202626898
2030
204417328
2050
206163134
2070
2080
2090
21045530
21145530
2120
2130
2140
215302
2160
217302
2180
2190
220302
2210
2220
2230
2240
2250
226111
2270
2280
2290
2300
2310
2322
2332
2342
2352
2362
2372
2380
2390
2400
2410
2420
2430
2440
2450
2460
24754
2480
2490
2500
2510
2527
2530
2540
2550
2560
2570
2580
2597
2600
2617
2620
2630
2640
2650
2667
2677
2680
2697
2707
2710
2727
2730
27440635
2750
27640635
2770
278749
279749
2800
2810
28240635
2830
28442
2850
2860
28740635
2880
289105
2900
29114
2920
2930
294105
2950
2960
29740635
2980
29921
3000
3010
30240635
3030
304140
305140
306140
3070
30898
3090
310238
3110
31214
3130
3140
31598
3160
3177
3180
3190
32098
3210
32221
3230
3240
32598
3260
3270
3287
3290
3300
3310
3320
3330
33498
3350
33698
3370
33898
3390
3400
3410
3420
3437
3440
3450
3460
3470
3480
3490
3500
3510
3520
3530
3540
3550
356259
357259
3580
359110
3600
3610
3620
36392
3640
3650
3660
367259
3680
369432
3700
3710
3720
373173
3740
375173
376173
3770
3780
3790
3800
3810
3820
3830
384259
3850
3860
3870
3880
389432
390432
3910
392864
3930
3940
3950
3960
3970
3980
3990
4000
4010
4020
4030
404259
4050
406259
407259
4080
4090
4100
411259
4120
4130
4140
4150
4160
4170
418259
4190
4200
4210
4220
423259
4240
425110
4260
4270
4280
42918
4300
4310
4320
433259
4340
4350
4360
4370
4380
439301
4400
441259
4420
443259
444189
4450
4460
447259
4480
449259
4500
4510
4520
4530
4540
4550
456301
4570
4580
4590
4601749
4610
4620
4630
4640
4650
466267
4670
468267
4690
4700
4710
4720
4730
4740
4750
4760
4770
4780
4790
4800
4810
4820
4830
4840
4850
4860
4870
4880
4890
4900
4910
4920
4930
4940
4950
4960
4970
4980
4990
5000
5010
5020
5030
5040
5050
5060
5070
5080
5090
5100
5110
5120
5130
5140
5150
5160
5170
5180
5190
5200
5210
5220
5230
5240
5250
5260
5270
5280
5290
5300
5310
5320
5330
5340
5350
5360
5370
5380
5390
5400
5410
5420
5430
5440
5450
5460
5470
5480
5490
5500
5510
5520
5530
5540
5550
5560
5570
5580
5590
5600
5610
5620
5630
5640
5650
5660
5670
5680
5690
5700
5710
5721
5730
5741
5750
5762
5770
5782
5790
5800
5810
5820
5830
5840
5851
5860
5871
5880
5892
5900
5911
5924
5930
5942
5954
5962
5970
5980
5990
6000
6010
6021
6031
6040
6051
6060
6072
6080
6091
6109
6110
6122
6132
6142
6150
6160
6170
6180
6190
6200
6211
6220
6231
6242
6250
6261
6277
6280
6292
6306
6316
6320
6330
6340
6350
6360
6371
6381
6390
6401
6412
6420
6438
6442
6450
6461
6470
6486
6496
6500
6510
6520
6530
6540
6551
6561
6570
6581
6592
6600
6616
6622
6630
6641
6650
6664
6674
6680
6690
6700
6710
6720
6731
6741
6750
6761
6772
6780
6791
68010
6812
6820
6832
6840
6850
6860
6870
6880
6890
6901
6911
6920
6931
6942
6950
69612
6972
6980
6991
7000
7012
7022
7032
7040
7050
7060
7070
7080
7091
7100
7111
7122
7130
71415
7152
7160
7170
7180
7190
7200
7211
7221
7230
7240
72515
7261
7270
7280
7290
7300
7310
7320
7330
7340
7350
7361
7370
7381
7390
7401
74188
7420
74359
7440
7450
74614
7470
7481
7490
7500
7510
7520
7530
7540
7550
7560
7570
75816
75914
7600
/++ A module containing the default test discovery logic Copyright: © 2017 Szabo Bogdan License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. Authors: Szabo Bogdan +/ module trial.discovery.unit; import std.stdio; import std.string; import std.traits; import std.conv; import std.array; import std.file; import std.algorithm; import std.range; import std.typecons; import trial.interfaces; import trial.discovery.code; static if(__VERSION__ >= 2077) { enum unitTestKey = "__un" ~ "ittest_"; } else { enum unitTestKey = "__un" ~ "ittestL"; } enum CommentType { none, begin, end, comment } CommentType commentType(T)(T line) { if (line.length < 2) { return CommentType.none; } if (line[0 .. 2] == "//") { return CommentType.comment; } if (line[0 .. 2] == "/+" || line[0 .. 2] == "/*") { return CommentType.begin; } if (line.indexOf("+/") != -1 || line.indexOf("*/") != -1) { return CommentType.end; } return CommentType.none; } @("It should group comments") unittest { string comments = "//line 1 // line 2 //// other line /** line 3 line 4 ****/ //// other line /++ line 5 line 6 +++/ /** line 7 * * line 8 */"; auto results = comments.compressComments; results.length.should.equal(6); results[0].value.should.equal("line 1 line 2"); results[1].value.should.equal("other line"); results[2].value.should.equal("line 3 line 4"); results[3].value.should.equal("other line"); results[4].value.should.equal("line 5 line 6"); results[5].value.should.equal("line 7 line 8"); results[0].line.should.equal(2); results[1].line.should.equal(4); results[2].line.should.equal(7); results[3].line.should.equal(9); results[4].line.should.equal(13); } struct Comment { ulong line; string value; } Comment[] commentGroupToString(T)(T[] group) { if (group.front[1] == CommentType.comment) { auto slice = group.until!(a => a[1] != CommentType.comment).array; string value = slice.map!(a => a[2].stripLeft('/').array.to!string).map!(a => a.strip) .join(' ').array.to!string; return [Comment(slice[slice.length - 1][0], value)]; } if (group.front[1] == CommentType.begin) { auto ch = group.front[2][1]; auto index = 0; auto newGroup = group.map!(a => Tuple!(int, CommentType, immutable(char), string)(a[0], a[1], a[2].length > 2 ? a[2][1] : ' ', a[2])).array; foreach (item; newGroup) { index++; if (item[1] == CommentType.end && item[2] == ch) { break; } } auto slice = group.map!(a => Tuple!(int, CommentType, immutable(char), string)(a[0], a[1], a[2].length > 2 ? a[2][1] : ' ', a[2])).take(index); string value = slice.map!(a => a[3].strip).map!(a => a.stripLeft('/') .stripLeft(ch).array.to!string).map!(a => a.strip).join(' ') .until(ch ~ "/").array.stripRight('/').stripRight(ch).strip.to!string; return [Comment(slice[slice.length - 1][0], value)]; } return []; } string getComment(const Comment[] comments, const ulong line, const string defaultValue) pure { auto r = comments.filter!(a => (line - a.line) < 3); return r.empty ? defaultValue : r.front.value; } bool connects(T)(T a, T b) { auto items = a[0] < b[0] ? [a, b] : [b, a]; if (items[1][0] - items[0][0] != 1) { return false; } if (a[1] == b[1]) { return true; } if (items[0][1] != CommentType.end && items[1][1] != CommentType.begin) { return true; } return false; } @("check comment types") unittest { "".commentType.should.equal(CommentType.none); "some".commentType.should.equal(CommentType.none); "//some".commentType.should.equal(CommentType.comment); "/+some".commentType.should.equal(CommentType.begin); "/*some".commentType.should.equal(CommentType.begin); "some+/some".commentType.should.equal(CommentType.end); "some*/some".commentType.should.equal(CommentType.end); } auto compressComments(string code) { Comment[] result; auto lines = code.splitter("\n").map!(a => a.strip).enumerate(1) .map!(a => Tuple!(int, CommentType, string)(a[0], a[1].commentType, a[1])).filter!( a => a[2] != "").array; auto tmp = [lines[0]]; auto prev = lines[0]; foreach (line; lines[1 .. $]) { if (tmp.length == 0 || line.connects(tmp[tmp.length - 1])) { tmp ~= line; } else { result ~= tmp.commentGroupToString; tmp = [line]; } } if (tmp.length > 0) { result ~= tmp.commentGroupToString; } return result; } /// Remove comment tokens string clearCommentTokens(string text) { return text.strip('/').strip('+').strip('*').strip; } /// clearCommentTokens should remove comment tokens unittest { clearCommentTokens("// text").should.equal("text"); clearCommentTokens("///// text").should.equal("text"); clearCommentTokens("/+++ text").should.equal("text"); clearCommentTokens("/*** text").should.equal("text"); clearCommentTokens("/*** text ***/").should.equal("text"); clearCommentTokens("/+++ text +++/").should.equal("text"); } /// The default test discovery looks for unit test sections and groups them by module class UnitTestDiscovery : ITestDiscovery { TestCase[string][string] testCases; TestCase[] getTestCases() { return testCases.values.map!(a => a.values).joiner.array; } TestCase[] discoverTestCases(string file) { TestCase[] testCases = []; version (Have_fluent_asserts_core) version (Have_libdparse) { import fluentasserts.core.results; auto tokens = fileToDTokens(file); void noTest() { assert(false, "you can not run this test"); } auto iterator = TokenIterator(tokens); auto moduleName = iterator.skipUntilType("module").skipOne.readUntilType(";").strip; string lastName; DLangAttribute[] attributes; foreach (token; iterator) { auto type = str(token.type); if (type == "}") { lastName = ""; attributes = []; } if (type == "@") { attributes ~= iterator.readAttribute; } if (type == "comment") { if (lastName != "") { lastName ~= " "; } lastName ~= token.text.clearCommentTokens; } if (type == "version") { iterator.skipUntilType(")"); } if (type == "unittest") { auto issues = attributes.filter!(a => a.identifier == "Issue"); auto flakynes = attributes.filter!(a => a.identifier == "Flaky"); auto stringAttributes = attributes.filter!(a => a.identifier == ""); Label[] labels = []; foreach (issue; issues) { labels ~= Label("issue", issue.value); } if (!flakynes.empty) { labels ~= Label("status_details", "flaky"); } if (!stringAttributes.empty) { lastName = stringAttributes.front.value.strip; } if (lastName == "") { static if(__VERSION__ >= 2077) { lastName = moduleName.replace(".", "_") ~ "_d_" ~ token.line.to!string; } else { lastName = unitTestKey ~ token.line.to!string; } } auto testCase = TestCase(moduleName, lastName, &noTest, labels); testCase.location = SourceLocation(file, token.line); testCases ~= testCase; } } } return testCases; } void addModule(string file, string moduleName)() { mixin("import " ~ moduleName ~ ";"); mixin("discover!(`" ~ file ~ "`, `" ~ moduleName ~ "`, " ~ moduleName ~ ")();"); } private { string testName(alias test)(ref Comment[] comments) { string defaultName = test.stringof.to!string; string name = defaultName; foreach (attr; __traits(getAttributes, test)) { static if (is(typeof(attr) == string)) { name = attr; } } enum len = unitTestKey.length; if (name == defaultName && name.indexOf(unitTestKey) == 0) { try { auto line = extractLine(name); if(line != 0) { name = comments.getComment(line, defaultName); } } catch (Exception e) { } } return name; } size_t extractLine(string name) { static if(__VERSION__ >= 2077) { auto idx = name.indexOf("_d_") + 3; auto lastIdx = name.lastIndexOf("_"); return idx != -1 ? name[idx .. lastIdx].to!size_t : 0; } else { enum len = unitTestKey.length; auto postFix = name[len .. $]; auto idx = postFix.indexOf("_"); return idx != -1 ? postFix[0 .. idx].to!size_t : 0; } } SourceLocation testSourceLocation(alias test)(string fileName) { string name = test.stringof.to!string; enum len = unitTestKey.length; size_t line; try { line = extractLine(name); } catch (Exception e) { return SourceLocation(); } return SourceLocation(fileName, line); } Label[] testLabels(alias test)() { Label[] labels; foreach (attr; __traits(getAttributes, test)) { static if (__traits(hasMember, attr, "labels")) { labels ~= attr.labels; } } return labels; } auto addTestCases(string file, alias moduleName, composite...)() if (composite.length == 1 && isUnitTestContainer!(composite)) { Comment[] comments = file.readText.compressComments; foreach (test; __traits(getUnitTests, composite)) { auto testCase = TestCase(moduleName, testName!(test)(comments), { test(); }, testLabels!(test)); testCase.location = testSourceLocation!test(file); testCases[moduleName][test.mangleof] = testCase; } } void discover(string file, alias moduleName, composite...)() if (composite.length == 1 && isUnitTestContainer!(composite)) { addTestCases!(file, moduleName, composite); static if (isUnitTestContainer!composite) { foreach (member; __traits(allMembers, composite)) { static if (__traits(compiles, __traits(getMember, composite, member)) && isSingleField!(__traits(getMember, composite, member)) && isUnitTestContainer!(__traits(getMember, composite, member)) && !isModule!(__traits(getMember, composite, member))) { if (__traits(getMember, composite, member).mangleof !in testCases) { discover!(file, moduleName, __traits(getMember, composite, member))(); } } } } } } } private template isUnitTestContainer(DECL...) if (DECL.length == 1) { static if (!isAccessible!DECL) { enum isUnitTestContainer = false; } else static if (is(FunctionTypeOf!(DECL[0]))) { enum isUnitTestContainer = false; } else static if (is(DECL[0]) && !isAggregateType!(DECL[0])) { enum isUnitTestContainer = false; } else static if (isPackage!(DECL[0])) { enum isUnitTestContainer = true; } else static if (isModule!(DECL[0])) { enum isUnitTestContainer = DECL[0].stringof != "module object"; } else static if (!__traits(compiles, fullyQualifiedName!(DECL[0]))) { enum isUnitTestContainer = false; } else static if (!is(typeof(__traits(allMembers, DECL[0])))) { enum isUnitTestContainer = false; } else { enum isUnitTestContainer = true; } } private template isModule(DECL...) if (DECL.length == 1) { static if (is(DECL[0])) enum isModule = false; else static if (is(typeof(DECL[0])) && !is(typeof(DECL[0]) == void)) enum isModule = false; else static if (!is(typeof(DECL[0].stringof))) enum isModule = false; else static if (is(FunctionTypeOf!(DECL[0]))) enum isModule = false; else enum isModule = DECL[0].stringof.startsWith("module "); } private template isPackage(DECL...) if (DECL.length == 1) { static if (is(DECL[0])) enum isPackage = false; else static if (is(typeof(DECL[0])) && !is(typeof(DECL[0]) == void)) enum isPackage = false; else static if (!is(typeof(DECL[0].stringof))) enum isPackage = false; else static if (is(FunctionTypeOf!(DECL[0]))) enum isPackage = false; else enum isPackage = DECL[0].stringof.startsWith("package "); } private template isAccessible(DECL...) if (DECL.length == 1) { enum isAccessible = __traits(compiles, testTempl!(DECL[0])()); } private template isSingleField(DECL...) { enum isSingleField = DECL.length == 1; } private void testTempl(X...)() if (X.length == 1) { static if (is(X[0])) { auto x = X[0].init; } else { auto x = X[0].stringof; } } /// This adds asserts to the module version (unittest) { import fluent.asserts; } /// It should find this test unittest { auto testDiscovery = new UnitTestDiscovery; testDiscovery.addModule!(__FILE__, "trial.discovery.unit"); testDiscovery.testCases.keys.should.contain("trial.discovery.unit"); testDiscovery.testCases["trial.discovery.unit"].values.map!"a.name".should.contain( "It should find this test"); } /// It should find this flaky test @Flaky unittest { auto testDiscovery = new UnitTestDiscovery; testDiscovery.addModule!(__FILE__, "trial.discovery.unit"); testDiscovery.testCases.keys.should.contain("trial.discovery.unit"); auto r = testDiscovery.testCases["trial.discovery.unit"].values.filter!( a => a.name == "It should find this flaky test"); r.empty.should.equal(false).because("a flaky test is in this module"); r.front.labels.map!(a => a.name).should.equal(["status_details"]); r.front.labels[0].value.should.equal("flaky"); } /// It should find the line of this test unittest { enum line = __LINE__ - 2; auto testDiscovery = new UnitTestDiscovery; testDiscovery.addModule!(__FILE__, "trial.discovery.unit"); testDiscovery.testCases.keys.should.contain("trial.discovery.unit"); auto r = testDiscovery.testCases["trial.discovery.unit"].values.filter!( a => a.name == "It should find the line of this test"); r.empty.should.equal(false).because("the location should be present"); r.front.location.fileName.should.endWith("unit.d"); r.front.location.line.should.equal(line); } /// It should find this test with issues attributes @Issue("1") @Issue("2") unittest { auto testDiscovery = new UnitTestDiscovery; testDiscovery.addModule!(__FILE__, "trial.discovery.unit"); testDiscovery.testCases.keys.should.contain("trial.discovery.unit"); auto r = testDiscovery.testCases["trial.discovery.unit"].values.filter!( a => a.name == "It should find this test with issues attributes"); r.empty.should.equal(false).because("an issue test is in this module"); r.front.labels.map!(a => a.name).should.equal(["issue", "issue"]); r.front.labels.map!(a => a.value).should.equal(["1", "2"]); } /// The discoverTestCases should find the test with issues attributes unittest { immutable line = __LINE__ - 2; auto testDiscovery = new UnitTestDiscovery; auto tests = testDiscovery.discoverTestCases(__FILE__); tests.length.should.be.greaterThan(0); auto testFilter = tests.filter!(a => a.name == "It should find this test with issues attributes"); testFilter.empty.should.equal(false); auto theTest = testFilter.front; theTest.labels.map!(a => a.name).should.equal(["issue", "issue"]); theTest.labels.map!(a => a.value).should.equal(["1", "2"]); } /// The discoverTestCases should find the test with the flaky attribute unittest { immutable line = __LINE__ - 2; auto testDiscovery = new UnitTestDiscovery; auto tests = testDiscovery.discoverTestCases(__FILE__); tests.length.should.be.greaterThan(0); auto testFilter = tests.filter!(a => a.name == "It should find this flaky test"); testFilter.empty.should.equal(false); auto theTest = testFilter.front; theTest.labels.map!(a => a.name).should.equal(["status_details"]); theTest.labels.map!(a => a.value).should.equal(["flaky"]); } @("", "The discoverTestCases should find the test with the string attribute name") unittest { immutable line = __LINE__ - 2; auto testDiscovery = new UnitTestDiscovery; auto tests = testDiscovery.discoverTestCases(__FILE__); tests.length.should.be.greaterThan(0); auto testFilter = tests.filter!( a => a.name == "The discoverTestCases should find the test with the string attribute name"); testFilter.empty.should.equal(false); testFilter.front.labels.length.should.equal(0); } /// The discoverTestCases /// should find this test unittest { immutable line = __LINE__ - 2; auto testDiscovery = new UnitTestDiscovery; auto tests = testDiscovery.discoverTestCases(__FILE__); tests.length.should.be.greaterThan(0); auto testFilter = tests.filter!(a => a.name == "The discoverTestCases should find this test"); testFilter.empty.should.equal(false); auto thisTest = testFilter.front; thisTest.suiteName.should.equal("trial.discovery.unit"); thisTest.location.fileName.should.equal(__FILE__); thisTest.location.line.should.equal(line); } /// discoverTestCases should ignore version(unittest) unittest { auto testDiscovery = new UnitTestDiscovery; auto tests = testDiscovery.discoverTestCases(__FILE__); tests.length.should.be.greaterThan(0); auto testFilter = tests.filter!(a => a.name == "This adds asserts to the module"); testFilter.empty.should.equal(true); } unittest { /// discoverTestCases should set the default test names immutable line = __LINE__ - 3; auto testDiscovery = new UnitTestDiscovery; static if(__VERSION__ >= 2077) { testDiscovery.discoverTestCases(__FILE__).map!(a => a.name) .array.should.contain(__MODULE__.replace(".", "_") ~ "_d_" ~ line.to!string); } else { testDiscovery.discoverTestCases(__FILE__).map!(a => a.name) .array.should.contain(unitTestKey ~ line.to!string); } } /// discoverTestCases should find the same tests like testCases unittest { auto testDiscovery = new UnitTestDiscovery; testDiscovery.addModule!(__FILE__, "trial.discovery.unit"); auto allTests = testDiscovery.getTestCases.sort!((a, b) => a.location.line < b.location.line).array; foreach (index, test; allTests) { static if(__VERSION__ >= 2077) { if (test.name.indexOf(__MODULE__.replace(".", "_") ~ "_d_718") != -1) { allTests[index].name = __MODULE__.replace(".", "_") ~ "_d_718"; } } else { if (test.name.indexOf(unitTestKey ~ "718") != -1) { allTests[index].name = unitTestKey ~ "718"; } } } testDiscovery.discoverTestCases(__FILE__).map!(a => a.toString).join("\n") .should.equal(allTests.map!(a => a.toString).join("\n")); }