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 }