github.com/markfisherdeloitte/godog@v0.7.9/fmt_pretty.go (about) 1 package godog 2 3 import ( 4 "fmt" 5 "io" 6 "math" 7 "regexp" 8 "strings" 9 "unicode/utf8" 10 11 "github.com/DATA-DOG/godog/colors" 12 "github.com/DATA-DOG/godog/gherkin" 13 ) 14 15 func init() { 16 Format("pretty", "Prints every feature with runtime statuses.", prettyFunc) 17 } 18 19 func prettyFunc(suite string, out io.Writer) Formatter { 20 return &pretty{ 21 basefmt: basefmt{ 22 started: timeNowFunc(), 23 indent: 2, 24 out: out, 25 }, 26 } 27 } 28 29 var outlinePlaceholderRegexp = regexp.MustCompile("<[^>]+>") 30 31 // a built in default pretty formatter 32 type pretty struct { 33 basefmt 34 35 // currently processed 36 feature *gherkin.Feature 37 scenario *gherkin.Scenario 38 outline *gherkin.ScenarioOutline 39 40 // state 41 bgSteps int 42 totalBgSteps int 43 steps int 44 commentPos int 45 46 // whether scenario or scenario outline keyword was printed 47 scenarioKeyword bool 48 49 // outline 50 outlineSteps []*stepResult 51 outlineNumExample int 52 outlineNumExamples int 53 } 54 55 func (f *pretty) Feature(ft *gherkin.Feature, p string, c []byte) { 56 if len(f.features) != 0 { 57 // not a first feature, add a newline 58 fmt.Fprintln(f.out, "") 59 } 60 f.features = append(f.features, &feature{Path: p, Feature: ft}) 61 fmt.Fprintln(f.out, keywordAndName(ft.Keyword, ft.Name)) 62 if strings.TrimSpace(ft.Description) != "" { 63 for _, line := range strings.Split(ft.Description, "\n") { 64 fmt.Fprintln(f.out, s(f.indent)+strings.TrimSpace(line)) 65 } 66 } 67 68 f.feature = ft 69 f.scenario = nil 70 f.outline = nil 71 f.bgSteps = 0 72 f.totalBgSteps = 0 73 if ft.Background != nil { 74 f.bgSteps = len(ft.Background.Steps) 75 f.totalBgSteps = len(ft.Background.Steps) 76 } 77 } 78 79 // Node takes a gherkin node for formatting 80 func (f *pretty) Node(node interface{}) { 81 f.basefmt.Node(node) 82 83 switch t := node.(type) { 84 case *gherkin.Examples: 85 f.outlineNumExamples = len(t.TableBody) 86 f.outlineNumExample++ 87 case *gherkin.Scenario: 88 f.scenario = t 89 f.outline = nil 90 f.steps = len(t.Steps) + f.totalBgSteps 91 f.scenarioKeyword = false 92 if isEmptyScenario(t) { 93 f.printUndefinedScenario(t) 94 } 95 case *gherkin.ScenarioOutline: 96 f.outline = t 97 f.scenario = nil 98 f.outlineNumExample = -1 99 f.scenarioKeyword = false 100 if isEmptyScenario(t) { 101 f.printUndefinedScenario(t) 102 } 103 case *gherkin.TableRow: 104 f.steps = len(f.outline.Steps) + f.totalBgSteps 105 f.outlineSteps = []*stepResult{} 106 } 107 } 108 109 func keywordAndName(keyword, name string) string { 110 title := whiteb(keyword + ":") 111 if len(name) > 0 { 112 title += " " + name 113 } 114 return title 115 } 116 117 func (f *pretty) printUndefinedScenario(sc interface{}) { 118 if f.bgSteps > 0 { 119 bg := f.feature.Background 120 f.commentPos = f.longestStep(bg.Steps, f.length(bg)) 121 fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(bg.Keyword, bg.Name)) 122 123 for _, step := range bg.Steps { 124 f.bgSteps-- 125 f.printStep(step, nil, colors.Cyan) 126 } 127 } 128 129 switch t := sc.(type) { 130 case *gherkin.Scenario: 131 f.commentPos = f.longestStep(t.Steps, f.length(sc)) 132 text := s(f.indent) + keywordAndName(t.Keyword, t.Name) 133 text += s(f.commentPos-f.length(t)+1) + f.line(t.Location) 134 fmt.Fprintln(f.out, "\n"+text) 135 case *gherkin.ScenarioOutline: 136 f.commentPos = f.longestStep(t.Steps, f.length(sc)) 137 text := s(f.indent) + keywordAndName(t.Keyword, t.Name) 138 text += s(f.commentPos-f.length(t)+1) + f.line(t.Location) 139 fmt.Fprintln(f.out, "\n"+text) 140 141 for _, example := range t.Examples { 142 max := longest(example, cyan) 143 f.printExampleHeader(example, max) 144 for _, row := range example.TableBody { 145 f.printExampleRow(row, max, cyan) 146 } 147 } 148 } 149 } 150 151 // Summary sumarize the feature formatter output 152 func (f *pretty) Summary() { 153 if len(f.failed) > 0 { 154 fmt.Fprintln(f.out, "\n--- "+red("Failed steps:")+"\n") 155 for _, fail := range f.failed { 156 fmt.Fprintln(f.out, s(2)+red(fail.scenarioDesc())+black(" # "+fail.scenarioLine())) 157 fmt.Fprintln(f.out, s(4)+red(strings.TrimSpace(fail.step.Keyword)+" "+fail.step.Text)+black(" # "+fail.line())) 158 fmt.Fprintln(f.out, s(6)+red("Error: ")+redb(fmt.Sprintf("%+v", fail.err))+"\n") 159 } 160 } 161 f.basefmt.Summary() 162 } 163 164 func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) { 165 var msg string 166 var clr colors.ColorFunc 167 168 ex := outline.Examples[f.outlineNumExample] 169 example, hasExamples := examples(ex) 170 if !hasExamples { 171 // do not print empty examples 172 return 173 } 174 175 firstExample := f.outlineNumExamples == len(example.TableBody) 176 printSteps := firstExample && f.outlineNumExample == 0 177 178 for i, res := range f.outlineSteps { 179 // determine example row status 180 switch { 181 case res.typ == failed: 182 msg = res.err.Error() 183 clr = res.typ.clr() 184 case res.typ == undefined || res.typ == pending: 185 clr = res.typ.clr() 186 case res.typ == skipped && clr == nil: 187 clr = cyan 188 } 189 if printSteps && i >= f.totalBgSteps { 190 // in first example, we need to print steps 191 var text string 192 ostep := outline.Steps[i-f.totalBgSteps] 193 if res.def != nil { 194 if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 { 195 var pos int 196 for i := 0; i < len(m); i++ { 197 pair := m[i] 198 text += cyan(ostep.Text[pos:pair[0]]) 199 text += cyanb(ostep.Text[pair[0]:pair[1]]) 200 pos = pair[1] 201 } 202 text += cyan(ostep.Text[pos:len(ostep.Text)]) 203 } else { 204 text = cyan(ostep.Text) 205 } 206 text += s(f.commentPos-f.length(ostep)+1) + black(fmt.Sprintf("# %s", res.def.definitionID())) 207 } else { 208 text = cyan(ostep.Text) 209 } 210 // print the step outline 211 fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(ostep.Keyword))+" "+text) 212 213 // print step argument 214 // @TODO: need to make example header cells bold 215 switch t := ostep.Argument.(type) { 216 case *gherkin.DataTable: 217 f.printTable(t, cyan) 218 case *gherkin.DocString: 219 var ct string 220 if len(t.ContentType) > 0 { 221 ct = " " + cyan(t.ContentType) 222 } 223 fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter)+ct) 224 for _, ln := range strings.Split(t.Content, "\n") { 225 fmt.Fprintln(f.out, s(f.indent*3)+cyan(ln)) 226 } 227 fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter)) 228 } 229 } 230 } 231 232 if clr == nil { 233 clr = green 234 } 235 236 max := longest(example, clr, cyan) 237 // an example table header 238 if firstExample { 239 f.printExampleHeader(example, max) 240 } 241 242 // an example table row 243 row := example.TableBody[len(example.TableBody)-f.outlineNumExamples] 244 f.printExampleRow(row, max, clr) 245 246 // if there is an error 247 if msg != "" { 248 fmt.Fprintln(f.out, s(f.indent*4)+redb(msg)) 249 } 250 } 251 252 func (f *pretty) printExampleRow(row *gherkin.TableRow, max []int, clr colors.ColorFunc) { 253 cells := make([]string, len(row.Cells)) 254 for i, cell := range row.Cells { 255 val := clr(cell.Value) 256 ln := utf8.RuneCountInString(val) 257 cells[i] = val + s(max[i]-ln) 258 } 259 fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |") 260 } 261 262 func (f *pretty) printExampleHeader(example *gherkin.Examples, max []int) { 263 cells := make([]string, len(example.TableHeader.Cells)) 264 // an example table header 265 fmt.Fprintln(f.out, "") 266 fmt.Fprintln(f.out, s(f.indent*2)+keywordAndName(example.Keyword, example.Name)) 267 268 for i, cell := range example.TableHeader.Cells { 269 val := cyan(cell.Value) 270 ln := utf8.RuneCountInString(val) 271 cells[i] = val + s(max[i]-ln) 272 } 273 fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |") 274 } 275 276 func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c colors.ColorFunc) { 277 text := s(f.indent*2) + c(strings.TrimSpace(step.Keyword)) + " " 278 switch { 279 case def != nil: 280 if m := def.Expr.FindStringSubmatchIndex(step.Text)[2:]; len(m) > 0 { 281 var pos, i int 282 for pos, i = 0, 0; i < len(m); i++ { 283 if m[i] == -1 { 284 continue // no index for this match 285 } 286 if math.Mod(float64(i), 2) == 0 { 287 text += c(step.Text[pos:m[i]]) 288 } else { 289 text += colors.Bold(c)(step.Text[pos:m[i]]) 290 } 291 pos = m[i] 292 } 293 text += c(step.Text[pos:len(step.Text)]) 294 } else { 295 text += c(step.Text) 296 } 297 text += s(f.commentPos-f.length(step)+1) + black(fmt.Sprintf("# %s", def.definitionID())) 298 default: 299 text += c(step.Text) 300 } 301 302 fmt.Fprintln(f.out, text) 303 switch t := step.Argument.(type) { 304 case *gherkin.DataTable: 305 f.printTable(t, c) 306 case *gherkin.DocString: 307 var ct string 308 if len(t.ContentType) > 0 { 309 ct = " " + c(t.ContentType) 310 } 311 fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter)+ct) 312 for _, ln := range strings.Split(t.Content, "\n") { 313 fmt.Fprintln(f.out, s(f.indent*3)+c(ln)) 314 } 315 fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter)) 316 } 317 } 318 319 func (f *pretty) printStepKind(res *stepResult) { 320 f.steps-- 321 if f.outline != nil { 322 f.outlineSteps = append(f.outlineSteps, res) 323 } 324 var bgStep bool 325 bg := f.feature.Background 326 327 // if has not printed background yet 328 switch { 329 // first background step 330 case f.bgSteps > 0 && f.bgSteps == len(bg.Steps): 331 f.commentPos = f.longestStep(bg.Steps, f.length(bg)) 332 fmt.Fprintln(f.out, "\n"+s(f.indent)+keywordAndName(bg.Keyword, bg.Name)) 333 f.bgSteps-- 334 bgStep = true 335 // subsequent background steps 336 case f.bgSteps > 0: 337 f.bgSteps-- 338 bgStep = true 339 // first step of scenario, print header and calculate comment position 340 case f.scenario != nil: 341 // print scenario keyword and value if first example 342 if !f.scenarioKeyword { 343 f.commentPos = f.longestStep(f.scenario.Steps, f.length(f.scenario)) 344 if bg != nil { 345 if bgLen := f.longestStep(bg.Steps, f.length(bg)); bgLen > f.commentPos { 346 f.commentPos = bgLen 347 } 348 } 349 text := s(f.indent) + keywordAndName(f.scenario.Keyword, f.scenario.Name) 350 text += s(f.commentPos-f.length(f.scenario)+1) + f.line(f.scenario.Location) 351 fmt.Fprintln(f.out, "\n"+text) 352 f.scenarioKeyword = true 353 } 354 // first step of outline scenario, print header and calculate comment position 355 case f.outline != nil: 356 // print scenario keyword and value if first example 357 if !f.scenarioKeyword { 358 f.commentPos = f.longestStep(f.outline.Steps, f.length(f.outline)) 359 if bg != nil { 360 if bgLen := f.longestStep(bg.Steps, f.length(bg)); bgLen > f.commentPos { 361 f.commentPos = bgLen 362 } 363 } 364 text := s(f.indent) + keywordAndName(f.outline.Keyword, f.outline.Name) 365 text += s(f.commentPos-f.length(f.outline)+1) + f.line(f.outline.Location) 366 fmt.Fprintln(f.out, "\n"+text) 367 f.scenarioKeyword = true 368 } 369 if len(f.outlineSteps) == len(f.outline.Steps)+f.totalBgSteps { 370 // an outline example steps has went through 371 f.printOutlineExample(f.outline) 372 f.outlineNumExamples-- 373 } 374 return 375 } 376 377 if !f.isBackgroundStep(res.step) || bgStep { 378 f.printStep(res.step, res.def, res.typ.clr()) 379 } 380 if res.err != nil { 381 fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", res.err))) 382 } 383 if res.typ == pending { 384 fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition")) 385 } 386 } 387 388 func (f *pretty) isBackgroundStep(step *gherkin.Step) bool { 389 if f.feature.Background == nil { 390 return false 391 } 392 393 for _, bstep := range f.feature.Background.Steps { 394 if bstep.Location.Line == step.Location.Line { 395 return true 396 } 397 } 398 return false 399 } 400 401 // print table with aligned table cells 402 func (f *pretty) printTable(t *gherkin.DataTable, c colors.ColorFunc) { 403 var l = longest(t, c) 404 var cols = make([]string, len(t.Rows[0].Cells)) 405 for _, row := range t.Rows { 406 for i, cell := range row.Cells { 407 val := c(cell.Value) 408 ln := utf8.RuneCountInString(val) 409 cols[i] = val + s(l[i]-ln) 410 } 411 fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cols, " | ")+" |") 412 } 413 } 414 415 func (f *pretty) Passed(step *gherkin.Step, match *StepDef) { 416 f.basefmt.Passed(step, match) 417 f.printStepKind(f.passed[len(f.passed)-1]) 418 } 419 420 func (f *pretty) Skipped(step *gherkin.Step, match *StepDef) { 421 f.basefmt.Skipped(step, match) 422 f.printStepKind(f.skipped[len(f.skipped)-1]) 423 } 424 425 func (f *pretty) Undefined(step *gherkin.Step, match *StepDef) { 426 f.basefmt.Undefined(step, match) 427 f.printStepKind(f.undefined[len(f.undefined)-1]) 428 } 429 430 func (f *pretty) Failed(step *gherkin.Step, match *StepDef, err error) { 431 f.basefmt.Failed(step, match, err) 432 f.printStepKind(f.failed[len(f.failed)-1]) 433 } 434 435 func (f *pretty) Pending(step *gherkin.Step, match *StepDef) { 436 f.basefmt.Pending(step, match) 437 f.printStepKind(f.pending[len(f.pending)-1]) 438 } 439 440 // longest gives a list of longest columns of all rows in Table 441 func longest(tbl interface{}, clrs ...colors.ColorFunc) []int { 442 var rows []*gherkin.TableRow 443 switch t := tbl.(type) { 444 case *gherkin.Examples: 445 rows = append(rows, t.TableHeader) 446 rows = append(rows, t.TableBody...) 447 case *gherkin.DataTable: 448 rows = append(rows, t.Rows...) 449 } 450 451 longest := make([]int, len(rows[0].Cells)) 452 for _, row := range rows { 453 for i, cell := range row.Cells { 454 for _, c := range clrs { 455 ln := utf8.RuneCountInString(c(cell.Value)) 456 if longest[i] < ln { 457 longest[i] = ln 458 } 459 } 460 461 ln := utf8.RuneCountInString(cell.Value) 462 if longest[i] < ln { 463 longest[i] = ln 464 } 465 } 466 } 467 return longest 468 } 469 470 func (f *pretty) longestStep(steps []*gherkin.Step, base int) int { 471 ret := base 472 for _, step := range steps { 473 length := f.length(step) 474 if length > ret { 475 ret = length 476 } 477 } 478 return ret 479 } 480 481 // a line number representation in feature file 482 func (f *pretty) line(loc *gherkin.Location) string { 483 return black(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line)) 484 } 485 486 func (f *pretty) length(node interface{}) int { 487 switch t := node.(type) { 488 case *gherkin.Background: 489 return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name) 490 case *gherkin.Step: 491 return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+" "+t.Text) 492 case *gherkin.Scenario: 493 return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name) 494 case *gherkin.ScenarioOutline: 495 return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name) 496 } 497 panic(fmt.Sprintf("unexpected node %T to determine length", node)) 498 }