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