1 module cucumber.result; 2 3 import core.time : Duration; 4 import std.algorithm : map; 5 import std.algorithm.iteration : filter; 6 import std.array : array, empty; 7 import std.conv : to; 8 import std.json : JSONValue, parseJSON; 9 import std.string : format, strip, toLower, tr; 10 import std.typecons : Nullable; 11 12 import asdf : serializationIgnoreOut, serializationIgnoreOutIf, 13 serializationKeyOut, serializationTransformOut; 14 import gherkin : Feature, Scenario, Step, GherkinTag = Tag; 15 16 /// 17 enum Result 18 { 19 FAILED = "failed", 20 SKIPPED = "skipped", 21 UNDEFINED = "undefined", 22 PASSED = "passed" 23 } 24 25 alias FAILED = Result.FAILED; 26 alias SKIPPED = Result.SKIPPED; 27 alias UNDEFINED = Result.UNDEFINED; 28 alias PASSED = Result.PASSED; 29 30 /// 31 mixin template isResult() 32 { 33 mixin template isResult(string result) 34 { 35 import std.string : capitalize; 36 import std.uni : toUpper; 37 38 mixin("bool is%s() const { return this.result == Result.%s; }".format(result.capitalize, 39 result.toUpper)); 40 } 41 42 mixin isResult!"failed"; 43 mixin isResult!"skipped"; 44 mixin isResult!"undefined"; 45 mixin isResult!"passed"; 46 } 47 48 /// 49 mixin template property(T, string parent, string key) 50 { 51 mixin property!(T, parent, key, key); 52 } 53 54 /// 55 mixin template property(T, string parent, string key, string field) 56 { 57 mixin("@property %s %s() const { return %s.%s; }".format(T.stringof, key, parent, field)); 58 } 59 60 /// 61 string createId(string name) 62 { 63 if (name.empty) 64 { 65 return ""; 66 } 67 return name.toLower.tr(` `, `-`); 68 } 69 70 /// 71 struct Comment 72 { 73 /// 74 string value; 75 /// 76 ulong line; 77 } 78 79 /// 80 struct Tag 81 { 82 /// 83 string name; 84 /// 85 ulong line; 86 } 87 88 /// 89 struct Row 90 { 91 /// 92 string[] cells; 93 } 94 95 /// 96 struct DocString 97 { 98 /// 99 string value; 100 /// 101 @serializationKeyOut("content_type") string contentType; 102 /// 103 ulong line; 104 } 105 106 /// 107 struct ResultSummary 108 { 109 /// 110 ulong failed, skipped, undefined, passed; 111 112 /// 113 ulong total() const 114 { 115 return failed + skipped + undefined + passed; 116 } 117 118 /// 119 this(Result result) 120 { 121 switch (result) 122 { 123 case Result.FAILED: 124 this.failed = 1; 125 break; 126 case Result.SKIPPED: 127 this.skipped = 1; 128 break; 129 case Result.UNDEFINED: 130 this.undefined = 1; 131 break; 132 case Result.PASSED: 133 this.passed = 1; 134 break; 135 default: 136 // do nothing 137 } 138 } 139 140 /// 141 ResultSummary opOpAssign(string operator)(ResultSummary val) 142 { 143 if (operator == "+") 144 { 145 this.failed += val.failed; 146 this.skipped += val.skipped; 147 this.undefined += val.undefined; 148 this.passed += val.passed; 149 return this; 150 } 151 assert(0); 152 } 153 } 154 155 /// 156 struct RunResult 157 { 158 /// 159 Duration time; 160 /// 161 ResultSummary[string] resultSummaries; 162 /// 163 FeatureResult[] featureResults; 164 165 /// 166 RunResult opOpAssign(string operator)(FeatureResult val) 167 { 168 if (operator == "+") 169 { 170 if (val.scenarioResults.empty) 171 { 172 return this; 173 } 174 this.featureResults ~= val; 175 this.time += val.time; 176 if (!("scenario" in resultSummaries)) 177 resultSummaries["scenario"] = ResultSummary(); 178 if (!("step" in resultSummaries)) 179 resultSummaries["step"] = ResultSummary(); 180 181 resultSummaries["scenario"] += val.resultSummary; 182 foreach (scenarioResult; val.scenarioResults) 183 { 184 resultSummaries["step"] += scenarioResult.resultSummary; 185 } 186 return this; 187 } 188 assert(0); 189 } 190 } 191 192 /// 193 struct FeatureResult 194 { 195 /// 196 @serializationIgnoreOut Feature feature; 197 /// 198 @serializationIgnoreOut Result result = Result.PASSED; 199 /// 200 @serializationIgnoreOut ScenarioResult[] scenarioResults; 201 /// 202 @serializationIgnoreOut Duration time; 203 /// 204 @serializationIgnoreOut ResultSummary resultSummary; 205 206 mixin isResult; 207 208 /// 209 FeatureResult opOpAssign(string operator)(ScenarioResult val) 210 { 211 if (operator == "+") 212 { 213 this.scenarioResults ~= val; 214 this.time += val.time; 215 if (!val.isPassed && this.isPassed) 216 { 217 this.result = Result.FAILED; 218 } 219 if (!val.scenario.isBackground) 220 { 221 this.resultSummary += ResultSummary(val.result); 222 } 223 return this; 224 } 225 assert(0); 226 } 227 228 mixin property!(string, "feature", "uri"); 229 230 /// 231 @property string id() 232 { 233 return createId(feature.name); 234 } 235 236 mixin property!(string, "feature", "keyword"); 237 mixin property!(string, "feature", "name"); 238 mixin property!(string, "feature", "description"); 239 mixin property!(ulong, "feature.location", "line"); 240 241 /// 242 @property @serializationIgnoreOutIf!`a.empty` Tag[] tags() 243 { 244 return feature.tags.map!(t => Tag(t.name, t.location.line)).array; 245 } 246 247 /// 248 @property @serializationIgnoreOutIf!`a.empty` Comment[] comments() 249 { 250 return feature.comments.map!(c => Comment(c.text.strip, c.location.line)).array; 251 } 252 253 /// 254 @property @serializationIgnoreOutIf!`a.empty` ScenarioResult[] elements() 255 { 256 return scenarioResults.filter!(r => !r.stepResults.empty || r.scenario.isBackground).array; 257 } 258 } 259 260 /// 261 struct ScenarioResult 262 { 263 /// 264 @serializationIgnoreOut Scenario scenario; 265 /// 266 @serializationIgnoreOut string location; 267 /// 268 @serializationIgnoreOut Result result = Result.PASSED; 269 /// 270 @serializationIgnoreOut StepResult[] stepResults; 271 /// 272 @serializationIgnoreOut Duration time; 273 /// 274 @serializationIgnoreOut ResultSummary resultSummary; 275 /// 276 @serializationIgnoreOut ulong exampleNumber; 277 /// 278 @serializationIgnoreOut GherkinTag[] scenarioTags; 279 280 mixin isResult; 281 282 /// 283 ScenarioResult opOpAssign(string operator)(StepResult val) 284 { 285 if (operator == "+") 286 { 287 this.stepResults ~= val; 288 this.time += val.time; 289 if (!val.isPassed && this.isPassed) 290 { 291 this.result = val.result; 292 } 293 this.resultSummary += ResultSummary(val.result); 294 return this; 295 } 296 assert(0); 297 } 298 299 /// 300 @serializationIgnoreOutIf!`a.empty` string id; 301 302 mixin property!(string, "scenario", "keyword"); 303 mixin property!(string, "scenario", "name"); 304 mixin property!(string, "scenario", "description"); 305 mixin property!(ulong, "scenario.location", "line"); 306 307 /// 308 @property string type() 309 { 310 return scenario.isBackground ? "background" : "scenario"; 311 } 312 313 /// 314 @property @serializationIgnoreOutIf!`a.empty` Comment[] comments() 315 { 316 return scenario.comments.map!(c => Comment(c.text.strip, c.location.line)).array; 317 } 318 319 /// 320 @property @serializationIgnoreOutIf!`a.empty` Tag[] tags() 321 { 322 if (scenario.isBackground) 323 { 324 return []; 325 } 326 return scenarioTags.map!(t => Tag(t.name, t.location.line)).array; 327 } 328 329 /// 330 @property @serializationIgnoreOutIf!`a.empty` StepResult[] steps() 331 { 332 return stepResults; 333 } 334 } 335 336 /// 337 struct StepResult 338 { 339 /// 340 @serializationIgnoreOut Step step; 341 /// 342 @serializationIgnoreOut string location; 343 /// 344 @serializationIgnoreOut Result result = Result.PASSED; 345 /// 346 @serializationIgnoreOut Duration time; 347 /// 348 @serializationIgnoreOut Nullable!Exception exception; 349 350 mixin isResult; 351 352 mixin property!(string, "step", "keyword"); 353 mixin property!(string, "step", "name", "text"); 354 mixin property!(ulong, "step.location", "line"); 355 356 /// 357 @property @serializationIgnoreOutIf!`a.empty` Row[] rows() 358 { 359 return step.dataTable.rows.map!(r => Row(r.cells.map!(c => c.value.empty 360 ? `` : c.value).array)).array; 361 } 362 363 /// 364 @property @serializationIgnoreOutIf!`a.value.empty`@serializationKeyOut("doc_string") 365 DocString docString() 366 { 367 if (step.docString.isNull) 368 { 369 return DocString(); 370 } 371 with (step.docString.get) 372 { 373 return DocString(content, contentType, location.line); 374 } 375 } 376 377 /// 378 @property @serializationIgnoreOutIf!`a.empty` Comment[] comments() 379 { 380 return step.comments.map!(c => Comment(c.text.strip, c.location.line)).array; 381 } 382 383 /// 384 @property Match match() 385 { 386 return Match(location); 387 } 388 389 /// 390 @property @serializationKeyOut("result") 391 ResultStatus resultStatus() 392 { 393 auto result = ResultStatus(result); 394 395 if (this.isPassed || this.isFailed) 396 { 397 result.duration = time.total!"nsecs"; 398 } 399 if (this.isFailed) 400 { 401 result.error_message = exception.get.message.to!string; 402 } 403 return result; 404 } 405 406 /// 407 struct Match 408 { 409 /// 410 string location; 411 } 412 413 /// 414 struct ResultStatus 415 { 416 /// 417 @serializationTransformOut!`a.toLower` Result status; 418 /// 419 @serializationIgnoreOutIf!`a.isNull`@serializationTransformOut!`a.get` Nullable!ulong duration; 420 /// 421 @serializationIgnoreOutIf!`a.isNull`@serializationTransformOut!`a.get` Nullable!string error_message; 422 } 423 }