github.com/data-DOG/godog@v0.7.9/fmt.go (about) 1 package godog 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "reflect" 9 "regexp" 10 "strconv" 11 "strings" 12 "text/template" 13 "time" 14 "unicode" 15 16 "github.com/DATA-DOG/godog/colors" 17 "github.com/DATA-DOG/godog/gherkin" 18 ) 19 20 // some snippet formatting regexps 21 var snippetExprCleanup = regexp.MustCompile("([\\/\\[\\]\\(\\)\\\\^\\$\\.\\|\\?\\*\\+\\'])") 22 var snippetExprQuoted = regexp.MustCompile("(\\W|^)\"(?:[^\"]*)\"(\\W|$)") 23 var snippetMethodName = regexp.MustCompile("[^a-zA-Z\\_\\ ]") 24 var snippetNumbers = regexp.MustCompile("(\\d+)") 25 26 var snippetHelperFuncs = template.FuncMap{ 27 "backticked": func(s string) string { 28 return "`" + s + "`" 29 }, 30 } 31 32 var undefinedSnippetsTpl = template.Must(template.New("snippets").Funcs(snippetHelperFuncs).Parse(` 33 {{ range . }}func {{ .Method }}({{ .Args }}) error { 34 return godog.ErrPending 35 } 36 37 {{end}}func FeatureContext(s *godog.Suite) { {{ range . }} 38 s.Step({{ backticked .Expr }}, {{ .Method }}){{end}} 39 } 40 `)) 41 42 type undefinedSnippet struct { 43 Method string 44 Expr string 45 argument interface{} // gherkin step argument 46 } 47 48 type registeredFormatter struct { 49 name string 50 fmt FormatterFunc 51 description string 52 } 53 54 var formatters []*registeredFormatter 55 56 // FindFmt searches available formatters registered 57 // and returns FormaterFunc matched by given 58 // format name or nil otherwise 59 func FindFmt(name string) FormatterFunc { 60 for _, el := range formatters { 61 if el.name == name { 62 return el.fmt 63 } 64 } 65 return nil 66 } 67 68 // Format registers a feature suite output 69 // formatter by given name, description and 70 // FormatterFunc constructor function, to initialize 71 // formatter with the output recorder. 72 func Format(name, description string, f FormatterFunc) { 73 formatters = append(formatters, ®isteredFormatter{ 74 name: name, 75 fmt: f, 76 description: description, 77 }) 78 } 79 80 // AvailableFormatters gives a map of all 81 // formatters registered with their name as key 82 // and description as value 83 func AvailableFormatters() map[string]string { 84 fmts := make(map[string]string, len(formatters)) 85 for _, f := range formatters { 86 fmts[f.name] = f.description 87 } 88 return fmts 89 } 90 91 // Formatter is an interface for feature runner 92 // output summary presentation. 93 // 94 // New formatters may be created to represent 95 // suite results in different ways. These new 96 // formatters needs to be registered with a 97 // godog.Format function call 98 type Formatter interface { 99 Feature(*gherkin.Feature, string, []byte) 100 Node(interface{}) 101 Defined(*gherkin.Step, *StepDef) 102 Failed(*gherkin.Step, *StepDef, error) 103 Passed(*gherkin.Step, *StepDef) 104 Skipped(*gherkin.Step, *StepDef) 105 Undefined(*gherkin.Step, *StepDef) 106 Pending(*gherkin.Step, *StepDef) 107 Summary() 108 } 109 110 // FormatterFunc builds a formatter with given 111 // suite name and io.Writer to record output 112 type FormatterFunc func(string, io.Writer) Formatter 113 114 type stepType int 115 116 const ( 117 passed stepType = iota 118 failed 119 skipped 120 undefined 121 pending 122 ) 123 124 func (st stepType) clr() colors.ColorFunc { 125 switch st { 126 case passed: 127 return green 128 case failed: 129 return red 130 case skipped: 131 return cyan 132 default: 133 return yellow 134 } 135 } 136 137 func (st stepType) String() string { 138 switch st { 139 case passed: 140 return "passed" 141 case failed: 142 return "failed" 143 case skipped: 144 return "skipped" 145 case undefined: 146 return "undefined" 147 case pending: 148 return "pending" 149 default: 150 return "unknown" 151 } 152 } 153 154 type stepResult struct { 155 typ stepType 156 feature *feature 157 owner interface{} 158 step *gherkin.Step 159 def *StepDef 160 err error 161 } 162 163 func (f stepResult) line() string { 164 return fmt.Sprintf("%s:%d", f.feature.Path, f.step.Location.Line) 165 } 166 167 func (f stepResult) scenarioDesc() string { 168 if sc, ok := f.owner.(*gherkin.Scenario); ok { 169 return fmt.Sprintf("%s: %s", sc.Keyword, sc.Name) 170 } 171 172 if row, ok := f.owner.(*gherkin.TableRow); ok { 173 for _, def := range f.feature.Feature.ScenarioDefinitions { 174 out, ok := def.(*gherkin.ScenarioOutline) 175 if !ok { 176 continue 177 } 178 179 for _, ex := range out.Examples { 180 for _, rw := range ex.TableBody { 181 if rw.Location.Line == row.Location.Line { 182 return fmt.Sprintf("%s: %s", out.Keyword, out.Name) 183 } 184 } 185 } 186 } 187 } 188 return f.line() // was not expecting different owner 189 } 190 191 func (f stepResult) scenarioLine() string { 192 if sc, ok := f.owner.(*gherkin.Scenario); ok { 193 return fmt.Sprintf("%s:%d", f.feature.Path, sc.Location.Line) 194 } 195 196 if row, ok := f.owner.(*gherkin.TableRow); ok { 197 for _, def := range f.feature.Feature.ScenarioDefinitions { 198 out, ok := def.(*gherkin.ScenarioOutline) 199 if !ok { 200 continue 201 } 202 203 for _, ex := range out.Examples { 204 for _, rw := range ex.TableBody { 205 if rw.Location.Line == row.Location.Line { 206 return fmt.Sprintf("%s:%d", f.feature.Path, out.Location.Line) 207 } 208 } 209 } 210 } 211 } 212 return f.line() // was not expecting different owner 213 } 214 215 type basefmt struct { 216 out io.Writer 217 owner interface{} 218 indent int 219 220 started time.Time 221 features []*feature 222 failed []*stepResult 223 passed []*stepResult 224 skipped []*stepResult 225 undefined []*stepResult 226 pending []*stepResult 227 } 228 229 func (f *basefmt) Node(n interface{}) { 230 switch t := n.(type) { 231 case *gherkin.TableRow: 232 f.owner = t 233 case *gherkin.Scenario: 234 f.owner = t 235 } 236 } 237 238 func (f *basefmt) Defined(*gherkin.Step, *StepDef) { 239 240 } 241 242 func (f *basefmt) Feature(ft *gherkin.Feature, p string, c []byte) { 243 f.features = append(f.features, &feature{Path: p, Feature: ft}) 244 } 245 246 func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) { 247 s := &stepResult{ 248 owner: f.owner, 249 feature: f.features[len(f.features)-1], 250 step: step, 251 def: match, 252 typ: passed, 253 } 254 f.passed = append(f.passed, s) 255 } 256 257 func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) { 258 s := &stepResult{ 259 owner: f.owner, 260 feature: f.features[len(f.features)-1], 261 step: step, 262 def: match, 263 typ: skipped, 264 } 265 f.skipped = append(f.skipped, s) 266 } 267 268 func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) { 269 s := &stepResult{ 270 owner: f.owner, 271 feature: f.features[len(f.features)-1], 272 step: step, 273 def: match, 274 typ: undefined, 275 } 276 f.undefined = append(f.undefined, s) 277 } 278 279 func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) { 280 s := &stepResult{ 281 owner: f.owner, 282 feature: f.features[len(f.features)-1], 283 step: step, 284 def: match, 285 err: err, 286 typ: failed, 287 } 288 f.failed = append(f.failed, s) 289 } 290 291 func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) { 292 s := &stepResult{ 293 owner: f.owner, 294 feature: f.features[len(f.features)-1], 295 step: step, 296 def: match, 297 typ: pending, 298 } 299 f.pending = append(f.pending, s) 300 } 301 302 func (f *basefmt) Summary() { 303 var total, passed, undefined int 304 for _, ft := range f.features { 305 for _, def := range ft.ScenarioDefinitions { 306 switch t := def.(type) { 307 case *gherkin.Scenario: 308 total++ 309 if len(t.Steps) == 0 { 310 undefined++ 311 } 312 case *gherkin.ScenarioOutline: 313 for _, ex := range t.Examples { 314 total += len(ex.TableBody) 315 if len(t.Steps) == 0 { 316 undefined += len(ex.TableBody) 317 } 318 } 319 } 320 } 321 } 322 passed = total - undefined 323 var owner interface{} 324 for _, undef := range f.undefined { 325 if owner != undef.owner { 326 undefined++ 327 owner = undef.owner 328 } 329 } 330 331 var steps, parts, scenarios []string 332 nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending) 333 if len(f.passed) > 0 { 334 steps = append(steps, green(fmt.Sprintf("%d passed", len(f.passed)))) 335 } 336 if len(f.failed) > 0 { 337 passed -= len(f.failed) 338 parts = append(parts, red(fmt.Sprintf("%d failed", len(f.failed)))) 339 steps = append(steps, parts[len(parts)-1]) 340 } 341 if len(f.pending) > 0 { 342 passed -= len(f.pending) 343 parts = append(parts, yellow(fmt.Sprintf("%d pending", len(f.pending)))) 344 steps = append(steps, yellow(fmt.Sprintf("%d pending", len(f.pending)))) 345 } 346 if len(f.undefined) > 0 { 347 passed -= undefined 348 parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined))) 349 steps = append(steps, yellow(fmt.Sprintf("%d undefined", len(f.undefined)))) 350 } else if undefined > 0 { 351 // there may be some scenarios without steps 352 parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined))) 353 } 354 if len(f.skipped) > 0 { 355 steps = append(steps, cyan(fmt.Sprintf("%d skipped", len(f.skipped)))) 356 } 357 if passed > 0 { 358 scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passed))) 359 } 360 scenarios = append(scenarios, parts...) 361 elapsed := timeNowFunc().Sub(f.started) 362 363 fmt.Fprintln(f.out, "") 364 if total == 0 { 365 fmt.Fprintln(f.out, "No scenarios") 366 } else { 367 fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", "))) 368 } 369 370 if nsteps == 0 { 371 fmt.Fprintln(f.out, "No steps") 372 } else { 373 fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", "))) 374 } 375 376 elapsedString := elapsed.String() 377 if elapsed.Nanoseconds() == 0 { 378 // go 1.5 and 1.6 prints 0 instead of 0s, if duration is zero. 379 elapsedString = "0s" 380 } 381 fmt.Fprintln(f.out, elapsedString) 382 383 // prints used randomization seed 384 seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64) 385 if err == nil && seed != 0 { 386 fmt.Fprintln(f.out, "") 387 fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed)) 388 } 389 390 if text := f.snippets(); text != "" { 391 fmt.Fprintln(f.out, "") 392 fmt.Fprintln(f.out, yellow("You can implement step definitions for undefined steps with these snippets:")) 393 fmt.Fprintln(f.out, yellow(text)) 394 } 395 } 396 397 func (s *undefinedSnippet) Args() (ret string) { 398 var ( 399 args []string 400 pos int 401 breakLoop bool 402 ) 403 for !breakLoop { 404 part := s.Expr[pos:] 405 ipos := strings.Index(part, "(\\d+)") 406 spos := strings.Index(part, "\"([^\"]*)\"") 407 switch { 408 case spos == -1 && ipos == -1: 409 breakLoop = true 410 case spos == -1: 411 pos += ipos + len("(\\d+)") 412 args = append(args, reflect.Int.String()) 413 case ipos == -1: 414 pos += spos + len("\"([^\"]*)\"") 415 args = append(args, reflect.String.String()) 416 case ipos < spos: 417 pos += ipos + len("(\\d+)") 418 args = append(args, reflect.Int.String()) 419 case spos < ipos: 420 pos += spos + len("\"([^\"]*)\"") 421 args = append(args, reflect.String.String()) 422 } 423 } 424 if s.argument != nil { 425 switch s.argument.(type) { 426 case *gherkin.DocString: 427 args = append(args, "*gherkin.DocString") 428 case *gherkin.DataTable: 429 args = append(args, "*gherkin.DataTable") 430 } 431 } 432 433 var last string 434 for i, arg := range args { 435 if last == "" || last == arg { 436 ret += fmt.Sprintf("arg%d, ", i+1) 437 } else { 438 ret = strings.TrimRight(ret, ", ") + fmt.Sprintf(" %s, arg%d, ", last, i+1) 439 } 440 last = arg 441 } 442 return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last) 443 } 444 445 func (f *basefmt) snippets() string { 446 if len(f.undefined) == 0 { 447 return "" 448 } 449 450 var index int 451 var snips []*undefinedSnippet 452 // build snippets 453 for _, u := range f.undefined { 454 steps := []string{u.step.Text} 455 arg := u.step.Argument 456 if u.def != nil { 457 steps = u.def.undefined 458 arg = nil 459 } 460 for _, step := range steps { 461 expr := snippetExprCleanup.ReplaceAllString(step, "\\$1") 462 expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") 463 expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") 464 expr = "^" + strings.TrimSpace(expr) + "$" 465 466 name := snippetNumbers.ReplaceAllString(step, " ") 467 name = snippetExprQuoted.ReplaceAllString(name, " ") 468 name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, "")) 469 var words []string 470 for i, w := range strings.Split(name, " ") { 471 switch { 472 case i != 0: 473 w = strings.Title(w) 474 case len(w) > 0: 475 w = string(unicode.ToLower(rune(w[0]))) + w[1:] 476 } 477 words = append(words, w) 478 } 479 name = strings.Join(words, "") 480 if len(name) == 0 { 481 index++ 482 name = fmt.Sprintf("stepDefinition%d", index) 483 } 484 485 var found bool 486 for _, snip := range snips { 487 if snip.Expr == expr { 488 found = true 489 break 490 } 491 } 492 if !found { 493 snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg}) 494 } 495 } 496 } 497 498 var buf bytes.Buffer 499 if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil { 500 panic(err) 501 } 502 // there may be trailing spaces 503 return strings.Replace(buf.String(), " \n", "\n", -1) 504 } 505 506 func (f *basefmt) isLastStep(s *gherkin.Step) bool { 507 ft := f.features[len(f.features)-1] 508 509 for _, def := range ft.ScenarioDefinitions { 510 if outline, ok := def.(*gherkin.ScenarioOutline); ok { 511 for n, step := range outline.Steps { 512 if step.Location.Line == s.Location.Line { 513 return n == len(outline.Steps)-1 514 } 515 } 516 } 517 518 if scenario, ok := def.(*gherkin.Scenario); ok { 519 for n, step := range scenario.Steps { 520 if step.Location.Line == s.Location.Line { 521 return n == len(scenario.Steps)-1 522 } 523 } 524 } 525 } 526 return false 527 }