1 module cucumber.formatter.pretty;
2 
3 import std.algorithm : max;
4 import std.algorithm.iteration : filter, map;
5 import std.array : array, empty, join;
6 import std.conv : to;
7 import std.range : repeat, walkLength;
8 import std.stdio : write, writef, writefln, writeln;
9 import std..string : leftJustifier, split;
10 import std.typecons : Nullable;
11 
12 import cucumber.formatter.base : Formatter;
13 import cucumber.formatter.color : colors, noColors;
14 import cucumber.result : FAILED, SKIPPED, UNDEFINED, PASSED, Result, RunResult,
15     ScenarioResult, StepResult;
16 import gherkin : Feature, Scenario, Step, Examples, TableRow, Comment, Cell;
17 
18 ///
19 class Pretty : Formatter
20 {
21     private bool noColor;
22     private bool noSnippets;
23     private bool noSource;
24 
25     ///
26     this(bool noColor, bool noSnippets, bool noSource)
27     {
28         this.noColor = noColor;
29         this.noSnippets = noSnippets;
30         this.noSource = noSource;
31     }
32 
33     ///
34     override string color(string result)
35     {
36         return noColor ? noColors[result] : colors[result];
37     }
38 
39     ///
40     override void feature(Feature feature)
41     {
42         base(feature);
43     }
44 
45     ///
46     override void scenario(ref Scenario scenario)
47     {
48         if (scenario.isScenarioOutline)
49         {
50             return;
51         }
52         base(scenario, "  ");
53     }
54 
55     ///
56     override void scenarioOutline(ref Scenario scenario)
57     {
58         base(scenario, "  ");
59         foreach (step; scenario.steps)
60         {
61             this.step(step, SKIPPED,
62                     scenario.parent.parent.uri ~ `:` ~ step.location.line.to!string);
63         }
64         writeln;
65     }
66 
67     ///
68     override void examples(Examples examples)
69     {
70         base(examples, "    ");
71     }
72 
73     ///
74     override void tableRow(TableRow tableRow, ref TableRow[] table, string resultColor)
75     {
76         if (this.table != table)
77         {
78             setCellSize(table);
79         }
80         writeln("      ", justifyCells(tableRow.cells, resultColor));
81     }
82 
83     ///
84     override void tableRow(TableRow tableRow, ref TableRow[] table, ScenarioResult scenarioResult)
85     {
86         with (scenarioResult)
87         {
88             this.tableRow(tableRow, table,
89                     (stepResults.filter!(r => r.isUndefined).empty) ? result : UNDEFINED);
90             foreach (stepResult; stepResults)
91             {
92                 error(stepResult.exception, "      ");
93             }
94         }
95     }
96 
97     ///
98     override void step(Step step, StepResult stepResult)
99     {
100         if (step.parent.isScenarioOutline)
101         {
102             return;
103         }
104 
105         this.step(step, stepResult.result, stepResult.location);
106         if (!step.docString.isNull)
107         {
108             with (step.docString.get)
109             {
110                 writeln("      ", color(stepResult.result), delimiter);
111                 writeln(content.split("\n").map!(x => "      " ~ x).array.join("\n"));
112                 writeln("      ", delimiter, color("reset"));
113             }
114         }
115         if (!step.dataTable.isNull)
116         {
117             foreach (row; step.dataTable.get.rows)
118             {
119                 tableRow(row, step.dataTable.get.rows, SKIPPED);
120             }
121         }
122         if (stepResult.isFailed)
123         {
124             error(stepResult.exception, "    ");
125         }
126     }
127 
128     ///
129     override void emptyLine()
130     {
131         writeln;
132     }
133 
134     ///
135     override void comment(Comment comment)
136     {
137         writeln(comment.text);
138     }
139 
140     ///
141     override void summarizeResult(RunResult result)
142     {
143         struct FailingScenario
144         {
145             string text;
146             string source;
147         }
148 
149         FailingScenario[] failingScenarios;
150         ulong maxLength;
151         foreach (featureResult; result.featureResults)
152         {
153             foreach (scenarioResult; featureResult.scenarioResults.filter!(x => x.isFailed))
154             {
155                 auto text = "cucumber " ~ featureResult.feature.parent.uri ~ ":"
156                     ~ scenarioResult.scenario.location.line.to!string;
157                 auto source = scenarioResult.scenario.keyword ~ `: ` ~ (scenarioResult.scenario.name.isNull
158                         ? `` : scenarioResult.scenario.name.get);
159                 if (!scenarioResult.exampleNumber.isNull)
160                 {
161                     source ~= ", Examples (#" ~ scenarioResult.exampleNumber.get.to!string ~ `)`;
162                 }
163 
164                 auto failingScenario = FailingScenario(text, source);
165                 maxLength = max(maxLength, failingScenario.text.walkLength);
166                 failingScenarios ~= failingScenario;
167             }
168         }
169 
170         if (!failingScenarios.empty)
171         {
172             writeln(color("failed"), "Failing Scenarios:", color("reset"));
173 
174             foreach (failingScenario; failingScenarios)
175             {
176                 if (noSource)
177                 {
178                     writeln(color("failed"), failingScenario.text, color("reset"));
179                 }
180                 else
181                 {
182                     writeln(color("failed"), leftJustifier(failingScenario.text, maxLength + 1),
183                             color("reset"), color("gray"), "# ",
184                             failingScenario.source, color("reset"));
185                 }
186             }
187             writeln;
188         }
189         runResult(result);
190     }
191 
192 private:
193 
194     Scenario runningScenario;
195     TableRow[] table;
196     ulong scenarioStringLength;
197     ulong[] cellSizes;
198 
199     void base(T)(T element, string extraIndent = "")
200     {
201         with (element)
202         {
203             if (!tags.empty)
204             {
205                 writeln(extraIndent, color("skipped"), tags.map!(t => t.name)
206                         .join(" "), color("reset"));
207             }
208             string line = extraIndent ~ keyword ~ ": ";
209             static if (is(typeof(name) == Nullable!string))
210             {
211                 line ~= name.isNull ? `` : name.get;
212             }
213             else static if (is(typeof(element) == Step))
214             {
215                 line ~= text;
216             }
217             else
218             {
219                 line ~= name;
220             }
221             static if (is(typeof(element) == Scenario))
222             {
223                 setScenarioStringLength(element);
224                 if (noSource)
225                 {
226                     write(line);
227                 }
228                 else
229                 {
230                     write(leftJustifier(line, scenarioStringLength + 4));
231                     write(color("gray"), " # ", element.parent.parent.uri, `:`,
232                             element.location.line.to!string, color("reset"));
233                 }
234             }
235             else
236             {
237                 write(line);
238             }
239             writeln;
240             if (!description.isNull)
241             {
242                 writeln(extraIndent, description);
243             }
244         }
245     }
246 
247     void step(Step step, string resultColor, string location)
248     {
249         setScenarioStringLength(step.parent);
250         if (noSource)
251         {
252             write("    ", color(resultColor), step.keyword ~ step.text, color("reset"));
253         }
254         else
255         {
256             write("    ", color(resultColor), leftJustifier(step.keyword ~ step.text,
257                     scenarioStringLength), color("reset"));
258             write(color("gray"), " # ", location, color("reset"));
259         }
260         writeln;
261     }
262 
263     void setScenarioStringLength(ref Scenario scenario)
264     {
265         if (this.runningScenario == scenario)
266         {
267             return;
268         }
269         scenarioStringLength = scenario.keyword.walkLength + (scenario.name.isNull
270                 ? 0 : scenario.name.get.walkLength);
271         foreach (step; scenario.steps)
272         {
273             auto stepStringLength = (step.keyword ~ (step.text)).walkLength;
274             scenarioStringLength = max(scenarioStringLength, stepStringLength);
275         }
276         this.runningScenario = scenario;
277     }
278 
279     void setCellSize(TableRow[] rows)
280     {
281         this.table = rows;
282         cellSizes = 0LU.repeat(rows.length).array;
283         foreach (i, row; rows)
284         {
285             foreach (j, cell; row.cells)
286             {
287                 if (!cell.value.empty)
288                 {
289                     cellSizes[j] = max(cellSizes[j], cell.value.walkLength);
290                 }
291             }
292         }
293     }
294 
295     string justifyCells(Cell[] cells, string resultColor)
296     {
297         string[] cellStrings;
298         foreach (i, cell; cells)
299         {
300             string cellString;
301             if (!cell.value.empty)
302             {
303                 cellString = color(resultColor) ~
304                         cell.value.leftJustifier(cellSizes[i]).to!string ~ color("reset");
305             }
306             cellStrings ~= cellString;
307         }
308         return "| " ~ cellStrings.join(" | ") ~ " |";
309     }
310 
311     void error(Nullable!Exception exception, string extraIndent = "")
312     {
313         if (exception.isNull)
314         {
315             return;
316         }
317         string message = exception.get.message.split("\n")
318             .map!(l => extraIndent ~ l).array.join("\n");
319         writefln("%s%s%s", color("failed"), message, color("reset"));
320     }
321 }