286 строки
6,3 КиБ
Go
286 строки
6,3 КиБ
Go
package godog
|
|
|
|
import (
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/cucumber/messages-go/v10"
|
|
|
|
"github.com/cucumber/godog/internal/models"
|
|
"github.com/cucumber/godog/internal/storage"
|
|
"github.com/cucumber/godog/internal/utils"
|
|
)
|
|
|
|
var errorInterface = reflect.TypeOf((*error)(nil)).Elem()
|
|
|
|
// ErrUndefined is returned in case if step definition was not found
|
|
var ErrUndefined = fmt.Errorf("step is undefined")
|
|
|
|
// ErrPending should be returned by step definition if
|
|
// step implementation is pending
|
|
var ErrPending = fmt.Errorf("step implementation is pending")
|
|
|
|
type suite struct {
|
|
steps []*models.StepDefinition
|
|
|
|
fmt Formatter
|
|
storage *storage.Storage
|
|
|
|
failed bool
|
|
randomSeed int64
|
|
stopOnFailure bool
|
|
strict bool
|
|
|
|
// suite event handlers
|
|
beforeScenarioHandlers []func(*Scenario)
|
|
beforeStepHandlers []func(*Step)
|
|
afterStepHandlers []func(*Step, error)
|
|
afterScenarioHandlers []func(*Scenario, error)
|
|
}
|
|
|
|
func (s *suite) matchStep(step *messages.Pickle_PickleStep) *models.StepDefinition {
|
|
def := s.matchStepText(step.Text)
|
|
if def != nil && step.Argument != nil {
|
|
def.Args = append(def.Args, step.Argument)
|
|
}
|
|
return def
|
|
}
|
|
|
|
func (s *suite) runStep(pickle *messages.Pickle, step *messages.Pickle_PickleStep, prevStepErr error) (err error) {
|
|
// run before step handlers
|
|
for _, f := range s.beforeStepHandlers {
|
|
f(step)
|
|
}
|
|
|
|
match := s.matchStep(step)
|
|
s.fmt.Defined(pickle, step, match)
|
|
|
|
// user multistep definitions may panic
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
err = &traceError{
|
|
msg: fmt.Sprintf("%v", e),
|
|
stack: callStack(),
|
|
}
|
|
}
|
|
|
|
if prevStepErr != nil {
|
|
return
|
|
}
|
|
|
|
if err == ErrUndefined {
|
|
return
|
|
}
|
|
|
|
sr := models.NewStepResult(pickle.Id, step.Id, match)
|
|
|
|
switch err {
|
|
case nil:
|
|
sr.Status = models.Passed
|
|
s.storage.MustInsertPickleStepResult(sr)
|
|
|
|
s.fmt.Passed(pickle, step, match)
|
|
case ErrPending:
|
|
sr.Status = models.Pending
|
|
s.storage.MustInsertPickleStepResult(sr)
|
|
|
|
s.fmt.Pending(pickle, step, match)
|
|
default:
|
|
sr.Status = models.Failed
|
|
sr.Err = err
|
|
s.storage.MustInsertPickleStepResult(sr)
|
|
|
|
s.fmt.Failed(pickle, step, match, err)
|
|
}
|
|
|
|
// run after step handlers
|
|
for _, f := range s.afterStepHandlers {
|
|
f(step, err)
|
|
}
|
|
}()
|
|
|
|
if undef, err := s.maybeUndefined(step.Text, step.Argument); err != nil {
|
|
return err
|
|
} else if len(undef) > 0 {
|
|
if match != nil {
|
|
match = &models.StepDefinition{
|
|
Args: match.Args,
|
|
HandlerValue: match.HandlerValue,
|
|
Expr: match.Expr,
|
|
Handler: match.Handler,
|
|
Nested: match.Nested,
|
|
Undefined: undef,
|
|
}
|
|
}
|
|
|
|
sr := models.NewStepResult(pickle.Id, step.Id, match)
|
|
sr.Status = models.Undefined
|
|
s.storage.MustInsertPickleStepResult(sr)
|
|
|
|
s.fmt.Undefined(pickle, step, match)
|
|
return ErrUndefined
|
|
}
|
|
|
|
if prevStepErr != nil {
|
|
sr := models.NewStepResult(pickle.Id, step.Id, match)
|
|
sr.Status = models.Skipped
|
|
s.storage.MustInsertPickleStepResult(sr)
|
|
|
|
s.fmt.Skipped(pickle, step, match)
|
|
return nil
|
|
}
|
|
|
|
err = s.maybeSubSteps(match.Run())
|
|
return
|
|
}
|
|
|
|
func (s *suite) maybeUndefined(text string, arg interface{}) ([]string, error) {
|
|
step := s.matchStepText(text)
|
|
if nil == step {
|
|
return []string{text}, nil
|
|
}
|
|
|
|
var undefined []string
|
|
if !step.Nested {
|
|
return undefined, nil
|
|
}
|
|
|
|
if arg != nil {
|
|
step.Args = append(step.Args, arg)
|
|
}
|
|
|
|
for _, next := range step.Run().(Steps) {
|
|
lines := strings.Split(next, "\n")
|
|
// @TODO: we cannot currently parse table or content body from nested steps
|
|
if len(lines) > 1 {
|
|
return undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument")
|
|
}
|
|
if len(lines[0]) > 0 && lines[0][len(lines[0])-1] == ':' {
|
|
return undefined, fmt.Errorf("nested steps cannot be multiline and have table or content body argument")
|
|
}
|
|
undef, err := s.maybeUndefined(next, nil)
|
|
if err != nil {
|
|
return undefined, err
|
|
}
|
|
undefined = append(undefined, undef...)
|
|
}
|
|
return undefined, nil
|
|
}
|
|
|
|
func (s *suite) maybeSubSteps(result interface{}) error {
|
|
if nil == result {
|
|
return nil
|
|
}
|
|
|
|
if err, ok := result.(error); ok {
|
|
return err
|
|
}
|
|
|
|
steps, ok := result.(Steps)
|
|
if !ok {
|
|
return fmt.Errorf("unexpected error, should have been []string: %T - %+v", result, result)
|
|
}
|
|
|
|
for _, text := range steps {
|
|
if def := s.matchStepText(text); def == nil {
|
|
return ErrUndefined
|
|
} else if err := s.maybeSubSteps(def.Run()); err != nil {
|
|
return fmt.Errorf("%s: %+v", text, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *suite) matchStepText(text string) *models.StepDefinition {
|
|
for _, h := range s.steps {
|
|
if m := h.Expr.FindStringSubmatch(text); len(m) > 0 {
|
|
var args []interface{}
|
|
for _, m := range m[1:] {
|
|
args = append(args, m)
|
|
}
|
|
|
|
// since we need to assign arguments
|
|
// better to copy the step definition
|
|
return &models.StepDefinition{
|
|
Args: args,
|
|
HandlerValue: h.HandlerValue,
|
|
Expr: h.Expr,
|
|
Handler: h.Handler,
|
|
Nested: h.Nested,
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *suite) runSteps(pickle *messages.Pickle, steps []*messages.Pickle_PickleStep) (err error) {
|
|
for _, step := range steps {
|
|
stepErr := s.runStep(pickle, step, err)
|
|
switch stepErr {
|
|
case ErrUndefined:
|
|
// do not overwrite failed error
|
|
if err == ErrUndefined || err == nil {
|
|
err = stepErr
|
|
}
|
|
case ErrPending:
|
|
err = stepErr
|
|
case nil:
|
|
default:
|
|
err = stepErr
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s *suite) shouldFail(err error) bool {
|
|
if err == nil {
|
|
return false
|
|
}
|
|
|
|
if err == ErrUndefined || err == ErrPending {
|
|
return s.strict
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func isEmptyFeature(pickles []*messages.Pickle) bool {
|
|
for _, pickle := range pickles {
|
|
if len(pickle.Steps) > 0 {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func (s *suite) runPickle(pickle *messages.Pickle) (err error) {
|
|
if len(pickle.Steps) == 0 {
|
|
pr := models.PickleResult{PickleID: pickle.Id, StartedAt: utils.TimeNowFunc()}
|
|
s.storage.MustInsertPickleResult(pr)
|
|
|
|
s.fmt.Pickle(pickle)
|
|
return ErrUndefined
|
|
}
|
|
|
|
// run before scenario handlers
|
|
for _, f := range s.beforeScenarioHandlers {
|
|
f(pickle)
|
|
}
|
|
|
|
pr := models.PickleResult{PickleID: pickle.Id, StartedAt: utils.TimeNowFunc()}
|
|
s.storage.MustInsertPickleResult(pr)
|
|
|
|
s.fmt.Pickle(pickle)
|
|
|
|
// scenario
|
|
err = s.runSteps(pickle, pickle.Steps)
|
|
|
|
// run after scenario handlers
|
|
for _, f := range s.afterScenarioHandlers {
|
|
f(pickle, err)
|
|
}
|
|
|
|
return
|
|
}
|