1 module cucumber.runner;
2 
3 import std.algorithm : each, filter;
4 import std.array : empty;
5 import std.conv : to;
6 import std.datetime.stopwatch : StopWatch, AutoStart;
7 import std.range : zip;
8 import std.string : replace, toLower, tr;
9 import std.typecons : Nullable;
10 
11 import cucumber.formatter;
12 import cucumber.reflection : findMatch, MatchResult;
13 import cucumber.result : FAILED, SKIPPED, UNDEFINED, PASSED, Result,
14     FeatureResult, ScenarioResult, StepResult, createId;
15 import gherkin : GherkinDocument, Feature, Background, Scenario, Step, Tag, Comment;
16 
17 /**
18  * Cucumber Feature Runner
19  */
20 class CucumberRunner
21 {
22 private:
23     GherkinDocument document;
24     Formatter formatter;
25     bool dryRun;
26     bool isFirstBackground = true;
27 
28 public:
29     ///
30     this(Formatter formatter, bool dryRun)
31     {
32         this.formatter = formatter;
33         this.dryRun = dryRun;
34     }
35 
36     /**
37       * Run GherkinDocument
38       *
39       * Params:
40       *   gherkinDocument = Gherkin document to run
41       */
42     FeatureResult runFeature(ModuleNames...)(GherkinDocument gherkinDocument)
43     {
44         this.document = gherkinDocument;
45         if (this.document.feature.isNull)
46         {
47             return FeatureResult();
48         }
49 
50         auto feature = this.document.feature.get;
51         auto featureResult = FeatureResult(feature);
52 
53         if (!feature.scenarios.empty)
54         {
55             formatter.feature(feature);
56         }
57 
58         foreach (scenario; feature.scenarios)
59         {
60             if (scenario.steps.empty && feature.background.isNull)
61             {
62                 continue;
63             }
64             if (scenario.isScenarioOutline)
65             {
66                 if ((scenario.examples.empty || scenario.steps.empty) && feature.background.isNull)
67                 {
68                     continue;
69                 }
70                 runScenarioOutline!ModuleNames(feature, scenario, feature.background).each!(
71                         r => featureResult += r);
72             }
73             else
74             {
75                 auto scenarioResults = runScenario!ModuleNames(feature,
76                         scenario, feature.background);
77                 if (!feature.background.isNull)
78                 {
79                     featureResult += scenarioResults[0];
80                 }
81                 featureResult += scenarioResults[1];
82             }
83         }
84 
85         return featureResult;
86     }
87 
88     ///
89     ScenarioResult[] runScenarioOutline(ModuleNames...)(Feature feature,
90             Scenario scenario, Nullable!Scenario background)
91     {
92         ScenarioResult[] results;
93 
94         if (scenario.examples.empty || (scenario.steps.empty && background.isNull))
95         {
96             return results;
97         }
98 
99         if (!scenario.steps.empty)
100         {
101             formatter.scenario(scenario);
102             foreach (step; scenario.steps)
103             {
104                 formatter.step(step, StepResult(step,
105                         scenario.uri ~ `:` ~ step.location.line.to!string, SKIPPED));
106             }
107         }
108 
109         foreach (examples; scenario.examples)
110         {
111             if (!examples.tableBody.empty && !scenario.steps.empty)
112             {
113                 formatter.examples(examples);
114             }
115             if (examples.tableHeader.empty)
116             {
117                 continue;
118             }
119 
120             auto table = examples.tableBody ~ examples.tableHeader;
121             if (!examples.tableBody.empty && !scenario.steps.empty)
122             {
123                 formatter.tableRow(examples.tableHeader, table, "skipped");
124             }
125 
126             foreach (i, row; examples.tableBody)
127             {
128                 string[string] examplesValues = null;
129                 foreach (example; zip(examples.tableHeader.cells, row.cells))
130                 {
131                     examplesValues[example[0].value] = example[1].value;
132                 }
133 
134                 auto _scenario = new Scenario(scenario.keyword, scenario.name,
135                         row.location, false, scenario.uri, scenario.comments);
136                 _scenario.tags = scenario.tags ~ examples.tags;
137                 _scenario.comments ~= examples.comments ~ row.comments;
138                 _scenario.description = scenario.description;
139                 _scenario.isScenarioOutline = true;
140                 foreach (step; scenario.steps)
141                 {
142                     auto _step = step;
143                     _step.isScenarioOutline = true;
144                     foreach (k, v; examplesValues)
145                     {
146                         _step.replace(`<` ~ k ~ `>`, v);
147                     }
148                     _scenario.steps ~= _step;
149                 }
150 
151                 auto result = runScenario!ModuleNames(feature, _scenario, background);
152 
153                 auto id = createId(feature.name) ~ `;` ~ createId(scenario.name);
154                 id ~= `;` ~ createId(examples.name);
155                 id ~= `;` ~ (i + 2).to!string;
156                 result[1].id = id;
157                 result[1].exampleNumber = i + 2;
158 
159                 if (!background.isNull)
160                 {
161                     results ~= result[0];
162                 }
163                 results ~= result[1];
164                 if (!scenario.steps.empty)
165                 {
166                     formatter.tableRow(row, table, result[1]);
167                 }
168             }
169         }
170 
171         return results;
172     }
173 
174     ///
175     ScenarioResult[] runScenario(ModuleNames...)(Feature feature,
176             Scenario scenario, Nullable!Scenario background)
177     {
178         ScenarioResult backgroundResult;
179         auto result = ScenarioResult(scenario, scenario.uri ~ `:`
180                 ~ scenario.location.line.to!string);
181 
182         if (!background.isNull)
183         {
184             Nullable!Scenario nullScenario;
185             backgroundResult = runScenario!ModuleNames(feature, background.get, nullScenario)[1];
186             this.isFirstBackground = false;
187             result.result = backgroundResult.result;
188         }
189 
190         if ((!scenario.isBackground || this.isFirstBackground)
191                 && !scenario.isScenarioOutline && !scenario.steps.empty)
192         {
193             formatter.scenario(scenario);
194             // Output failed steps in Background
195             backgroundResult.stepResults
196                 .filter!(r => r.isFailed)
197                 .each!(r => formatter.step(r.step, r));
198         }
199 
200         if (!scenario.isBackground)
201         {
202             auto id = createId(feature.name) ~ `;` ~ createId(scenario.name);
203             result.id = id;
204             result.scenarioTags = feature.tags ~ scenario.tags;
205         }
206 
207         foreach (step; scenario.steps)
208         {
209             StepResult stepResult;
210 
211             if (result.isPassed)
212             {
213                 stepResult = runStep!ModuleNames(step);
214             }
215             else
216             {
217                 stepResult = runStep!ModuleNames(step, true);
218                 stepResult.result = stepResult.isUndefined ? UNDEFINED : SKIPPED;
219             }
220             stepResult.location = scenario.uri ~ `:` ~ (scenario.isScenarioOutline
221                     ? scenario.location.line : step.location.line).to!string;
222 
223             result += stepResult;
224 
225             if ((!scenario.isBackground || this.isFirstBackground) && !scenario.isScenarioOutline)
226             {
227                 formatter.step(step, stepResult);
228             }
229         }
230 
231         return [backgroundResult, result];
232     }
233 
234     ///
235     StepResult runStep(ModuleNames...)(Step step, bool skip = false)
236     {
237         auto result = StepResult(step);
238         auto func = findMatch!ModuleNames(step.text);
239         auto sw = StopWatch(AutoStart.yes);
240 
241         if (func)
242         {
243             result.location = func.source;
244             if (skip || this.dryRun)
245             {
246                 result.result = SKIPPED;
247             }
248             else
249             {
250                 try
251                 {
252                     if (!step.docString.isNull)
253                     {
254                         func(step.docString.get);
255                     }
256                     else if (!step.dataTable.empty)
257                     {
258                         func(step.dataTable);
259                     }
260                     else
261                     {
262                         func();
263                     }
264                 }
265                 catch (Exception e)
266                 {
267                     result.result = FAILED;
268                     result.exception = e;
269                 }
270             }
271         }
272         else
273         {
274             result.result = UNDEFINED;
275         }
276         result.time = sw.peek();
277 
278         return result;
279     }
280 }