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 }