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(`<`, `<`); 31 escapedData = escapedData.replace(`>`, `>`); 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 }