github.com/lonnblad/godog@v0.7.14-0.20200306004719-1b0cb3259847/suite.go (about) 1 package godog 2 3 import ( 4 "bytes" 5 "fmt" 6 "io" 7 "os" 8 "path/filepath" 9 "reflect" 10 "regexp" 11 "sort" 12 "strconv" 13 "strings" 14 "time" 15 "unicode/utf8" 16 17 "github.com/cucumber/gherkin-go/v9" 18 "github.com/cucumber/messages-go/v9" 19 ) 20 21 var errorInterface = reflect.TypeOf((*error)(nil)).Elem() 22 var typeOfBytes = reflect.TypeOf([]byte(nil)) 23 24 type feature struct { 25 *messages.GherkinDocument 26 pickles []*messages.Pickle 27 pickleResults []*pickleResult 28 29 time time.Time 30 Content []byte `json:"-"` 31 Path string `json:"path"` 32 order int 33 } 34 35 func (f feature) findScenario(astScenarioID string) *messages.GherkinDocument_Feature_Scenario { 36 for _, child := range f.GherkinDocument.Feature.Children { 37 if sc := child.GetScenario(); sc != nil && sc.Id == astScenarioID { 38 return sc 39 } 40 } 41 42 return nil 43 } 44 45 func (f feature) findBackground(astScenarioID string) *messages.GherkinDocument_Feature_Background { 46 var bg *messages.GherkinDocument_Feature_Background 47 48 for _, child := range f.GherkinDocument.Feature.Children { 49 if tmp := child.GetBackground(); tmp != nil { 50 bg = tmp 51 } 52 53 if sc := child.GetScenario(); sc != nil && sc.Id == astScenarioID { 54 return bg 55 } 56 } 57 58 return nil 59 } 60 61 func (f feature) findExample(exampleAstID string) (*messages.GherkinDocument_Feature_Scenario_Examples, *messages.GherkinDocument_Feature_TableRow) { 62 for _, child := range f.GherkinDocument.Feature.Children { 63 if sc := child.GetScenario(); sc != nil { 64 for _, example := range sc.Examples { 65 for _, row := range example.TableBody { 66 if row.Id == exampleAstID { 67 return example, row 68 } 69 } 70 } 71 } 72 } 73 74 return nil, nil 75 } 76 77 func (f feature) findStep(astStepID string) *messages.GherkinDocument_Feature_Step { 78 for _, child := range f.GherkinDocument.Feature.Children { 79 if sc := child.GetScenario(); sc != nil { 80 for _, step := range sc.GetSteps() { 81 if step.Id == astStepID { 82 return step 83 } 84 } 85 } 86 87 if bg := child.GetBackground(); bg != nil { 88 for _, step := range bg.GetSteps() { 89 if step.Id == astStepID { 90 return step 91 } 92 } 93 } 94 } 95 96 return nil 97 } 98 99 func (f feature) startedAt() time.Time { 100 return f.time 101 } 102 103 func (f feature) finishedAt() time.Time { 104 if len(f.pickleResults) == 0 { 105 return f.startedAt() 106 } 107 108 return f.pickleResults[len(f.pickleResults)-1].finishedAt() 109 } 110 111 func (f feature) appendStepResult(s *stepResult) { 112 pickles := f.pickleResults[len(f.pickleResults)-1] 113 pickles.stepResults = append(pickles.stepResults, s) 114 } 115 116 func (f feature) lastPickleResult() *pickleResult { 117 return f.pickleResults[len(f.pickleResults)-1] 118 } 119 120 func (f feature) lastStepResult() *stepResult { 121 last := f.lastPickleResult() 122 return last.stepResults[len(last.stepResults)-1] 123 } 124 125 type sortByName []*feature 126 127 func (s sortByName) Len() int { return len(s) } 128 func (s sortByName) Less(i, j int) bool { return s[i].Feature.Name < s[j].Feature.Name } 129 func (s sortByName) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 130 131 type pickleResult struct { 132 Name string 133 time time.Time 134 stepResults []*stepResult 135 } 136 137 func (s pickleResult) startedAt() time.Time { 138 return s.time 139 } 140 141 func (s pickleResult) finishedAt() time.Time { 142 if len(s.stepResults) == 0 { 143 return s.startedAt() 144 } 145 146 return s.stepResults[len(s.stepResults)-1].time 147 } 148 149 // ErrUndefined is returned in case if step definition was not found 150 var ErrUndefined = fmt.Errorf("step is undefined") 151 152 // ErrPending should be returned by step definition if 153 // step implementation is pending 154 var ErrPending = fmt.Errorf("step implementation is pending") 155 156 // Suite allows various contexts 157 // to register steps and event handlers. 158 // 159 // When running a test suite, the instance of Suite 160 // is passed to all functions (contexts), which 161 // have it as a first and only argument. 162 // 163 // Note that all event hooks does not catch panic errors 164 // in order to have a trace information. Only step 165 // executions are catching panic error since it may 166 // be a context specific error. 167 type Suite struct { 168 steps []*StepDefinition 169 features []*feature 170 fmt Formatter 171 172 failed bool 173 randomSeed int64 174 stopOnFailure bool 175 strict bool 176 177 // suite event handlers 178 beforeSuiteHandlers []func() 179 beforeFeatureHandlers []func(*messages.GherkinDocument) 180 beforeScenarioHandlers []func(*messages.Pickle) 181 beforeStepHandlers []func(*messages.Pickle_PickleStep) 182 afterStepHandlers []func(*messages.Pickle_PickleStep, error) 183 afterScenarioHandlers []func(*messages.Pickle, error) 184 afterFeatureHandlers []func(*messages.GherkinDocument) 185 afterSuiteHandlers []func() 186 } 187 188 // Step allows to register a *StepDefinition in Godog 189 // feature suite, the definition will be applied 190 // to all steps matching the given Regexp expr. 191 // 192 // It will panic if expr is not a valid regular 193 // expression or stepFunc is not a valid step 194 // handler. 195 // 196 // Note that if there are two definitions which may match 197 // the same step, then only the first matched handler 198 // will be applied. 199 // 200 // If none of the *StepDefinition is matched, then 201 // ErrUndefined error will be returned when 202 // running steps. 203 func (s *Suite) Step(expr interface{}, stepFunc interface{}) { 204 var regex *regexp.Regexp 205 206 switch t := expr.(type) { 207 case *regexp.Regexp: 208 regex = t 209 case string: 210 regex = regexp.MustCompile(t) 211 case []byte: 212 regex = regexp.MustCompile(string(t)) 213 default: 214 panic(fmt.Sprintf("expecting expr to be a *regexp.Regexp or a string, got type: %T", expr)) 215 } 216 217 v := reflect.ValueOf(stepFunc) 218 typ := v.Type() 219 if typ.Kind() != reflect.Func { 220 panic(fmt.Sprintf("expected handler to be func, but got: %T", stepFunc)) 221 } 222 223 if typ.NumOut() != 1 { 224 panic(fmt.Sprintf("expected handler to return only one value, but it has: %d", typ.NumOut())) 225 } 226 227 def := &StepDefinition{ 228 Handler: stepFunc, 229 Expr: regex, 230 hv: v, 231 } 232 233 typ = typ.Out(0) 234 switch typ.Kind() { 235 case reflect.Interface: 236 if !typ.Implements(errorInterface) { 237 panic(fmt.Sprintf("expected handler to return an error, but got: %s", typ.Kind())) 238 } 239 case reflect.Slice: 240 if typ.Elem().Kind() != reflect.String { 241 panic(fmt.Sprintf("expected handler to return []string for multistep, but got: []%s", typ.Kind())) 242 } 243 def.nested = true 244 default: 245 panic(fmt.Sprintf("expected handler to return an error or []string, but got: %s", typ.Kind())) 246 } 247 248 s.steps = append(s.steps, def) 249 } 250 251 // BeforeSuite registers a function or method 252 // to be run once before suite runner. 253 // 254 // Use it to prepare the test suite for a spin. 255 // Connect and prepare database for instance... 256 func (s *Suite) BeforeSuite(fn func()) { 257 s.beforeSuiteHandlers = append(s.beforeSuiteHandlers, fn) 258 } 259 260 // BeforeFeature registers a function or method 261 // to be run once before every feature execution. 262 // 263 // If godog is run with concurrency option, it will 264 // run every feature per goroutine. So user may choose 265 // whether to isolate state within feature context or 266 // scenario. 267 // 268 // Best practice is not to have any state dependency on 269 // every scenario, but in some cases if VM for example 270 // needs to be started it may take very long for each 271 // scenario to restart it. 272 // 273 // Use it wisely and avoid sharing state between scenarios. 274 func (s *Suite) BeforeFeature(fn func(*messages.GherkinDocument)) { 275 s.beforeFeatureHandlers = append(s.beforeFeatureHandlers, fn) 276 } 277 278 // BeforeScenario registers a function or method 279 // to be run before every pickle. 280 // 281 // It is a good practice to restore the default state 282 // before every scenario so it would be isolated from 283 // any kind of state. 284 func (s *Suite) BeforeScenario(fn func(*messages.Pickle)) { 285 s.beforeScenarioHandlers = append(s.beforeScenarioHandlers, fn) 286 } 287 288 // BeforeStep registers a function or method 289 // to be run before every step. 290 func (s *Suite) BeforeStep(fn func(*messages.Pickle_PickleStep)) { 291 s.beforeStepHandlers = append(s.beforeStepHandlers, fn) 292 } 293 294 // AfterStep registers an function or method 295 // to be run after every step. 296 // 297 // It may be convenient to return a different kind of error 298 // in order to print more state details which may help 299 // in case of step failure 300 // 301 // In some cases, for example when running a headless 302 // browser, to take a screenshot after failure. 303 func (s *Suite) AfterStep(fn func(*messages.Pickle_PickleStep, error)) { 304 s.afterStepHandlers = append(s.afterStepHandlers, fn) 305 } 306 307 // AfterScenario registers an function or method 308 // to be run after every pickle. 309 func (s *Suite) AfterScenario(fn func(*messages.Pickle, error)) { 310 s.afterScenarioHandlers = append(s.afterScenarioHandlers, fn) 311 } 312 313 // AfterFeature registers a function or method 314 // to be run once after feature executed all scenarios. 315 func (s *Suite) AfterFeature(fn func(*messages.GherkinDocument)) { 316 s.afterFeatureHandlers = append(s.afterFeatureHandlers, fn) 317 } 318 319 // AfterSuite registers a function or method 320 // to be run once after suite runner 321 func (s *Suite) AfterSuite(fn func()) { 322 s.afterSuiteHandlers = append(s.afterSuiteHandlers, fn) 323 } 324 325 func (s *Suite) run() { 326 // run before suite handlers 327 for _, f := range s.beforeSuiteHandlers { 328 f() 329 } 330 // run features 331 for _, f := range s.features { 332 s.runFeature(f) 333 if s.failed && s.stopOnFailure { 334 // stop on first failure 335 break 336 } 337 } 338 // run after suite handlers 339 for _, f := range s.afterSuiteHandlers { 340 f() 341 } 342 } 343 344 func (s *Suite) matchStep(step *messages.Pickle_PickleStep) *StepDefinition { 345 def := s.matchStepText(step.Text) 346 if def != nil && step.Argument != nil { 347 def.args = append(def.args, step.Argument) 348 } 349 return def 350 } 351 352 func (s *Suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep, prevStepErr error) (err error) { 353 // run before step handlers 354 for _, f := range s.beforeStepHandlers { 355 f(step) 356 } 357 358 match := s.matchStep(step) 359 s.fmt.Defined(pickle, step, match) 360 361 // user multistep definitions may panic 362 defer func() { 363 if e := recover(); e != nil { 364 err = &traceError{ 365 msg: fmt.Sprintf("%v", e), 366 stack: callStack(), 367 } 368 } 369 370 if prevStepErr != nil { 371 return 372 } 373 374 if err == ErrUndefined { 375 return 376 } 377 378 switch err { 379 case nil: 380 s.fmt.Passed(pickle, step, match) 381 case ErrPending: 382 s.fmt.Pending(pickle, step, match) 383 default: 384 s.fmt.Failed(pickle, step, match, err) 385 } 386 387 // run after step handlers 388 for _, f := range s.afterStepHandlers { 389 f(step, err) 390 } 391 }() 392 393 if undef, err := s.maybeUndefined(step.Text, step.Argument); err != nil { 394 return err 395 } else if len(undef) > 0 { 396 if match != nil { 397 match = &StepDefinition{ 398 args: match.args, 399 hv: match.hv, 400 Expr: match.Expr, 401 Handler: match.Handler, 402 nested: match.nested, 403 undefined: undef, 404 } 405 } 406 s.fmt.Undefined(pickle, step, match) 407 return ErrUndefined 408 } 409 410 if prevStepErr != nil { 411 s.fmt.Skipped(pickle, step, match) 412 return nil 413 } 414 415 err = s.maybeSubSteps(match.run()) 416 return 417 } 418 419 func (s *Suite) maybeUndefined(text string, arg interface{}) ([]string, error) { 420 step := s.matchStepText(text) 421 if nil == step { 422 return []string{text}, nil 423 } 424 425 var undefined []string 426 if !step.nested { 427 return undefined, nil 428 } 429 430 if arg != nil { 431 step.args = append(step.args, arg) 432 } 433 434 for _, next := range step.run().(Steps) { 435 lines := strings.Split(next, "\n") 436 // @TODO: we cannot currently parse table or content body from nested steps 437 if len(lines) > 1 { 438 return undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument") 439 } 440 if len(lines[0]) > 0 && lines[0][len(lines[0])-1] == ':' { 441 return undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument") 442 } 443 undef, err := s.maybeUndefined(next, nil) 444 if err != nil { 445 return undefined, err 446 } 447 undefined = append(undefined, undef...) 448 } 449 return undefined, nil 450 } 451 452 func (s *Suite) maybeSubSteps(result interface{}) error { 453 if nil == result { 454 return nil 455 } 456 457 if err, ok := result.(error); ok { 458 return err 459 } 460 461 steps, ok := result.(Steps) 462 if !ok { 463 return fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result) 464 } 465 466 for _, text := range steps { 467 if def := s.matchStepText(text); def == nil { 468 return ErrUndefined 469 } else if err := s.maybeSubSteps(def.run()); err != nil { 470 return fmt.Errorf("%s: %+v", text, err) 471 } 472 } 473 return nil 474 } 475 476 func (s *Suite) matchStepText(text string) *StepDefinition { 477 for _, h := range s.steps { 478 if m := h.Expr.FindStringSubmatch(text); len(m) > 0 { 479 var args []interface{} 480 for _, m := range m[1:] { 481 args = append(args, m) 482 } 483 484 // since we need to assign arguments 485 // better to copy the step definition 486 return &StepDefinition{ 487 args: args, 488 hv: h.hv, 489 Expr: h.Expr, 490 Handler: h.Handler, 491 nested: h.nested, 492 } 493 } 494 } 495 return nil 496 } 497 498 func (s *Suite) runSteps(pickle *messages.Pickle, steps []*messages.Pickle_PickleStep) (err error) { 499 for _, step := range steps { 500 stepErr := s.runStep(pickle, step, err) 501 switch stepErr { 502 case ErrUndefined: 503 // do not overwrite failed error 504 if err == ErrUndefined || err == nil { 505 err = stepErr 506 } 507 case ErrPending: 508 err = stepErr 509 case nil: 510 default: 511 err = stepErr 512 } 513 } 514 return 515 } 516 517 func (s *Suite) shouldFail(err error) bool { 518 if err == nil { 519 return false 520 } 521 522 if err == ErrUndefined || err == ErrPending { 523 return s.strict 524 } 525 526 return true 527 } 528 529 func (s *Suite) runFeature(f *feature) { 530 if !isEmptyFeature(f.pickles) { 531 for _, fn := range s.beforeFeatureHandlers { 532 fn(f.GherkinDocument) 533 } 534 } 535 536 s.fmt.Feature(f.GherkinDocument, f.Path, f.Content) 537 538 defer func() { 539 if !isEmptyFeature(f.pickles) { 540 for _, fn := range s.afterFeatureHandlers { 541 fn(f.GherkinDocument) 542 } 543 } 544 }() 545 546 for _, pickle := range f.pickles { 547 err := s.runPickle(pickle) 548 if s.shouldFail(err) { 549 s.failed = true 550 if s.stopOnFailure { 551 return 552 } 553 } 554 } 555 } 556 557 func isEmptyFeature(pickles []*messages.Pickle) bool { 558 for _, pickle := range pickles { 559 if len(pickle.Steps) > 0 { 560 return false 561 } 562 } 563 564 return true 565 } 566 567 func (s *Suite) runPickle(pickle *messages.Pickle) (err error) { 568 if len(pickle.Steps) == 0 { 569 s.fmt.Pickle(pickle) 570 return ErrUndefined 571 } 572 573 // run before scenario handlers 574 for _, f := range s.beforeScenarioHandlers { 575 f(pickle) 576 } 577 578 s.fmt.Pickle(pickle) 579 580 // scenario 581 err = s.runSteps(pickle, pickle.Steps) 582 583 // run after scenario handlers 584 for _, f := range s.afterScenarioHandlers { 585 f(pickle, err) 586 } 587 588 return 589 } 590 591 func (s *Suite) printStepDefinitions(w io.Writer) { 592 var longest int 593 for _, def := range s.steps { 594 n := utf8.RuneCountInString(def.Expr.String()) 595 if longest < n { 596 longest = n 597 } 598 } 599 for _, def := range s.steps { 600 n := utf8.RuneCountInString(def.Expr.String()) 601 location := def.definitionID() 602 spaces := strings.Repeat(" ", longest-n) 603 fmt.Fprintln(w, yellow(def.Expr.String())+spaces, blackb("# "+location)) 604 } 605 if len(s.steps) == 0 { 606 fmt.Fprintln(w, "there were no contexts registered, could not find any step definition..") 607 } 608 } 609 610 var pathLineRe = regexp.MustCompile(`:([\d]+)$`) 611 612 func extractFeaturePathLine(p string) (string, int) { 613 line := -1 614 retPath := p 615 if m := pathLineRe.FindStringSubmatch(p); len(m) > 0 { 616 if i, err := strconv.Atoi(m[1]); err == nil { 617 line = i 618 retPath = p[:strings.LastIndexByte(p, ':')] 619 } 620 } 621 return retPath, line 622 } 623 624 func parseFeatureFile(path string, newIDFunc func() string) (*feature, error) { 625 reader, err := os.Open(path) 626 if err != nil { 627 return nil, err 628 } 629 defer reader.Close() 630 631 var buf bytes.Buffer 632 gherkinDocument, err := gherkin.ParseGherkinDocument(io.TeeReader(reader, &buf), newIDFunc) 633 if err != nil { 634 return nil, fmt.Errorf("%s - %v", path, err) 635 } 636 637 pickles := gherkin.Pickles(*gherkinDocument, path, newIDFunc) 638 639 return &feature{ 640 GherkinDocument: gherkinDocument, 641 pickles: pickles, 642 Content: buf.Bytes(), 643 Path: path, 644 }, nil 645 } 646 647 func parseFeatureDir(dir string, newIDFunc func() string) ([]*feature, error) { 648 var features []*feature 649 return features, filepath.Walk(dir, func(p string, f os.FileInfo, err error) error { 650 if err != nil { 651 return err 652 } 653 654 if f.IsDir() { 655 return nil 656 } 657 658 if !strings.HasSuffix(p, ".feature") { 659 return nil 660 } 661 662 feat, err := parseFeatureFile(p, newIDFunc) 663 if err != nil { 664 return err 665 } 666 features = append(features, feat) 667 return nil 668 }) 669 } 670 671 func parsePath(path string) ([]*feature, error) { 672 var features []*feature 673 674 path, line := extractFeaturePathLine(path) 675 676 fi, err := os.Stat(path) 677 if err != nil { 678 return features, err 679 } 680 681 newIDFunc := (&messages.Incrementing{}).NewId 682 683 if fi.IsDir() { 684 return parseFeatureDir(path, newIDFunc) 685 } 686 687 ft, err := parseFeatureFile(path, newIDFunc) 688 if err != nil { 689 return features, err 690 } 691 692 // filter scenario by line number 693 var pickles []*messages.Pickle 694 for _, pickle := range ft.pickles { 695 sc := ft.findScenario(pickle.AstNodeIds[0]) 696 697 if line == -1 || uint32(line) == sc.Location.Line { 698 pickles = append(pickles, pickle) 699 } 700 } 701 ft.pickles = pickles 702 703 return append(features, ft), nil 704 } 705 706 func parseFeatures(filter string, paths []string) ([]*feature, error) { 707 byPath := make(map[string]*feature) 708 var order int 709 for _, path := range paths { 710 feats, err := parsePath(path) 711 switch { 712 case os.IsNotExist(err): 713 return nil, fmt.Errorf(`feature path "%s" is not available`, path) 714 case os.IsPermission(err): 715 return nil, fmt.Errorf(`feature path "%s" is not accessible`, path) 716 case err != nil: 717 return nil, err 718 } 719 720 for _, ft := range feats { 721 if _, duplicate := byPath[ft.Path]; duplicate { 722 continue 723 } 724 725 ft.order = order 726 order++ 727 byPath[ft.Path] = ft 728 } 729 } 730 731 return filterFeatures(filter, byPath), nil 732 } 733 734 type sortByOrderGiven []*feature 735 736 func (s sortByOrderGiven) Len() int { return len(s) } 737 func (s sortByOrderGiven) Less(i, j int) bool { return s[i].order < s[j].order } 738 func (s sortByOrderGiven) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 739 740 func filterFeatures(tags string, collected map[string]*feature) (features []*feature) { 741 for _, ft := range collected { 742 applyTagFilter(tags, ft) 743 features = append(features, ft) 744 } 745 746 sort.Sort(sortByOrderGiven(features)) 747 748 return features 749 } 750 751 func applyTagFilter(tags string, ft *feature) { 752 if len(tags) == 0 { 753 return 754 } 755 756 var pickles []*messages.Pickle 757 for _, pickle := range ft.pickles { 758 if matchesTags(tags, pickle.Tags) { 759 pickles = append(pickles, pickle) 760 } 761 } 762 763 ft.pickles = pickles 764 } 765 766 // based on http://behat.readthedocs.org/en/v2.5/guides/6.cli.html#gherkin-filters 767 func matchesTags(filter string, tags []*messages.Pickle_PickleTag) (ok bool) { 768 ok = true 769 for _, andTags := range strings.Split(filter, "&&") { 770 var okComma bool 771 for _, tag := range strings.Split(andTags, ",") { 772 tag = strings.Replace(strings.TrimSpace(tag), "@", "", -1) 773 if tag[0] == '~' { 774 tag = tag[1:] 775 okComma = !hasTag(tags, tag) || okComma 776 } else { 777 okComma = hasTag(tags, tag) || okComma 778 } 779 } 780 ok = ok && okComma 781 } 782 return 783 } 784 785 func hasTag(tags []*messages.Pickle_PickleTag, tag string) bool { 786 for _, t := range tags { 787 tName := strings.Replace(t.Name, "@", "", -1) 788 789 if tName == tag { 790 return true 791 } 792 } 793 return false 794 }