github.com/maps90/godog@v0.7.5-0.20170923143419-0093943021d4/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, whiteb(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 case *gherkin.ScenarioOutline: 93 f.outline = t 94 f.scenario = nil 95 f.outlineNumExample = -1 96 f.scenarioKeyword = false 97 case *gherkin.TableRow: 98 f.steps = len(f.outline.Steps) + f.totalBgSteps 99 f.outlineSteps = []*stepResult{} 100 } 101 } 102 103 // Summary sumarize the feature formatter output 104 func (f *pretty) Summary() { 105 // failed steps on background are not scenarios 106 var failedScenarios []*stepResult 107 for _, fail := range f.failed { 108 switch fail.owner.(type) { 109 case *gherkin.Scenario: 110 failedScenarios = append(failedScenarios, fail) 111 case *gherkin.ScenarioOutline: 112 failedScenarios = append(failedScenarios, fail) 113 } 114 } 115 if len(failedScenarios) > 0 { 116 fmt.Fprintln(f.out, "\n--- "+red("Failed scenarios:")+"\n") 117 var unique []string 118 for _, fail := range failedScenarios { 119 var found bool 120 for _, in := range unique { 121 if in == fail.line() { 122 found = true 123 break 124 } 125 } 126 if !found { 127 unique = append(unique, fail.line()) 128 } 129 } 130 131 for _, fail := range unique { 132 fmt.Fprintln(f.out, " "+red(fail)) 133 } 134 } 135 f.basefmt.Summary() 136 } 137 138 func (f *pretty) printOutlineExample(outline *gherkin.ScenarioOutline) { 139 var msg string 140 var clr colors.ColorFunc 141 142 ex := outline.Examples[f.outlineNumExample] 143 example, hasExamples := examples(ex) 144 if !hasExamples { 145 // do not print empty examples 146 return 147 } 148 149 firstExample := f.outlineNumExamples == len(example.TableBody) 150 printSteps := firstExample && f.outlineNumExample == 0 151 152 for i, res := range f.outlineSteps { 153 // determine example row status 154 switch { 155 case res.typ == failed: 156 msg = res.err.Error() 157 clr = res.typ.clr() 158 case res.typ == undefined || res.typ == pending: 159 clr = res.typ.clr() 160 case res.typ == skipped && clr == nil: 161 clr = cyan 162 } 163 if printSteps && i >= f.totalBgSteps { 164 // in first example, we need to print steps 165 var text string 166 ostep := outline.Steps[i-f.totalBgSteps] 167 if res.def != nil { 168 if m := outlinePlaceholderRegexp.FindAllStringIndex(ostep.Text, -1); len(m) > 0 { 169 var pos int 170 for i := 0; i < len(m); i++ { 171 pair := m[i] 172 text += cyan(ostep.Text[pos:pair[0]]) 173 text += cyanb(ostep.Text[pair[0]:pair[1]]) 174 pos = pair[1] 175 } 176 text += cyan(ostep.Text[pos:len(ostep.Text)]) 177 } else { 178 text = cyan(ostep.Text) 179 } 180 text += s(f.commentPos-f.length(ostep)+1) + black(fmt.Sprintf("# %s", res.def.definitionID())) 181 } else { 182 text = cyan(ostep.Text) 183 } 184 // print the step outline 185 fmt.Fprintln(f.out, s(f.indent*2)+cyan(strings.TrimSpace(ostep.Keyword))+" "+text) 186 187 // print step argument 188 // @TODO: need to make example header cells bold 189 switch t := ostep.Argument.(type) { 190 case *gherkin.DataTable: 191 f.printTable(t, cyan) 192 case *gherkin.DocString: 193 var ct string 194 if len(t.ContentType) > 0 { 195 ct = " " + cyan(t.ContentType) 196 } 197 fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter)+ct) 198 for _, ln := range strings.Split(t.Content, "\n") { 199 fmt.Fprintln(f.out, s(f.indent*3)+cyan(ln)) 200 } 201 fmt.Fprintln(f.out, s(f.indent*3)+cyan(t.Delimitter)) 202 } 203 } 204 } 205 206 if clr == nil { 207 clr = green 208 } 209 cells := make([]string, len(example.TableHeader.Cells)) 210 max := longest(example, clr, cyan) 211 // an example table header 212 if firstExample { 213 fmt.Fprintln(f.out, "") 214 fmt.Fprintln(f.out, s(f.indent*2)+whiteb(example.Keyword+": ")+example.Name) 215 216 for i, cell := range example.TableHeader.Cells { 217 val := cyan(cell.Value) 218 ln := utf8.RuneCountInString(val) 219 cells[i] = val + s(max[i]-ln) 220 } 221 fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |") 222 } 223 224 // an example table row 225 row := example.TableBody[len(example.TableBody)-f.outlineNumExamples] 226 for i, cell := range row.Cells { 227 val := clr(cell.Value) 228 ln := utf8.RuneCountInString(val) 229 cells[i] = val + s(max[i]-ln) 230 } 231 fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cells, " | ")+" |") 232 233 // if there is an error 234 if msg != "" { 235 fmt.Fprintln(f.out, s(f.indent*4)+redb(msg)) 236 } 237 } 238 239 func (f *pretty) printStep(step *gherkin.Step, def *StepDef, c colors.ColorFunc) { 240 text := s(f.indent*2) + c(strings.TrimSpace(step.Keyword)) + " " 241 switch { 242 case def != nil: 243 if m := def.Expr.FindStringSubmatchIndex(step.Text)[2:]; len(m) > 0 { 244 var pos, i int 245 for pos, i = 0, 0; i < len(m); i++ { 246 if m[i] == -1 { 247 continue // no index for this match 248 } 249 if math.Mod(float64(i), 2) == 0 { 250 text += c(step.Text[pos:m[i]]) 251 } else { 252 text += colors.Bold(c)(step.Text[pos:m[i]]) 253 } 254 pos = m[i] 255 } 256 text += c(step.Text[pos:len(step.Text)]) 257 } else { 258 text += c(step.Text) 259 } 260 text += s(f.commentPos-f.length(step)+1) + black(fmt.Sprintf("# %s", def.definitionID())) 261 default: 262 text += c(step.Text) 263 } 264 265 fmt.Fprintln(f.out, text) 266 switch t := step.Argument.(type) { 267 case *gherkin.DataTable: 268 f.printTable(t, c) 269 case *gherkin.DocString: 270 var ct string 271 if len(t.ContentType) > 0 { 272 ct = " " + c(t.ContentType) 273 } 274 fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter)+ct) 275 for _, ln := range strings.Split(t.Content, "\n") { 276 fmt.Fprintln(f.out, s(f.indent*3)+c(ln)) 277 } 278 fmt.Fprintln(f.out, s(f.indent*3)+c(t.Delimitter)) 279 } 280 } 281 282 func (f *pretty) printStepKind(res *stepResult) { 283 f.steps-- 284 if f.outline != nil { 285 f.outlineSteps = append(f.outlineSteps, res) 286 } 287 288 // if has not printed background yet 289 switch { 290 // first background step 291 case f.bgSteps > 0 && f.bgSteps == len(f.feature.Background.Steps): 292 f.commentPos = f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background)) 293 fmt.Fprintln(f.out, "\n"+s(f.indent)+whiteb(f.feature.Background.Keyword+": "+f.feature.Background.Name)) 294 f.bgSteps-- 295 // subsequent background steps 296 case f.bgSteps > 0: 297 f.bgSteps-- 298 // first step of scenario, print header and calculate comment position 299 case f.scenario != nil: 300 // print scenario keyword and value if first example 301 if !f.scenarioKeyword { 302 f.commentPos = f.longestStep(f.scenario.Steps, f.length(f.scenario)) 303 if f.feature.Background != nil { 304 if bgLen := f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background)); bgLen > f.commentPos { 305 f.commentPos = bgLen 306 } 307 } 308 text := s(f.indent) + whiteb(f.scenario.Keyword+": ") + f.scenario.Name 309 text += s(f.commentPos-f.length(f.scenario)+1) + f.line(f.scenario.Location) 310 fmt.Fprintln(f.out, "\n"+text) 311 f.scenarioKeyword = true 312 } 313 // first step of outline scenario, print header and calculate comment position 314 case f.outline != nil: 315 // print scenario keyword and value if first example 316 if !f.scenarioKeyword { 317 f.commentPos = f.longestStep(f.outline.Steps, f.length(f.outline)) 318 if f.feature.Background != nil { 319 if bgLen := f.longestStep(f.feature.Background.Steps, f.length(f.feature.Background)); bgLen > f.commentPos { 320 f.commentPos = bgLen 321 } 322 } 323 text := s(f.indent) + whiteb(f.outline.Keyword+": ") + f.outline.Name 324 text += s(f.commentPos-f.length(f.outline)+1) + f.line(f.outline.Location) 325 fmt.Fprintln(f.out, "\n"+text) 326 f.scenarioKeyword = true 327 } 328 if len(f.outlineSteps) == len(f.outline.Steps)+f.totalBgSteps { 329 // an outline example steps has went through 330 f.printOutlineExample(f.outline) 331 f.outlineNumExamples-- 332 } 333 return 334 } 335 336 f.printStep(res.step, res.def, res.typ.clr()) 337 if res.err != nil { 338 fmt.Fprintln(f.out, s(f.indent*2)+redb(fmt.Sprintf("%+v", res.err))) 339 } 340 if res.typ == pending { 341 fmt.Fprintln(f.out, s(f.indent*3)+yellow("TODO: write pending definition")) 342 } 343 } 344 345 // print table with aligned table cells 346 func (f *pretty) printTable(t *gherkin.DataTable, c colors.ColorFunc) { 347 var l = longest(t, c) 348 var cols = make([]string, len(t.Rows[0].Cells)) 349 for _, row := range t.Rows { 350 for i, cell := range row.Cells { 351 val := c(cell.Value) 352 ln := utf8.RuneCountInString(val) 353 cols[i] = val + s(l[i]-ln) 354 } 355 fmt.Fprintln(f.out, s(f.indent*3)+"| "+strings.Join(cols, " | ")+" |") 356 } 357 } 358 359 func (f *pretty) Passed(step *gherkin.Step, match *StepDef) { 360 f.basefmt.Passed(step, match) 361 f.printStepKind(f.passed[len(f.passed)-1]) 362 } 363 364 func (f *pretty) Skipped(step *gherkin.Step, match *StepDef) { 365 f.basefmt.Skipped(step, match) 366 f.printStepKind(f.skipped[len(f.skipped)-1]) 367 } 368 369 func (f *pretty) Undefined(step *gherkin.Step, match *StepDef) { 370 f.basefmt.Undefined(step, match) 371 f.printStepKind(f.undefined[len(f.undefined)-1]) 372 } 373 374 func (f *pretty) Failed(step *gherkin.Step, match *StepDef, err error) { 375 f.basefmt.Failed(step, match, err) 376 f.printStepKind(f.failed[len(f.failed)-1]) 377 } 378 379 func (f *pretty) Pending(step *gherkin.Step, match *StepDef) { 380 f.basefmt.Pending(step, match) 381 f.printStepKind(f.pending[len(f.pending)-1]) 382 } 383 384 // longest gives a list of longest columns of all rows in Table 385 func longest(tbl interface{}, clrs ...colors.ColorFunc) []int { 386 var rows []*gherkin.TableRow 387 switch t := tbl.(type) { 388 case *gherkin.Examples: 389 rows = append(rows, t.TableHeader) 390 rows = append(rows, t.TableBody...) 391 case *gherkin.DataTable: 392 rows = append(rows, t.Rows...) 393 } 394 395 longest := make([]int, len(rows[0].Cells)) 396 for _, row := range rows { 397 for i, cell := range row.Cells { 398 for _, c := range clrs { 399 ln := utf8.RuneCountInString(c(cell.Value)) 400 if longest[i] < ln { 401 longest[i] = ln 402 } 403 } 404 405 ln := utf8.RuneCountInString(cell.Value) 406 if longest[i] < ln { 407 longest[i] = ln 408 } 409 } 410 } 411 return longest 412 } 413 414 func (f *pretty) longestStep(steps []*gherkin.Step, base int) int { 415 ret := base 416 for _, step := range steps { 417 length := f.length(step) 418 if length > ret { 419 ret = length 420 } 421 } 422 return ret 423 } 424 425 // a line number representation in feature file 426 func (f *pretty) line(loc *gherkin.Location) string { 427 return black(fmt.Sprintf("# %s:%d", f.features[len(f.features)-1].Path, loc.Line)) 428 } 429 430 func (f *pretty) length(node interface{}) int { 431 switch t := node.(type) { 432 case *gherkin.Background: 433 return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name) 434 case *gherkin.Step: 435 return f.indent*2 + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+" "+t.Text) 436 case *gherkin.Scenario: 437 return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name) 438 case *gherkin.ScenarioOutline: 439 return f.indent + utf8.RuneCountInString(strings.TrimSpace(t.Keyword)+": "+t.Name) 440 } 441 panic(fmt.Sprintf("unexpected node %T to determine length", node)) 442 }