github.com/maps90/godog@v0.7.5-0.20170923143419-0093943021d4/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 func findFmt(format string) FormatterFunc { 57 for _, el := range formatters { 58 if el.name == format { 59 return el.fmt 60 } 61 } 62 return nil 63 } 64 65 // Format registers a feature suite output 66 // formatter by given name, description and 67 // FormatterFunc constructor function, to initialize 68 // formatter with the output recorder. 69 func Format(name, description string, f FormatterFunc) { 70 formatters = append(formatters, ®isteredFormatter{ 71 name: name, 72 fmt: f, 73 description: description, 74 }) 75 } 76 77 // AvailableFormatters gives a map of all 78 // formatters registered with their name as key 79 // and description as value 80 func AvailableFormatters() map[string]string { 81 fmts := make(map[string]string, len(formatters)) 82 for _, f := range formatters { 83 fmts[f.name] = f.description 84 } 85 return fmts 86 } 87 88 // Formatter is an interface for feature runner 89 // output summary presentation. 90 // 91 // New formatters may be created to represent 92 // suite results in different ways. These new 93 // formatters needs to be registered with a 94 // godog.Format function call 95 type Formatter interface { 96 Feature(*gherkin.Feature, string, []byte) 97 Node(interface{}) 98 Defined(*gherkin.Step, *StepDef) 99 Failed(*gherkin.Step, *StepDef, error) 100 Passed(*gherkin.Step, *StepDef) 101 Skipped(*gherkin.Step, *StepDef) 102 Undefined(*gherkin.Step, *StepDef) 103 Pending(*gherkin.Step, *StepDef) 104 Summary() 105 } 106 107 // FormatterFunc builds a formatter with given 108 // suite name and io.Writer to record output 109 type FormatterFunc func(string, io.Writer) Formatter 110 111 type stepType int 112 113 const ( 114 passed stepType = iota 115 failed 116 skipped 117 undefined 118 pending 119 ) 120 121 func (st stepType) clr() colors.ColorFunc { 122 switch st { 123 case passed: 124 return green 125 case failed: 126 return red 127 case skipped: 128 return cyan 129 default: 130 return yellow 131 } 132 } 133 134 func (st stepType) String() string { 135 switch st { 136 case passed: 137 return "passed" 138 case failed: 139 return "failed" 140 case skipped: 141 return "skipped" 142 case undefined: 143 return "undefined" 144 case pending: 145 return "pending" 146 default: 147 return "unknown" 148 } 149 } 150 151 type stepResult struct { 152 typ stepType 153 feature *feature 154 owner interface{} 155 step *gherkin.Step 156 def *StepDef 157 err error 158 } 159 160 func (f stepResult) line() string { 161 return fmt.Sprintf("%s:%d", f.feature.Path, f.step.Location.Line) 162 } 163 164 type basefmt struct { 165 out io.Writer 166 owner interface{} 167 indent int 168 169 started time.Time 170 features []*feature 171 failed []*stepResult 172 passed []*stepResult 173 skipped []*stepResult 174 undefined []*stepResult 175 pending []*stepResult 176 } 177 178 func (f *basefmt) Node(n interface{}) { 179 switch t := n.(type) { 180 case *gherkin.TableRow: 181 f.owner = t 182 case *gherkin.Scenario: 183 f.owner = t 184 } 185 } 186 187 func (f *basefmt) Defined(*gherkin.Step, *StepDef) { 188 189 } 190 191 func (f *basefmt) Feature(ft *gherkin.Feature, p string, c []byte) { 192 f.features = append(f.features, &feature{Path: p, Feature: ft}) 193 } 194 195 func (f *basefmt) Passed(step *gherkin.Step, match *StepDef) { 196 s := &stepResult{ 197 owner: f.owner, 198 feature: f.features[len(f.features)-1], 199 step: step, 200 def: match, 201 typ: passed, 202 } 203 f.passed = append(f.passed, s) 204 } 205 206 func (f *basefmt) Skipped(step *gherkin.Step, match *StepDef) { 207 s := &stepResult{ 208 owner: f.owner, 209 feature: f.features[len(f.features)-1], 210 step: step, 211 def: match, 212 typ: skipped, 213 } 214 f.skipped = append(f.skipped, s) 215 } 216 217 func (f *basefmt) Undefined(step *gherkin.Step, match *StepDef) { 218 s := &stepResult{ 219 owner: f.owner, 220 feature: f.features[len(f.features)-1], 221 step: step, 222 def: match, 223 typ: undefined, 224 } 225 f.undefined = append(f.undefined, s) 226 } 227 228 func (f *basefmt) Failed(step *gherkin.Step, match *StepDef, err error) { 229 s := &stepResult{ 230 owner: f.owner, 231 feature: f.features[len(f.features)-1], 232 step: step, 233 def: match, 234 err: err, 235 typ: failed, 236 } 237 f.failed = append(f.failed, s) 238 } 239 240 func (f *basefmt) Pending(step *gherkin.Step, match *StepDef) { 241 s := &stepResult{ 242 owner: f.owner, 243 feature: f.features[len(f.features)-1], 244 step: step, 245 def: match, 246 typ: pending, 247 } 248 f.pending = append(f.pending, s) 249 } 250 251 func (f *basefmt) Summary() { 252 var total, passed, undefined int 253 for _, ft := range f.features { 254 for _, def := range ft.ScenarioDefinitions { 255 switch t := def.(type) { 256 case *gherkin.Scenario: 257 total++ 258 case *gherkin.ScenarioOutline: 259 for _, ex := range t.Examples { 260 if examples, hasExamples := examples(ex); hasExamples { 261 total += len(examples.TableBody) 262 } 263 } 264 } 265 } 266 } 267 passed = total 268 var owner interface{} 269 for _, undef := range f.undefined { 270 if owner != undef.owner { 271 undefined++ 272 owner = undef.owner 273 } 274 } 275 276 var steps, parts, scenarios []string 277 nsteps := len(f.passed) + len(f.failed) + len(f.skipped) + len(f.undefined) + len(f.pending) 278 if len(f.passed) > 0 { 279 steps = append(steps, green(fmt.Sprintf("%d passed", len(f.passed)))) 280 } 281 if len(f.failed) > 0 { 282 passed -= len(f.failed) 283 parts = append(parts, red(fmt.Sprintf("%d failed", len(f.failed)))) 284 steps = append(steps, parts[len(parts)-1]) 285 } 286 if len(f.pending) > 0 { 287 passed -= len(f.pending) 288 parts = append(parts, yellow(fmt.Sprintf("%d pending", len(f.pending)))) 289 steps = append(steps, yellow(fmt.Sprintf("%d pending", len(f.pending)))) 290 } 291 if len(f.undefined) > 0 { 292 passed -= undefined 293 parts = append(parts, yellow(fmt.Sprintf("%d undefined", undefined))) 294 steps = append(steps, yellow(fmt.Sprintf("%d undefined", len(f.undefined)))) 295 } 296 if len(f.skipped) > 0 { 297 steps = append(steps, cyan(fmt.Sprintf("%d skipped", len(f.skipped)))) 298 } 299 if passed > 0 { 300 scenarios = append(scenarios, green(fmt.Sprintf("%d passed", passed))) 301 } 302 scenarios = append(scenarios, parts...) 303 elapsed := timeNowFunc().Sub(f.started) 304 305 fmt.Fprintln(f.out, "") 306 if total == 0 { 307 fmt.Fprintln(f.out, "No scenarios") 308 } else { 309 fmt.Fprintln(f.out, fmt.Sprintf("%d scenarios (%s)", total, strings.Join(scenarios, ", "))) 310 } 311 312 if nsteps == 0 { 313 fmt.Fprintln(f.out, "No steps") 314 } else { 315 fmt.Fprintln(f.out, fmt.Sprintf("%d steps (%s)", nsteps, strings.Join(steps, ", "))) 316 } 317 fmt.Fprintln(f.out, elapsed) 318 319 // prints used randomization seed 320 seed, err := strconv.ParseInt(os.Getenv("GODOG_SEED"), 10, 64) 321 if err == nil && seed != 0 { 322 fmt.Fprintln(f.out, "") 323 fmt.Fprintln(f.out, "Randomized with seed:", colors.Yellow(seed)) 324 } 325 326 if text := f.snippets(); text != "" { 327 fmt.Fprintln(f.out, yellow("\nYou can implement step definitions for undefined steps with these snippets:")) 328 fmt.Fprintln(f.out, yellow(text)) 329 } 330 } 331 332 func (s *undefinedSnippet) Args() (ret string) { 333 var args []string 334 var pos, idx int 335 var breakLoop bool 336 for !breakLoop { 337 part := s.Expr[pos:] 338 ipos := strings.Index(part, "(\\d+)") 339 spos := strings.Index(part, "\"([^\"]*)\"") 340 switch { 341 case spos == -1 && ipos == -1: 342 breakLoop = true 343 case spos == -1: 344 idx++ 345 pos += ipos + len("(\\d+)") 346 args = append(args, reflect.Int.String()) 347 case ipos == -1: 348 idx++ 349 pos += spos + len("\"([^\"]*)\"") 350 args = append(args, reflect.String.String()) 351 case ipos < spos: 352 idx++ 353 pos += ipos + len("(\\d+)") 354 args = append(args, reflect.Int.String()) 355 case spos < ipos: 356 idx++ 357 pos += spos + len("\"([^\"]*)\"") 358 args = append(args, reflect.String.String()) 359 } 360 } 361 if s.argument != nil { 362 idx++ 363 switch s.argument.(type) { 364 case *gherkin.DocString: 365 args = append(args, "*gherkin.DocString") 366 case *gherkin.DataTable: 367 args = append(args, "*gherkin.DataTable") 368 } 369 } 370 371 var last string 372 for i, arg := range args { 373 if last == "" || last == arg { 374 ret += fmt.Sprintf("arg%d, ", i+1) 375 } else { 376 ret = strings.TrimRight(ret, ", ") + fmt.Sprintf(" %s, arg%d, ", last, i+1) 377 } 378 last = arg 379 } 380 return strings.TrimSpace(strings.TrimRight(ret, ", ") + " " + last) 381 } 382 383 func (f *basefmt) snippets() string { 384 if len(f.undefined) == 0 { 385 return "" 386 } 387 388 var index int 389 var snips []*undefinedSnippet 390 // build snippets 391 for _, u := range f.undefined { 392 steps := []string{u.step.Text} 393 arg := u.step.Argument 394 if u.def != nil { 395 steps = u.def.undefined 396 arg = nil 397 } 398 for _, step := range steps { 399 expr := snippetExprCleanup.ReplaceAllString(step, "\\$1") 400 expr = snippetNumbers.ReplaceAllString(expr, "(\\d+)") 401 expr = snippetExprQuoted.ReplaceAllString(expr, "$1\"([^\"]*)\"$2") 402 expr = "^" + strings.TrimSpace(expr) + "$" 403 404 name := snippetNumbers.ReplaceAllString(step, " ") 405 name = snippetExprQuoted.ReplaceAllString(name, " ") 406 name = strings.TrimSpace(snippetMethodName.ReplaceAllString(name, "")) 407 var words []string 408 for i, w := range strings.Split(name, " ") { 409 switch { 410 case i != 0: 411 w = strings.Title(w) 412 case len(w) > 0: 413 w = string(unicode.ToLower(rune(w[0]))) + w[1:] 414 } 415 words = append(words, w) 416 } 417 name = strings.Join(words, "") 418 if len(name) == 0 { 419 index++ 420 name = fmt.Sprintf("stepDefinition%d", index) 421 } 422 423 var found bool 424 for _, snip := range snips { 425 if snip.Expr == expr { 426 found = true 427 break 428 } 429 } 430 if !found { 431 snips = append(snips, &undefinedSnippet{Method: name, Expr: expr, argument: arg}) 432 } 433 } 434 } 435 436 var buf bytes.Buffer 437 if err := undefinedSnippetsTpl.Execute(&buf, snips); err != nil { 438 panic(err) 439 } 440 // there may be trailing spaces 441 return strings.Replace(buf.String(), " \n", "\n", -1) 442 } 443 444 func (f *basefmt) isLastStep(s *gherkin.Step) bool { 445 ft := f.features[len(f.features)-1] 446 447 for _, def := range ft.ScenarioDefinitions { 448 if outline, ok := def.(*gherkin.ScenarioOutline); ok { 449 for n, step := range outline.Steps { 450 if step.Location.Line == s.Location.Line { 451 return n == len(outline.Steps)-1 452 } 453 } 454 } 455 456 if scenario, ok := def.(*gherkin.Scenario); ok { 457 for n, step := range scenario.Steps { 458 if step.Location.Line == s.Location.Line { 459 return n == len(scenario.Steps)-1 460 } 461 } 462 } 463 } 464 return false 465 }