github.com/Secbyte/godog@v0.7.14-0.20200116175429-d8f0aeeb70cf/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 // MetadataFormatter extend formatter to allow metdata 111 type MetadataFormatter interface { 112 Formatter 113 ProvideMetadataAndGetCurrentStepID(a *AllMetaData, getCurrentStepID func() string) 114 } 115 116 // FormatterFunc builds a formatter with given 117 // suite name and io.Writer to record output 118 type FormatterFunc func(string, io.Writer) Formatter 119 120 type stepType int 121 122 const ( 123 passed stepType = iota 124 failed 125 skipped 126 undefined 127 pending 128 ) 129 130 func (st stepType) clr() colors.ColorFunc { 131 switch st { 132 case passed: 133 return green 134 case failed: 135 return red 136 case skipped: 137 return cyan 138 default: 139 return yellow 140 } 141 } 142 143 func (st stepType) String() string { 144 switch st { 145 case passed: 146 return "passed" 147 case failed: 148 return "failed" 149 case skipped: 150 return "skipped" 151 case undefined: 152 return "undefined" 153 case pending: 154 return "pending" 155 default: 156 return "unknown" 157 } 158 } 159 160 type stepResult struct { 161 typ stepType 162 feature *feature 163 owner interface{} 164 step *gherkin.Step 165 def *StepDef 166 err error 167 } 168 169 func (f stepResult) line() string { 170 return fmt.Sprintf("%s:%d", f.feature.Path, f.step.Location.Line) 171 } 172 173 func (f stepResult) scenarioDesc() string { 174 if sc, ok := f.owner.(*gherkin.Scenario); ok { 175 return fmt.Sprintf("%s: %s", sc.Keyword, sc.Name) 176 } 177 178 if row, ok := f.owner.(*gherkin.TableRow); ok { 179 for _, def := range f.feature.Feature.ScenarioDefinitions { 180 out, ok := def.(*gherkin.ScenarioOutline) 181 if !ok { 182 continue 183 } 184 185 for _, ex := range out.Examples { 186 for _, rw := range ex.TableBody { 187 if rw.Location.Line == row.Location.Line { 188 return fmt.Sprintf("%s: %s", out.Keyword, out.Name) 189 } 190 } 191 } 192 } 193 } 194 return f.line() // was not expecting different owner 195 } 196 197 func (f stepResult) scenarioLine() string { 198 if sc, ok := f.owner.(*gherkin.Scenario); ok { 199 return fmt.Sprintf("%s:%d", f.feature.Path, sc.Location.Line) 200 } 201 202 if row, ok := f.owner.(*gherkin.TableRow); ok { 203 for _, def := range f.feature.Feature.ScenarioDefinitions { 204 out, ok := def.(*gherkin.ScenarioOutline) 205 if !ok { 206 continue 207 } 208 209 for _, ex := range out.Examples { 210 for _, rw := range ex.TableBody { 211 if rw.Location.Line == row.Location.Line { 212 return fmt.Sprintf("%s:%d", f.feature.Path, out.Location.Line) 213 } 214 } 215 } 216 } 217 } 218 return f.line() // was not expecting different owner 219 } 220 221 type basefmt struct { 222 out io.Writer 223 owner interface{} 224 indent int 225 226 started time.Time 227 features []*feature 228 failed []*stepResult 229 passed []*stepResult 230 skipped []*stepResult 231 undefined []*stepResult 232 pending []*stepResult 233 } 234 235 func (f *basefmt) Node(n interface{}) { 236 switch t := n.(type) { 237 case *gherkin.TableRow: 238 f.owner = t 239 case *gherkin.Scenario: 240 f.owner = t 241 } 242 } 243 244 func (f *basefmt) Defined(*gherkin.Step, *StepDef) { 245 246 } 247 248 func (f *basefmt) Feature(ft *gherkin.Feature, p string, c []byte) { 249 f.features = append(f.features, &feature{Path: p, Feature: ft}) 250 } 251 252 func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) { 253 s := &stepResult{ 254 owner: f.owner, 255 feature: f.features[len(f.features)-1], 256 step: step, 257 def: match, 258 typ: passed, 259 } 260 f.passed = append(f.passed, s) 261 } 262 263 func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) { 264 s := &stepResult{ 265 owner: f.owner, 266 feature: f.features[len(f.features)-1], 267 step: step, 268 def: match, 269 typ: skipped, 270 } 271 f.skipped = append(f.skipped, s) 272 } 273 274 func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) { 275 s := &stepResult{ 276 owner: f.owner, 277 feature: f.features[len(f.features)-1], 278 step: step, 279 def: match, 280 typ: undefined, 281 } 282 f.undefined = append(f.undefined, s) 283 } 284 285 func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) { 286 s := &stepResult{ 287 owner: f.owner, 288 feature: f.features[len(f.features)-1], 289 step: step, 290 def: match, 291 err: err, 292 typ: failed, 293 } 294 f.failed = append(f.failed, s) 295 } 296 297 func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) { 298 s := &stepResult{ 299 owner: f.owner, 300 feature: f.features[len(f.features)-1], 301 step: step, 302 def: match, 303 typ: pending, 304 } 305 f.pending = append(f.pending, s) 306 } 307 308 func (f *basefmt) Summary() { 309 var total, passed, undefined int 310 for _, ft := range f.features { 311 for _, def := range ft.ScenarioDefinitions { 312 switch t := def.(type) { 313 case *gherkin.Scenario: 314 total++ 315 if len(t.Steps) == 0 { 316 undefined++ 317 } 318 case *gherkin.ScenarioOutline: 319 for _, ex := range t.Examples { 320 total += len(ex.TableBody) 321 if len(t.Steps) == 0 { 322 undefined += len(ex.TableBody) 323 } 324 } 325 } 326 } 327 } 328 passed = total - undefined 329 var owner interface{} 330 for _, undef := range f.undefined { 331 if owner != undef.owner { 332 undefined++ 333 owner = undef.owner 334 } 335 } 336 337 var steps, parts, scenarios []string 338 nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending) 339 if len(f.passed) > 0 { 340 steps = append(steps, green(fmt.Sprintf("%d passed", len(f.passed)))) 341 } 342 if len(f.failed) > 0 { 343 passed -= len(f.failed) 344 parts = append(parts, red(fmt.Sprintf("%d failed", len(f.failed)))) 345 steps = append(steps, parts[len(parts)-1]) 346 } 347 if len(f.pending) > 0 { 348 passed -= len(f.pending) 349 parts = append(parts, yellow(fmt.Sprintf("%d pending", len(f.pending)))) 350 steps = append(steps, yellow(fmt.Sprintf("%d pending", len(f.pending)))) 351 } 352 if len(f.undefined) > 0 { 353 passed -= undefined 354 parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined))) 355 steps = append(steps, yellow(fmt.Sprintf("%d undefined", len(f.undefined)))) 356 } else if undefined > 0 { 357 // there may be some scenarios without steps 358 parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined))) 359 } 360 if len(f.skipped) > 0 { 361 steps = append(steps, cyan(fmt.Sprintf("%d skipped", len(f.skipped)))) 362 } 363 if passed > 0 { 364 scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passed))) 365 } 366 scenarios = append(scenarios, parts...) 367 elapsed := timeNowFunc().Sub(f.started) 368 369 fmt.Fprintln(f.out, "") 370 if total == 0 { 371 fmt.Fprintln(f.out, "No scenarios") 372 } else { 373 fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", "))) 374 } 375 376 if nsteps == 0 { 377 fmt.Fprintln(f.out, "No steps") 378 } else { 379 fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", "))) 380 } 381 382 elapsedString := elapsed.String() 383 if elapsed.Nanoseconds() == 0 { 384 // go 1.5 and 1.6 prints 0 instead of 0s, if duration is zero. 385 elapsedString = "0s" 386 } 387 fmt.Fprintln(f.out, elapsedString) 388 389 // prints used randomization seed 390 seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64) 391 if err == nil && seed != 0 { 392 fmt.Fprintln(f.out, "") 393 fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed)) 394 } 395 396 if text := f.snippets(); text != "" { 397 fmt.Fprintln(f.out, "") 398 fmt.Fprintln(f.out, yellow("You can implement step definitions for undefined steps with these snippets:")) 399 fmt.Fprintln(f.out, yellow(text)) 400 } 401 } 402 403 func (s *undefinedSnippet) Args() (ret string) { 404 var ( 405 args []string 406 pos int 407 breakLoop bool 408 ) 409 for !breakLoop { 410 part := s.Expr[pos:] 411 ipos := strings.Index(part, "(\\d+)") 412 spos := strings.Index(part, "\"([^\"]*)\"") 413 switch { 414 case spos == -1 && ipos == -1: 415 breakLoop = true 416 case spos == -1: 417 pos += ipos + len("(\\d+)") 418 args = append(args, reflect.Int.String()) 419 case ipos == -1: 420 pos += spos + len("\"([^\"]*)\"") 421 args = append(args, reflect.String.String()) 422 case ipos < spos: 423 pos += ipos + len("(\\d+)") 424 args = append(args, reflect.Int.String()) 425 case spos < ipos: 426 pos += spos + len("\"([^\"]*)\"") 427 args = append(args, reflect.String.String()) 428 } 429 } 430 if s.argument != nil { 431 switch s.argument.(type) { 432 case *gherkin.DocString: 433 args = append(args, "*gherkin.DocString") 434 case *gherkin.DataTable: 435 args = append(args, "*gherkin.DataTable") 436 } 437 } 438 439 var last string 440 for i, arg := range args { 441 if last == "" || last == arg { 442 ret += fmt.Sprintf("arg%d, ", i+1) 443 } else { 444 ret = strings.TrimRight(ret, ", ") + fmt.Sprintf(" %s, arg%d, ", last, i+1) 445 } 446 last = arg 447 } 448 return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last) 449 } 450 451 func (f *basefmt) snippets() string { 452 if len(f.undefined) == 0 { 453 return "" 454 } 455 456 var index int 457 var snips []*undefinedSnippet 458 // build snippets 459 for _, u := range f.undefined { 460 steps := []string{u.step.Text} 461 arg := u.step.Argument 462 if u.def != nil { 463 steps = u.def.undefined 464 arg = nil 465 } 466 for _, step := range steps { 467 expr := snippetExprCleanup.ReplaceAllString(step, "\\$1") 468 expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") 469 expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") 470 expr = "^" + strings.TrimSpace(expr) + "$" 471 472 name := snippetNumbers.ReplaceAllString(step, " ") 473 name = snippetExprQuoted.ReplaceAllString(name, " ") 474 name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, "")) 475 var words []string 476 for i, w := range strings.Split(name, " ") { 477 switch { 478 case i != 0: 479 w = strings.Title(w) 480 case len(w) > 0: 481 w = string(unicode.ToLower(rune(w[0]))) + w[1:] 482 } 483 words = append(words, w) 484 } 485 name = strings.Join(words, "") 486 if len(name) == 0 { 487 index++ 488 name = fmt.Sprintf("stepDefinition%d", index) 489 } 490 491 var found bool 492 for _, snip := range snips { 493 if snip.Expr == expr { 494 found = true 495 break 496 } 497 } 498 if !found { 499 snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg}) 500 } 501 } 502 } 503 504 var buf bytes.Buffer 505 if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil { 506 panic(err) 507 } 508 // there may be trailing spaces 509 return strings.Replace(buf.String(), " \n", "\n", -1) 510 } 511 512 func (f *basefmt) isLastStep(s *gherkin.Step) bool { 513 ft := f.features[len(f.features)-1] 514 515 for _, def := range ft.ScenarioDefinitions { 516 if outline, ok := def.(*gherkin.ScenarioOutline); ok { 517 for n, step := range outline.Steps { 518 if step.Location.Line == s.Location.Line { 519 return n == len(outline.Steps)-1 520 } 521 } 522 } 523 524 if scenario, ok := def.(*gherkin.Scenario); ok { 525 for n, step := range scenario.Steps { 526 if step.Location.Line == s.Location.Line { 527 return n == len(scenario.Steps)-1 528 } 529 } 530 } 531 } 532 return false 533 }