1 module cucumber.formatter.pretty; 2 3 import std.algorithm : max; 4 import std.algorithm.iteration : 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, split; 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 if (scenario.isScenarioOutline) 49 { 50 return; 51 } 52 base(scenario, " "); 53 } 54 55 /// 56 override void scenarioOutline(ref Scenario scenario) 57 { 58 base(scenario, " "); 59 foreach (step; scenario.steps) 60 { 61 this.step(step, SKIPPED, 62 scenario.parent.parent.uri ~ `:` ~ step.location.line.to!string); 63 } 64 writeln; 65 } 66 67 /// 68 override void examples(Examples examples) 69 { 70 base(examples, " "); 71 } 72 73 /// 74 override void tableRow(TableRow tableRow, ref TableRow[] table, string resultColor) 75 { 76 if (this.table != table) 77 { 78 setCellSize(table); 79 } 80 writeln(" ", justifyCells(tableRow.cells, resultColor)); 81 } 82 83 /// 84 override void tableRow(TableRow tableRow, ref TableRow[] table, ScenarioResult scenarioResult) 85 { 86 with (scenarioResult) 87 { 88 this.tableRow(tableRow, table, 89 (stepResults.filter!(r => r.isUndefined).empty) ? result : UNDEFINED); 90 foreach (stepResult; stepResults) 91 { 92 error(stepResult.exception, " "); 93 } 94 } 95 } 96 97 /// 98 override void step(Step step, StepResult stepResult) 99 { 100 if (step.parent.isScenarioOutline) 101 { 102 return; 103 } 104 105 this.step(step, stepResult.result, stepResult.location); 106 if (!step.docString.isNull) 107 { 108 with (step.docString.get) 109 { 110 writeln(" ", color(stepResult.result), delimiter); 111 writeln(content.split("\n").map!(x => " " ~ x).array.join("\n")); 112 writeln(" ", delimiter, color("reset")); 113 } 114 } 115 if (!step.dataTable.isNull) 116 { 117 foreach (row; step.dataTable.get.rows) 118 { 119 tableRow(row, step.dataTable.get.rows, SKIPPED); 120 } 121 } 122 if (stepResult.isFailed) 123 { 124 error(stepResult.exception, " "); 125 } 126 } 127 128 /// 129 override void emptyLine() 130 { 131 writeln; 132 } 133 134 /// 135 override void comment(Comment comment) 136 { 137 writeln(comment.text); 138 } 139 140 /// 141 override void summarizeResult(RunResult result) 142 { 143 struct FailingScenario 144 { 145 string text; 146 string source; 147 } 148 149 FailingScenario[] failingScenarios; 150 ulong maxLength; 151 foreach (featureResult; result.featureResults) 152 { 153 foreach (scenarioResult; featureResult.scenarioResults.filter!(x => x.isFailed)) 154 { 155 auto text = "cucumber " ~ featureResult.feature.parent.uri ~ ":" 156 ~ scenarioResult.scenario.location.line.to!string; 157 auto source = scenarioResult.scenario.keyword ~ `: ` ~ (scenarioResult.scenario.name.isNull 158 ? `` : scenarioResult.scenario.name.get); 159 if (!scenarioResult.exampleNumber.isNull) 160 { 161 source ~= ", Examples (#" ~ scenarioResult.exampleNumber.get.to!string ~ `)`; 162 } 163 164 auto failingScenario = FailingScenario(text, source); 165 maxLength = max(maxLength, failingScenario.text.walkLength); 166 failingScenarios ~= failingScenario; 167 } 168 } 169 170 if (!failingScenarios.empty) 171 { 172 writeln(color("failed"), "Failing Scenarios:", color("reset")); 173 174 foreach (failingScenario; failingScenarios) 175 { 176 if (noSource) 177 { 178 writeln(color("failed"), failingScenario.text, color("reset")); 179 } 180 else 181 { 182 writeln(color("failed"), leftJustifier(failingScenario.text, maxLength + 1), 183 color("reset"), color("gray"), "# ", 184 failingScenario.source, color("reset")); 185 } 186 } 187 writeln; 188 } 189 runResult(result); 190 } 191 192 private: 193 194 Scenario runningScenario; 195 TableRow[] table; 196 ulong scenarioStringLength; 197 ulong[] cellSizes; 198 199 void base(T)(T element, string extraIndent = "") 200 { 201 with (element) 202 { 203 if (!tags.empty) 204 { 205 writeln(extraIndent, color("skipped"), tags.map!(t => t.name) 206 .join(" "), color("reset")); 207 } 208 string line = extraIndent ~ keyword ~ ": "; 209 static if (is(typeof(name) == Nullable!string)) 210 { 211 line ~= name.isNull ? `` : name.get; 212 } 213 else static if (is(typeof(element) == Step)) 214 { 215 line ~= text; 216 } 217 else 218 { 219 line ~= name; 220 } 221 static if (is(typeof(element) == Scenario)) 222 { 223 setScenarioStringLength(element); 224 if (noSource) 225 { 226 write(line); 227 } 228 else 229 { 230 write(leftJustifier(line, scenarioStringLength + 4)); 231 write(color("gray"), " # ", element.parent.parent.uri, `:`, 232 element.location.line.to!string, color("reset")); 233 } 234 } 235 else 236 { 237 write(line); 238 } 239 writeln; 240 if (!description.isNull) 241 { 242 writeln(extraIndent, description); 243 } 244 } 245 } 246 247 void step(Step step, string resultColor, string location) 248 { 249 setScenarioStringLength(step.parent); 250 if (noSource) 251 { 252 write(" ", color(resultColor), step.keyword ~ step.text, color("reset")); 253 } 254 else 255 { 256 write(" ", color(resultColor), leftJustifier(step.keyword ~ step.text, 257 scenarioStringLength), color("reset")); 258 write(color("gray"), " # ", location, color("reset")); 259 } 260 writeln; 261 } 262 263 void setScenarioStringLength(ref Scenario scenario) 264 { 265 if (this.runningScenario == scenario) 266 { 267 return; 268 } 269 scenarioStringLength = scenario.keyword.walkLength + (scenario.name.isNull 270 ? 0 : scenario.name.get.walkLength); 271 foreach (step; scenario.steps) 272 { 273 auto stepStringLength = (step.keyword ~ (step.text)).walkLength; 274 scenarioStringLength = max(scenarioStringLength, stepStringLength); 275 } 276 this.runningScenario = scenario; 277 } 278 279 void setCellSize(TableRow[] rows) 280 { 281 this.table = rows; 282 cellSizes = 0LU.repeat(rows.length).array; 283 foreach (i, row; rows) 284 { 285 foreach (j, cell; row.cells) 286 { 287 if (!cell.value.empty) 288 { 289 cellSizes[j] = max(cellSizes[j], cell.value.walkLength); 290 } 291 } 292 } 293 } 294 295 string justifyCells(Cell[] cells, string resultColor) 296 { 297 string[] cellStrings; 298 foreach (i, cell; cells) 299 { 300 string cellString; 301 if (!cell.value.empty) 302 { 303 cellString = color(resultColor) ~ 304 cell.value.leftJustifier(cellSizes[i]).to!string ~ color("reset"); 305 } 306 cellStrings ~= cellString; 307 } 308 return "| " ~ cellStrings.join(" | ") ~ " |"; 309 } 310 311 void error(Nullable!Exception exception, string extraIndent = "") 312 { 313 if (exception.isNull) 314 { 315 return; 316 } 317 string message = exception.get.message.split("\n") 318 .map!(l => extraIndent ~ l).array.join("\n"); 319 writefln("%s%s%s", color("failed"), message, color("reset")); 320 } 321 }