1 module cucumber.formatter.pretty; 2 3 import std.algorithm : max; 4 import std.algorithm.iteration : each, 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, replace, split, stripLeft; 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 writeln(); 49 base(scenario, " "); 50 } 51 52 /// 53 override void examples(Examples examples) 54 { 55 writeln(); 56 base(examples, " "); 57 } 58 59 /// 60 override void tableRow(TableRow tableRow, ref TableRow[] table, string resultColor) 61 { 62 if (this.table != table) 63 { 64 setCellSize(table); 65 } 66 writeln(" ", justifyCells(tableRow.cells, resultColor)); 67 } 68 69 /// 70 override void tableRow(TableRow tableRow, ref TableRow[] table, ScenarioResult scenarioResult) 71 { 72 tableRow.comments.each!(c => comment(c, " ")); 73 with (scenarioResult) 74 { 75 this.tableRow(tableRow, table, 76 (stepResults.filter!(r => r.isUndefined).empty) ? result : UNDEFINED); 77 foreach (stepResult; stepResults) 78 { 79 error(stepResult.exception, " "); 80 } 81 } 82 } 83 84 /// 85 override void step(Step step, StepResult stepResult) 86 { 87 this.step(step, stepResult.result, stepResult.location); 88 89 if (stepResult.isFailed) 90 { 91 error(stepResult.exception, " "); 92 } 93 } 94 95 /// 96 override void comment(Comment comment) 97 { 98 writeln(comment.text); 99 } 100 101 /// 102 override void summarizeResult(RunResult result) 103 { 104 struct FailingScenario 105 { 106 string text; 107 string source; 108 } 109 110 FailingScenario[] failingScenarios; 111 ulong maxLength; 112 foreach (featureResult; result.featureResults) 113 { 114 foreach (scenarioResult; featureResult.scenarioResults.filter!(x => x.isFailed)) 115 { 116 auto text = "cucumber " ~ featureResult.feature.uri ~ ":" 117 ~ scenarioResult.scenario.location.line.to!string; 118 auto source = scenarioResult.scenario.keyword ~ `: ` ~ ( 119 scenarioResult.scenario.name); 120 if (!scenarioResult.exampleNumber > 0) 121 { 122 source ~= ", Examples (#" ~ scenarioResult.exampleNumber.to!string ~ `)`; 123 } 124 125 auto failingScenario = FailingScenario(text, source); 126 maxLength = max(maxLength, failingScenario.text.walkLength); 127 failingScenarios ~= failingScenario; 128 } 129 } 130 131 if (!failingScenarios.empty) 132 { 133 writeln; 134 writeln(color("failed"), "Failing Scenarios:", color("reset")); 135 136 foreach (failingScenario; failingScenarios) 137 { 138 if (noSource) 139 { 140 writeln(color("failed"), failingScenario.text, color("reset")); 141 } 142 else 143 { 144 writeln(color("failed"), leftJustifier(failingScenario.text, maxLength + 1), 145 color("reset"), color("gray"), "# ", 146 failingScenario.source, color("reset")); 147 } 148 } 149 } 150 151 if (!result.featureResults.empty) 152 { 153 writeln; 154 } 155 runResult(result, noSource); 156 } 157 158 private: 159 160 Scenario runningScenario; 161 TableRow[] table; 162 ulong scenarioStringLength; 163 ulong[] cellSizes; 164 165 void comment(Comment comment, string extraIndent = "") 166 { 167 writeln(extraIndent ~ comment.text.stripLeft); 168 } 169 170 void base(T)(T element, string extraIndent = "") 171 { 172 with (element) 173 { 174 comments.each!(c => comment(c, extraIndent)); 175 if (!tags.empty) 176 { 177 writeln(extraIndent, color("skipped"), tags.map!(t => t.name) 178 .join(" "), color("reset")); 179 } 180 string line = extraIndent ~ keyword ~ ": "; 181 static if (is(typeof(element) == Step)) 182 { 183 line ~= text; 184 } 185 else 186 { 187 line ~= name; 188 } 189 static if (is(typeof(element) == Scenario)) 190 { 191 setScenarioStringLength(element); 192 if (noSource) 193 { 194 write(line); 195 } 196 else 197 { 198 write(leftJustifier(line, scenarioStringLength + 4)); 199 write(color("gray"), " # ", element.uri, `:`, 200 element.location.line.to!string, color("reset")); 201 } 202 } 203 else 204 { 205 write(line); 206 } 207 writeln; 208 209 if (!description.empty) 210 { 211 writeln(description); 212 } 213 } 214 } 215 216 void step(Step step, string resultColor, string location) 217 { 218 step.comments.each!(c => comment(c, " ")); 219 if (noSource) 220 { 221 write(" ", color(resultColor), step.keyword ~ step.text, color("reset")); 222 } 223 else 224 { 225 write(" ", color(resultColor), leftJustifier(step.keyword ~ step.text, 226 scenarioStringLength), color("reset")); 227 write(color("gray"), " # ", location, color("reset")); 228 } 229 writeln; 230 if (!step.docString.isNull) 231 { 232 with (step.docString.get) 233 { 234 writeln(" ", color(resultColor), `"""`); 235 writeln(content.split("\n").map!(x => x.empty ? `` : (" " ~ x)).array.join( 236 "\n")); 237 writeln(" ", `"""`, color("reset")); 238 } 239 } 240 if (!step.dataTable.empty) 241 { 242 foreach (row; step.dataTable.rows) 243 { 244 tableRow(row, step.dataTable.rows, SKIPPED); 245 } 246 } 247 } 248 249 void setScenarioStringLength(ref Scenario scenario) 250 { 251 if (this.runningScenario == scenario) 252 { 253 return; 254 } 255 scenarioStringLength = scenario.keyword.walkLength + scenario.name.walkLength; 256 foreach (step; scenario.steps) 257 { 258 auto stepStringLength = (step.keyword ~ (step.text)).walkLength; 259 scenarioStringLength = max(scenarioStringLength, stepStringLength); 260 } 261 this.runningScenario = scenario; 262 } 263 264 void setCellSize(TableRow[] rows) 265 { 266 import std.regex : ctRegex, regexReplace = replace; 267 268 if (rows.empty) 269 { 270 return; 271 } 272 this.table = rows; 273 cellSizes = 0LU.repeat(rows[0].cells.length).array; 274 foreach (i, row; rows) 275 { 276 foreach (j, cell; row.cells) 277 { 278 if (!cell.value.empty) 279 { 280 auto value = cell.value.replace('\\', `\\`).replace("\n", 281 `\n`).regexReplace(ctRegex!(`(([^\\])\||^\|)`), `$2\|`); 282 cellSizes[j] = max(cellSizes[j], value.walkLength); 283 } 284 } 285 } 286 } 287 288 string justifyCells(Cell[] cells, string resultColor) 289 { 290 import std.regex : ctRegex, regexReplace = replace; 291 292 string[] cellStrings; 293 foreach (i, cell; cells) 294 { 295 string cellString; 296 if (!cell.value.empty) 297 { 298 auto value = cell.value.replace('\\', `\\`).replace("\n", `\n`) 299 .regexReplace(ctRegex!(`(([^\\])\||^\|)`), `$2\|`); 300 cellString = color(resultColor) ~ value.leftJustifier(cellSizes[i]) 301 .to!string ~ color("reset"); 302 } 303 cellStrings ~= cellString; 304 } 305 return "| " ~ cellStrings.join(" | ") ~ " |"; 306 } 307 308 void error(Nullable!Exception exception, string extraIndent = "") 309 { 310 if (exception.isNull) 311 { 312 return; 313 } 314 string message = exception.get.message.split("\n") 315 .map!(l => extraIndent ~ l).array.join("\n"); 316 writefln("%s%s%s", color("failed"), message, color("reset")); 317 } 318 } 319 320 unittest 321 { 322 import std.algorithm : canFind; 323 import std.file : readText; 324 import std.path : baseName; 325 import std.stdio : File, stdout; 326 import unit_threaded.assertions : should; 327 import cucumber.runner : CucumberRunner; 328 import gherkin.util : getFeatureFiles; 329 import gherkin.parser : Parser; 330 331 const auto ignoredFeatureFiles = [ 332 // dfmt off 333 "several_examples", // tags over Examples 334 "tags", // tags over Examples 335 "complex_background", // Rule included 336 "i18n_emoji", 337 "i18n_fr", 338 "i18n_no", 339 "minimal-example", // Example (related to Rule) included 340 "padded_example", // Cucumber-Ruby Scenario Outline issue 341 "rule", 342 "rule_without_name_and_description", 343 "scenario_outline", // Cucumber-Ruby Scenario Outline issue 344 "spaces_in_language", 345 // dfmt on 346 ]; 347 348 foreach (featureFile; getFeatureFiles([ 349 ``, "gherkin-d/cucumber/gherkin/testdata/good/" 350 ])) 351 { 352 if (ignoredFeatureFiles.canFind(baseName(featureFile, ".feature"))) 353 { 354 continue; 355 } 356 auto gherkinDocument = Parser.parseFromFile(featureFile); 357 auto formatter = new Pretty(true, true, true); 358 auto runner = new CucumberRunner(formatter, true); 359 360 auto original = stdout; 361 stdout.open("cucumber-d_formatter_pretty.out", "wt"); 362 RunResult result; 363 result += runner.runFeature!"cucumber.formatter.pretty"(gherkinDocument); 364 formatter.summarizeResult(result); 365 stdout = original; 366 367 const auto expected = readText("testdata/formatter/pretty/" ~ baseName(featureFile)); 368 const auto actual = readText("cucumber-d_formatter_pretty.out"); 369 actual.should == expected; 370 371 } 372 }