1 /++
2   A module containing the XUnitReporter
3 
4   Copyright: © 2017 Szabo Bogdan
5   License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6   Authors: Szabo Bogdan
7 +/
8 module trial.reporters.xunit;
9 
10 import std.stdio;
11 import std.array;
12 import std.conv;
13 import std.datetime;
14 import std..string;
15 import std.algorithm;
16 import std.file;
17 import std.path;
18 import std.uuid;
19 import std.range;
20 
21 import trial.interfaces;
22 import trial.reporters.writer;
23 
24 private string escapeXUnit(string data) {
25   string escapedData = data.dup;
26 
27   escapedData = escapedData.replace(`&`, `&`);
28   escapedData = escapedData.replace(`"`, `"`);
29   escapedData = escapedData.replace(`'`, `'`);
30   escapedData = escapedData.replace(`<`, `&lt;`);
31   escapedData = escapedData.replace(`>`, `&gt;`);
32 
33   return escapedData;
34 }
35 
36 /// The XUnit reporter creates a xml containing the test results
37 class XUnitReporter : ILifecycleListener
38 {
39 
40   private {
41     immutable string destination;
42   }
43 
44   this(string destination) {
45     this.destination = destination;
46   }
47 
48   void begin(ulong testCount) {
49     if(exists(destination)) {
50       std.file.rmdirRecurse(destination);
51     }
52   }
53 
54   void update() {}
55 
56   void end(SuiteResult[] result)
57   {
58     if(!exists(destination)) {
59       destination.mkdirRecurse;
60     }
61 
62     foreach(item; result) {
63       string uuid = randomUUID.toString;
64       string xml = `<?xml version="1.0" encoding="UTF-8"?>` ~ "\n"~ `<testsuites>` ~ "\n" ~ XUnitSuiteXml(item, uuid).toString ~ "\n</testsuites>\n";
65 
66       std.file.write(buildPath(destination, item.name ~ ".xml"), xml);
67     }
68   }
69 }
70 
71 struct XUnitSuiteXml {
72   /// The suite result
73   SuiteResult result;
74 
75   /// The suite id
76   string uuid;
77 
78   /// Converts the suiteResult to a xml string
79   string toString() {
80     auto epoch = SysTime.fromUnixTime(0);
81     string tests = result.tests.map!(a => XUnitTestXml(a, uuid).toString).array.join("\n");
82 
83 
84     auto failures = result.tests.filter!(a => a.status == TestResult.Status.failure).count;
85     auto skipped = result.tests.filter!(a => a.status == TestResult.Status.skip).count;
86     auto errors = result.tests.filter!(a =>
87       a.status != TestResult.Status.success &&
88       a.status != TestResult.Status.skip &&
89       a.status != TestResult.Status.failure).count;
90 
91     if(tests != "") {
92       tests = "\n" ~ tests;
93     }
94 
95     auto xml = `  <testsuite name="` ~ result.name ~ `" errors="` ~ errors.to!string ~ `" skipped="` ~ skipped.to!string ~ `" tests="` ~ result.tests.length.to!string ~ `" failures="` ~ failures.to!string ~ `" time="0" timestamp="` ~ result.begin.toISOExtString ~ `">`
96      ~ tests ~ `
97   </testsuite>`;
98 
99     return xml;
100   }
101 }
102 
103 version(unittest) {
104   version(Have_fluent_asserts) {
105     import fluent.asserts;
106   }
107 }
108 
109 /// XUnitTestXml should transform a suite with a success test
110 unittest
111 {
112   auto epoch = SysTime.fromUnixTime(0);
113   auto result = SuiteResult("Test Suite");
114   result.begin = Clock.currTime;
115 
116   TestResult test = new TestResult("Test");
117   test.begin = Clock.currTime;
118   test.end = Clock.currTime;
119   test.status = TestResult.Status.success;
120 
121   result.end = Clock.currTime;
122 
123   result.tests = [ test ];
124 
125   auto xunit = XUnitSuiteXml(result);
126 
127   xunit.toString.should.equal(`  <testsuite name="` ~ result.name.escapeXUnit ~ `" errors="0" skipped="0" tests="1" failures="0" time="0" timestamp="`~result.begin.toISOExtString~`">
128       <testcase name="Test">
129       </testcase>
130   </testsuite>`);
131 }
132 
133 /// XUnitTestXml should transform a suite with a failed test
134 unittest
135 {
136   auto epoch = SysTime.fromUnixTime(0);
137   auto result = SuiteResult("Test Suite");
138   result.begin = Clock.currTime;
139 
140   TestResult test = new TestResult("Test");
141   test.begin = Clock.currTime;
142   test.end = Clock.currTime;
143   test.status = TestResult.Status.failure;
144 
145   result.end = Clock.currTime;
146 
147   result.tests = [ test ];
148 
149   auto xunit = XUnitSuiteXml(result);
150 
151   xunit.toString.should.equal(`  <testsuite name="` ~ result.name.escapeXUnit ~ `" errors="0" skipped="0" tests="1" failures="1" time="0" timestamp="`~result.begin.toISOExtString~`">
152       <testcase name="Test">
153       <failure/>
154       </testcase>
155   </testsuite>`);
156 }
157 
158 /// XUnitTestXml should transform a suite with a skipped test
159 unittest
160 {
161   auto epoch = SysTime.fromUnixTime(0);
162   auto result = SuiteResult("Test Suite");
163   result.begin = Clock.currTime;
164 
165   TestResult test = new TestResult("Test");
166   test.begin = Clock.currTime;
167   test.end = Clock.currTime;
168   test.status = TestResult.Status.skip;
169 
170   result.end = Clock.currTime;
171 
172   result.tests = [ test ];
173 
174   auto xunit = XUnitSuiteXml(result);
175 
176   xunit.toString.should.equal(`  <testsuite name="` ~ result.name.escapeXUnit ~ `" errors="0" skipped="1" tests="1" failures="0" time="0" timestamp="`~result.begin.toISOExtString~`">
177       <testcase name="Test">
178       <skipped/>
179       </testcase>
180   </testsuite>`);
181 }
182 
183 
184 /// XUnitTestXml should transform a suite with a unknown test
185 unittest
186 {
187   auto epoch = SysTime.fromUnixTime(0);
188   auto result = SuiteResult("Test Suite");
189   result.begin = Clock.currTime;
190 
191   TestResult test = new TestResult("Test");
192   test.begin = Clock.currTime;
193   test.end = Clock.currTime;
194   test.status = TestResult.Status.unknown;
195 
196   result.end = Clock.currTime;
197 
198   result.tests = [ test ];
199 
200   auto xunit = XUnitSuiteXml(result);
201 
202   xunit.toString.should.equal(`  <testsuite name="` ~ result.name.escapeXUnit ~ `" errors="1" skipped="0" tests="1" failures="0" time="0" timestamp="`~result.begin.toISOExtString~`">
203       <testcase name="Test">
204       <error message="unknown status">unknown</error>
205       </testcase>
206   </testsuite>`);
207 }
208 
209 /// XUnitTestXml should transform an empty suite
210 unittest
211 {
212   auto epoch = SysTime.fromUnixTime(0);
213   auto result = SuiteResult("Test Suite");
214   result.begin = Clock.currTime;
215   result.end = Clock.currTime;
216 
217   auto xunit = XUnitSuiteXml(result);
218 
219   xunit.toString.should.equal(`  <testsuite name="` ~ result.name.escapeXUnit ~ `" errors="0" skipped="0" tests="0" failures="0" time="0" timestamp="` ~ result.begin.toISOExtString ~ `">
220   </testsuite>`);
221 }
222 
223 struct XUnitTestXml {
224   ///
225   TestResult result;
226 
227   ///
228   string uuid;
229 
230   /// Return the string representation of the test
231   string toString() {
232     auto time = (result.end -result.begin).total!"msecs";
233     string xml = `      <testcase name="` ~ result.name.escapeXUnit ~ `">` ~ "\n";
234 
235     if(result.status == TestResult.Status.failure) {
236       if(result.throwable !is null) {
237         auto lines = result.throwable.msg.split("\n") ~ "no message";
238 
239         xml ~= `      <failure message="` ~ lines[0].escapeXUnit ~ `">` ~ result.throwable.to!string.escapeXUnit ~ `</failure>` ~ "\n";
240       } else {
241         xml ~= `      <failure/>` ~ "\n";
242       }
243     } else if(result.status == TestResult.Status.skip) {
244       xml ~= `      <skipped/>` ~ "\n";
245     } else if(result.status != TestResult.Status.success) {
246         xml ~= `      <error message="unknown status">` ~ result.status.to!string.escapeXUnit ~ `</error>` ~ "\n";
247     }
248 
249     xml ~= `      </testcase>`;
250 
251     return xml;
252   }
253 }
254 
255 /// XUnitTestXml should transform a success test
256 unittest
257 {
258   auto epoch = SysTime.fromUnixTime(0);
259 
260   TestResult result = new TestResult("Test");
261   result.begin = Clock.currTime;
262   result.end = Clock.currTime;
263   result.status = TestResult.Status.success;
264 
265   auto allure = XUnitTestXml(result);
266 
267   allure.toString.strip.should.equal(`<testcase name="Test">` ~ "\n      </testcase>");
268 }
269 
270 /// XUnitTestXml should transform a failing test
271 unittest
272 {
273   auto epoch = SysTime.fromUnixTime(0);
274   TestResult result = new TestResult("Test");
275   result.begin = Clock.currTime;
276   result.end = Clock.currTime;
277   result.status = TestResult.Status.failure;
278   result.throwable = new Exception("message");
279 
280   auto xunit = XUnitTestXml(result);
281   xunit.toString.strip.should.equal(`<testcase name="Test">` ~ "\n" ~
282   `      <failure message="message">` ~ result.throwable.to!string ~ `</failure>` ~ "\n" ~
283   `      </testcase>`);
284 }