1 module cucumber.runner;
2 
3 import std.algorithm : each, filter;
4 import std.conv : to;
5 import std.datetime.stopwatch : StopWatch, AutoStart;
6 import std.range : zip;
7 import std..string : replace;
8 import std.typecons : Nullable;
9 
10 import cucumber.formatter;
11 import cucumber.reflection : findMatch, MatchResult;
12 import cucumber.result : FAILED, SKIPPED, UNDEFINED, PASSED, Result,
13     FeatureResult, ScenarioResult, StepResult;
14 import gherkin : GherkinDocument, Background, Scenario, Step;
15 
16 /**
17  * Cucumber Feature Runner
18  */
19 class CucumberRunner
20 {
21 private:
22     GherkinDocument document;
23     Formatter formatter;
24     bool dryRun;
25     ulong lineNumber;
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         outputComments(feature.location.line);
54         formatter.feature(feature);
55 
56         foreach (scenario; feature.scenarios)
57         {
58             if (scenario.isScenarioOutline)
59             {
60                 runScenarioOutline!ModuleNames(scenario, feature.background).each!(
61                         r => featureResult += r);
62             }
63             else
64             {
65                 featureResult += runScenario!ModuleNames(scenario, feature.background);
66             }
67         }
68 
69         return featureResult;
70     }
71 
72     ///
73     ScenarioResult[] runScenarioOutline(ModuleNames...)(Scenario scenario,
74             Nullable!Scenario background)
75     {
76         ScenarioResult[] results;
77 
78         outputComments(scenario.location.line);
79         formatter.scenarioOutline(scenario);
80 
81         foreach (examples; scenario.examples)
82         {
83             outputComments(examples.location.line);
84             formatter.examples(examples);
85             if (examples.tableHeader.isNull)
86             {
87                 continue;
88             }
89 
90             auto table = examples.tableBody ~ examples.tableHeader.get;
91             outputComments(examples.tableHeader.get.location.line);
92             formatter.tableRow(examples.tableHeader.get, table, "skipped");
93 
94             foreach (i, row; examples.tableBody)
95             {
96                 ScenarioResult result;
97 
98                 string[string] examplesValues = null;
99                 foreach (example; zip(examples.tableHeader.get.cells, row.cells))
100                 {
101                     examplesValues[example[0].value] = example[1].value;
102                 }
103 
104                 auto _scenario = new Scenario(scenario.keyword, scenario.name.isNull
105                         ? `` : scenario.name.get, row.location, scenario.parent, false);
106                 _scenario.tags = scenario.tags;
107                 _scenario.isScenarioOutline = true;
108                 foreach (step; scenario.steps)
109                 {
110                     auto _step = step;
111                     foreach (k, v; examplesValues)
112                     {
113                         _step.text = _step.text.replace(`<` ~ k ~ `>`, v);
114                     }
115                     _scenario.steps ~= _step;
116                 }
117 
118                 result = runScenario!ModuleNames(_scenario, background);
119                 result.exampleNumber = i + 1;
120                 results ~= result;
121                 outputComments(row.location.line);
122                 formatter.tableRow(row, table, result);
123             }
124             formatter.emptyLine();
125         }
126 
127         return results;
128     }
129 
130     ///
131     ScenarioResult runScenario(ModuleNames...)(Scenario scenario, Nullable!Scenario background)
132     {
133         auto result = ScenarioResult(scenario,
134                 this.document.uri ~ `:` ~ scenario.location.line.to!string);
135 
136         if (!background.isNull)
137         {
138             Nullable!Scenario nullScenario;
139             runScenario!ModuleNames(background.get, nullScenario).stepResults.each!(
140                     r => result += r);
141             if (isFirstBackground)
142             {
143                 formatter.emptyLine();
144             }
145             this.isFirstBackground = false;
146         }
147 
148         outputComments(scenario.location.line);
149         if (!scenario.isBackground || this.isFirstBackground)
150         {
151             formatter.scenario(scenario);
152 
153             // Output failed steps in Background
154             if (!scenario.isScenarioOutline)
155             {
156                 result.stepResults
157                     .filter!(r => r.isFailed)
158                     .each!(r => formatter.step(r.step, r));
159             }
160         }
161 
162         foreach (step; scenario.steps)
163         {
164             auto stepResult = StepResult(step);
165             if (result.isPassed)
166             {
167                 stepResult = runStep!ModuleNames(step);
168             }
169             else
170             {
171                 stepResult = runStep!ModuleNames(step, true);
172                 stepResult.result = stepResult.isUndefined ? UNDEFINED : SKIPPED;
173             }
174             result += stepResult;
175 
176             outputComments(step.location.line);
177             if (!scenario.isBackground || this.isFirstBackground)
178             {
179                 formatter.step(step, stepResult);
180             }
181         }
182 
183         if (!scenario.isScenarioOutline && !scenario.isBackground)
184         {
185             formatter.emptyLine();
186         }
187 
188         return result;
189     }
190 
191     ///
192     StepResult runStep(ModuleNames...)(Step step, bool skip = false)
193     {
194 
195         auto result = StepResult(step, this.document.uri ~ `:` ~ step.location.line.to!string);
196         auto func = findMatch!ModuleNames(step.text);
197         auto sw = StopWatch(AutoStart.yes);
198 
199         if (func)
200         {
201             result.location = func.source;
202             if (skip || this.dryRun)
203             {
204                 result.result = SKIPPED;
205             }
206             else
207             {
208                 try
209                 {
210                     func();
211                 }
212                 catch (Exception e)
213                 {
214                     result.result = FAILED;
215                     result.exception = e;
216                 }
217             }
218         }
219         else
220         {
221             result.result = UNDEFINED;
222         }
223         result.time = sw.peek();
224 
225         return result;
226     }
227 
228     private void outputComments(ulong currentLine)
229     {
230         if (lineNumber > currentLine)
231         {
232             return;
233         }
234         foreach (comment; this.document.comments)
235         {
236             if (comment.location.line > lineNumber && comment.location.line < currentLine)
237             {
238                 formatter.comment(comment);
239             }
240         }
241         lineNumber = currentLine;
242     }
243 }