trial.reporters.allure 222/229(96%) 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
25329
260
27329
28329
29329
30329
31329
320
33329
340
350
360
370
380
390
400
410
420
430
441
451
460
470
480
491
500
510
520
530
540
550
560
570
581
591
600
610
62129
6342
6442
650
6642
670
680
690
700
710
720
730
740
750
760
770
780
790
800
810
820
830
840
8545
8645
8745
8845
890
900
910
920
9345
94267
950
9645
9743
980
990
10045
1010
1020
1030
1040
1050
1060
1070
1080
10945
1101
1113
1121
1130
1140
11545
1160
1170
1180
1190
1200
12145
1220
1230
1240
1250
1260
1270
1280
1290
1300
1310
1321
1331
1341
1350
1361
1371
1381
1391
1400
1411
1420
1431
1440
1451
1460
1472
1480
1490
1500
1510
1520
1530
1540
1550
1560
1570
1580
1590
1600
1610
1620
1630
1640
1650
1661
1671
1681
1691
1700
1711
1720
1732
1740
1750
1760
1770
1780
1790
1800
1810
1820
1830
1840
1850
1860
1870
1880
1890
1901
1911
1920
1931
1940
1950
1961
1971
1980
1990
2001
2010
2021
2031
2041
2051
2060
2071
2080
2092
2100
2110
2120
2130
2140
2150
2160
2170
2180
2190
2200
2210
2220
2230
2240
2250
2260
2270
2280
2290
2300
2310
2320
2330
2340
2350
2360
237227
238227
239227
240227
2410
2420
2430
2440
245227
2460
2470
2480
2491
2501
2510
2520
2530
2540
255226
256226
2570
2580
2590
2600
2610
2620
2630
2640
265227
266227
267227
2680
269227
270227
2710
272227
2733
2740
27521
2764
2770
2780
2793
2800
2810
282227
2831
2840
2850
2860
2870
2880
289227
2902
29112
2922
2930
2940
295227
2964
29712
2984
2990
3000
301227
3020
303227
3040
3050
3060
3070
3080
3090
3101
3110
3121
3131
3141
3151
3160
3171
3180
3192
3200
3210
3220
3230
3240
3250
3260
3270
3280
3290
3301
3311
3321
3331
3341
3351
3361
3370
3381
3391
3400
3411
3422
3430
3440
3450
3460
3470
3480
3490
3500
3510
3520
3530
3540
3551
3560
3571
3581
3591
3601
3610
3621
3631
3641
3651
3660
3671
3680
3691
3700
3712
3720
3730
3740
3750
3760
3770
3780
3790
3800
3810
3820
3830
3840
3850
3860
3870
3881
3890
3901
3911
3921
3931
3941
3950
3961
3970
3982
3990
4000
4010
4020
4030
4040
4050
4060
4070
4080
4090
4101
4111
4120
4131
4140
4150
4161
4171
4180
4190
4201
4210
4220
4231
4240
4251
4261
4271
4281
4291
4300
4311
4320
4332
4340
4350
4360
4370
4380
4390
4400
4410
4420
4430
4440
4450
4460
4470
4480
4490
4500
45110
45210
45310
45410
45510
4560
4570
4580
4590
46010
46110
46210
4630
4640
46510
4661
4675
4681
4690
4700
47110
4721
4733
4741
4750
4760
47710
4780
47910
4800
4810
4820
4830
4840
4850
4861
4871
4881
4891
4901
4910
4921
4930
4942
4950
4960
4970
4980
4990
5000
5010
5020
5031
5041
5051
5061
5071
5080
5091
5101
5111
5121
5130
5141
5150
5161
5170
5182
5190
5200
5210
5220
5230
5240
5250
5260
5270
5280
5290
5300
5310
5320
5330
5340
5351
5360
5371
5380
5391
5400
5411
5420
5430
5441
5450
5460
5470
5481
5491
5501
5511
5521
5530
5541
5550
5561
5570
5582
5590
5600
5610
5620
5630
5640
5650
5660
5670
5680
5690
5700
5710
5720
5730
5740
5750
5760
5770
5780
5790
5800
5810
5820
5838
5848
5850
5868
5876
5880
5890
5908
5910
5920
5939
5949
5959
5960
5978
5985
5990
6000
6018
6020
6030
6040
6050
6066
6070
6080
6090
6100
6110
6120
6130
6141
6151
6160
6171
6181
6190
6200
6211
6220
6230
6241
6250
6262
6270
6280
6290
6300
6311
6321
6330
6341
6350
6361
6371
6381
6391
6400
6410
6421
6430
6440
6451
6460
6472
6480
/++ A module containing the AllureReporter 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.reporters.allure; import std.stdio; import std.array; import std.conv; import std.datetime; import std.string; import std.algorithm; import std.file; import std.path; import std.uuid; import std.range; import trial.interfaces; import trial.reporters.writer; private string escape(string data) { string escapedData = data.dup; escapedData = escapedData.replace(`&`, `&`); escapedData = escapedData.replace(`"`, `"`); escapedData = escapedData.replace(`'`, `'`); escapedData = escapedData.replace(`<`, `<`); escapedData = escapedData.replace(`>`, `>`); return escapedData; } /// The Allure reporter creates a xml containing the test results, the steps /// and the attachments. http://allure.qatools.ru/ class AllureReporter : ILifecycleListener { private { immutable string destination; } this(string destination) { this.destination = destination; } void begin(ulong testCount) { if(exists(destination)) { std.file.rmdirRecurse(destination); } } void update() {} void end(SuiteResult[] result) { if(!exists(destination)) { destination.mkdirRecurse; } foreach(item; result) { string uuid = randomUUID.toString; string xml = AllureSuiteXml(destination, item, uuid).toString; std.file.write(buildPath(destination, uuid ~ "-testsuite.xml"), xml); } } } struct AllureSuiteXml { /// The suite result SuiteResult result; /// The suite id string uuid; /// The allure version const string allureVersion = "1.5.2"; private { immutable string destination; } this(const string destination, SuiteResult result, string uuid) { this.destination = destination; this.result = result; this.uuid = uuid; } /// Converts the suiteResult to a xml string string toString() { auto epoch = SysTime.fromUnixTime(0); string tests = result.tests.map!(a => AllureTestXml(destination, a, uuid).toString).array.join("\n"); if(tests != "") { tests = "\n" ~ tests; } auto xml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <ns2:test-suite start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" version="` ~ this.allureVersion ~ `" xmlns:ns2="urn:model.allure.qatools.yandex.ru"> <name>` ~ result.name.escape ~ `</name> <title>` ~ result.name.escape ~ `</title> <test-cases>` ~ tests ~ ` </test-cases> `; if(result.attachments.length > 0) { xml ~= " <attachments>\n"; xml ~= result.attachments.map!(a => AllureAttachmentXml(destination, a, 6, uuid)).map!(a => a.toString).array.join('\n') ~ "\n"; xml ~= " </attachments>\n"; } xml ~= ` <labels> <label name="framework" value="Trial"/> <label name="language" value="D"/> </labels> </ns2:test-suite>`; return xml; } } version(unittest) { import fluent.asserts; } @("AllureSuiteXml should transform an empty suite") unittest { auto epoch = SysTime.fromUnixTime(0); auto result = SuiteResult("Test Suite"); result.begin = Clock.currTime; TestResult test = new TestResult("Test"); test.begin = Clock.currTime; test.end = Clock.currTime; test.status = TestResult.Status.success; result.end = Clock.currTime; result.tests = [ test ]; auto allure = AllureSuiteXml("allure", result, ""); allure.toString.should.equal(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <ns2:test-suite start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" version="1.5.2" xmlns:ns2="urn:model.allure.qatools.yandex.ru"> <name>` ~ result.name ~ `</name> <title>` ~ result.name ~ `</title> <test-cases> <test-case start="` ~ (test.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>Test</name> </test-case> </test-cases> <labels> <label name="framework" value="Trial"/> <label name="language" value="D"/> </labels> </ns2:test-suite>`); } @("AllureSuiteXml should transform a suite with a success test") unittest { auto epoch = SysTime.fromUnixTime(0); auto result = SuiteResult("Test Suite"); result.begin = Clock.currTime; result.end = Clock.currTime; auto allure = AllureSuiteXml("allure", result, ""); allure.toString.should.equal(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <ns2:test-suite start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" version="1.5.2" xmlns:ns2="urn:model.allure.qatools.yandex.ru"> <name>` ~ result.name ~ `</name> <title>` ~ result.name ~ `</title> <test-cases> </test-cases> <labels> <label name="framework" value="Trial"/> <label name="language" value="D"/> </labels> </ns2:test-suite>`); } /// AllureSuiteXml should add the attachments unittest { string resource = buildPath(getcwd(), "some_text.txt"); std.file.write(resource, ""); auto uuid = randomUUID.toString; scope(exit) { remove(resource); remove("allure/" ~ uuid ~ "/title.0.some_text.txt"); } auto epoch = SysTime.fromUnixTime(0); auto result = SuiteResult("Test Suite"); result.begin = Clock.currTime; result.end = Clock.currTime; result.attachments = [ Attachment("title", resource, "plain/text") ]; auto allure = AllureSuiteXml("allure", result, uuid); allure.toString.should.equal( `<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <ns2:test-suite start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" version="1.5.2" xmlns:ns2="urn:model.allure.qatools.yandex.ru"> <name>` ~ result.name ~ `</name> <title>` ~ result.name ~ `</title> <test-cases> </test-cases> <attachments> <attachment title="title" source="` ~ uuid ~ `/title.0.some_text.txt" type="plain/text" /> </attachments> <labels> <label name="framework" value="Trial"/> <label name="language" value="D"/> </labels> </ns2:test-suite>`); } struct AllureTestXml { /// TestResult result; /// string uuid; private { immutable string destination; } this(const string destination, TestResult result, string uuid) { this.destination = destination; this.result = result; this.uuid = uuid; } /// Converts a test result to allure status string allureStatus() { switch(result.status) { case TestResult.Status.created: return "canceled"; case TestResult.Status.failure: return "failed"; case TestResult.Status.skip: return "canceled"; case TestResult.Status.success: return "passed"; default: return "unknown"; } } /// Return the string representation of the test string toString() { auto epoch = SysTime.fromUnixTime(0); auto start = (result.begin - epoch).total!"msecs"; auto stop = (result.end - epoch).total!"msecs"; string xml = ` <test-case start="` ~ start.to!string ~ `" stop="` ~ stop.to!string ~ `" status="` ~ allureStatus ~ `">` ~ "\n"; xml ~= ` <name>` ~ result.name.escape ~ `</name>` ~ "\n"; if(result.labels.length > 0) { xml ~= " <labels>\n"; foreach(label; result.labels) { xml ~= " <label name=\"" ~ label.name ~ "\" value=\"" ~ label.value ~ "\"/>\n"; } xml ~= " </labels>\n"; } if(result.throwable !is null) { xml ~= ` <failure> <message>` ~ result.throwable.msg.escape ~ `</message> <stack-trace>` ~ result.throwable.to!string.escape ~ `</stack-trace> </failure>` ~ "\n"; } if(result.steps.length > 0) { xml ~= " <steps>\n"; xml ~= result.steps.map!(a => AllureStepXml(destination, a, 14, uuid)).map!(a => a.toString).array.join('\n') ~ "\n"; xml ~= " </steps>\n"; } if(result.attachments.length > 0) { xml ~= " <attachments>\n"; xml ~= result.attachments.map!(a => AllureAttachmentXml(destination, a, 14, uuid)).map!(a => a.toString).array.join('\n') ~ "\n"; xml ~= " </attachments>\n"; } xml ~= ` </test-case>`; return xml; } } @("AllureTestXml should transform a success test") unittest { auto epoch = SysTime.fromUnixTime(0); TestResult result = new TestResult("Test"); result.begin = Clock.currTime; result.end = Clock.currTime; result.status = TestResult.Status.success; auto allure = AllureTestXml("allure", result, ""); allure.toString.should.equal( ` <test-case start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>Test</name> </test-case>`); } @("AllureTestXml should transform a failing test") unittest { import trial.step; Step("prepare the test data"); auto epoch = SysTime.fromUnixTime(0); TestResult result = new TestResult("Test"); result.begin = Clock.currTime; result.end = Clock.currTime; result.status = TestResult.Status.failure; result.throwable = new Exception("message"); Step("create the report listener"); auto allure = AllureTestXml("allure", result, ""); Step("perform checks"); allure.toString.should.equal( ` <test-case start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="failed"> <name>Test</name> <failure> <message>message</message> <stack-trace>object.Exception@lifecycle/trial/reporters/allure.d(` ~ result.throwable.line.to!string ~ `): message</stack-trace> </failure> </test-case>`); } /// AllureTestXml should transform a test with steps unittest { auto epoch = SysTime.fromUnixTime(0); TestResult result = new TestResult("Test"); result.begin = Clock.currTime; result.end = Clock.currTime; result.status = TestResult.Status.success; StepResult step = new StepResult(); step.name = "some step"; step.begin = result.begin; step.end = result.end; result.steps = [step, step]; auto allure = AllureTestXml("allure", result, ""); allure.toString.should.equal( ` <test-case start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>Test</name> <steps> <step start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>some step</name> </step> <step start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>some step</name> </step> </steps> </test-case>`); } /// AllureTestXml should transform a test with labels unittest { auto epoch = SysTime.fromUnixTime(0); TestResult result = new TestResult("Test"); result.begin = Clock.currTime; result.end = Clock.currTime; result.status = TestResult.Status.success; result.labels ~= Label("status_details", "flaky"); auto allure = AllureTestXml("allure", result, ""); allure.toString.should.equal( ` <test-case start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>Test</name> <labels> <label name="status_details" value="flaky"/> </labels> </test-case>`); } /// AllureTestXml should add the attachments unittest { string resource = buildPath(getcwd(), "some_text.txt"); std.file.write(resource, ""); auto uuid = randomUUID.toString; scope(exit) { if(exists(resource)) { remove(resource); } remove("allure/" ~ uuid ~ "/title.0.some_text.txt"); } auto epoch = SysTime.fromUnixTime(0); TestResult result = new TestResult("Test"); result.begin = Clock.currTime; result.end = Clock.currTime; result.status = TestResult.Status.success; result.attachments = [ Attachment("title", resource, "plain/text") ]; auto allure = AllureTestXml("allure", result, uuid); allure.toString.should.equal( ` <test-case start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>Test</name> <attachments> <attachment title="title" source="` ~ uuid ~ `/title.0.some_text.txt" type="plain/text" /> </attachments> </test-case>`); } struct AllureStepXml { private { StepResult step; size_t indent; string uuid; immutable string destination; } this(const string destination, StepResult step, size_t indent, string uuid) { this.step = step; this.indent = indent; this.uuid = uuid; this.destination = destination; } /// Return the string representation of the step string toString() { auto epoch = SysTime.fromUnixTime(0); const spaces = " " ~ (" ".repeat(indent).array.join()); string result = spaces ~ `<step start="` ~ (step.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (step.end - epoch).total!"msecs".to!string ~ `" status="passed">` ~ "\n" ~ spaces ~ ` <name>` ~ step.name.escape ~ `</name>` ~ "\n"; if(step.steps.length > 0) { result ~= spaces ~ " <steps>\n"; result ~= step.steps.map!(a => AllureStepXml(destination, a, indent + 6, uuid)).map!(a => a.to!string).array.join('\n') ~ "\n"; result ~= spaces ~ " </steps>\n"; } if(step.attachments.length > 0) { result ~= spaces ~ " <attachments>\n"; result ~= step.attachments.map!(a => AllureAttachmentXml(destination, a, indent + 6, uuid)).map!(a => a.to!string).array.join('\n') ~ "\n"; result ~= spaces ~ " </attachments>\n"; } result ~= spaces ~ `</step>`; return result; } } /// AllureStepXml should transform a step unittest { auto epoch = SysTime.fromUnixTime(0); StepResult result = new StepResult(); result.name = "step"; result.begin = Clock.currTime; result.end = Clock.currTime; auto allure = AllureStepXml("allure", result, 0, ""); allure.toString.should.equal( ` <step start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>step</name> </step>`); } /// AllureStepXml should transform nested steps unittest { auto epoch = SysTime.fromUnixTime(0); StepResult result = new StepResult(); result.name = "step"; result.begin = Clock.currTime; result.end = Clock.currTime; StepResult step = new StepResult(); step.name = "some step"; step.begin = result.begin; step.end = result.end; result.steps = [ step, step ]; auto allure = AllureStepXml("allure", result, 0, ""); allure.toString.should.equal( ` <step start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>step</name> <steps> <step start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>some step</name> </step> <step start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>some step</name> </step> </steps> </step>`); } /// AllureStepXml should add the attachments unittest { string resource = buildPath(getcwd(), "some_image.png"); scope(exit) { resource.remove(); } std.file.write(resource, ""); auto uuid = randomUUID.toString; scope(exit) { rmdirRecurse("allure"); } auto epoch = SysTime.fromUnixTime(0); StepResult result = new StepResult(); result.name = "step"; result.begin = Clock.currTime; result.end = Clock.currTime; result.attachments = [ Attachment("name", resource, "image/png") ]; auto allure = AllureStepXml("allure", result, 0, uuid); allure.toString.should.equal( ` <step start="` ~ (result.begin - epoch).total!"msecs".to!string ~ `" stop="` ~ (result.end - epoch).total!"msecs".to!string ~ `" status="passed"> <name>step</name> <attachments> <attachment title="name" source="` ~ uuid ~ `/name.0.some_image.png" type="image/png" /> </attachments> </step>`); } /// Allure representation of an atachment. /// It will copy the file to the allure folder with an unique name struct AllureAttachmentXml { private { const { Attachment attachment; size_t indent; } string allureFile; } @disable this(); /// Init the struct and copy the atachment to the allure folder this(const string destination, Attachment attachment, size_t indent, string uuid) { this.indent = indent; if(!exists(buildPath(destination, uuid))) { buildPath(destination, uuid).mkdirRecurse; } ulong index; do { allureFile = buildPath(uuid, attachment.name ~ "." ~ index.to!string ~ "." ~ baseName(attachment.file)); index++; } while(buildPath(destination, allureFile).exists); if(attachment.file.exists) { std.file.copy(attachment.file, buildPath(destination, allureFile)); } this.attachment = Attachment(attachment.name, buildPath(destination, allureFile), attachment.mime); } /// convert the attachment to string string toString() { return (" ".repeat(indent).array.join()) ~ "<attachment title=\"" ~ attachment.name ~ "\" source=\"" ~ allureFile ~ "\" type=\"" ~ attachment.mime ~ "\" />"; } } /// Allure attachments should be copied to a folder containing the suite name unittest { string resource = buildPath(getcwd(), "some_image.png"); std.file.write(resource, ""); auto uuid = randomUUID.toString; auto expectedPath = buildPath(getcwd(), "allure", uuid, "name.0.some_image.png"); scope(exit) { rmdirRecurse("allure"); } auto a = AllureAttachmentXml("allure", Attachment("name", resource, ""), 0, uuid); expectedPath.exists.should.equal(true); } /// Allure attachments should avoid name collisions unittest { string resource = buildPath(getcwd(), "some_image.png"); std.file.write(resource, ""); auto uuid = randomUUID.toString; buildPath(getcwd(), "allure", uuid).mkdirRecurse; auto expectedPath = buildPath(getcwd(), "allure", uuid, "name.1.some_image.png"); auto existingPath = buildPath(getcwd(), "allure", uuid, "name.0.some_image.png"); std.file.write(existingPath, ""); scope(exit) { rmdirRecurse("allure"); } auto a = AllureAttachmentXml("allure", Attachment("name", resource, ""), 0, uuid); expectedPath.exists.should.equal(true); }