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 }