From 2046da161156b6f2ce02dec5b4bf722c527025cd Mon Sep 17 00:00:00 2001 From: gedi Date: Sat, 27 Jun 2015 22:40:15 +0300 Subject: [PATCH] use reflection to set step arguments #9 * 9226bc5 more expressive conversion errors --- README.md | 14 ++-- arguments.go | 143 -------------------------------- example/ls_test.go | 19 +++-- suite.go | 201 ++++++++++++++++++++++++++++++++++++++------- suite_test.go | 77 +++++++++-------- 5 files changed, 226 insertions(+), 228 deletions(-) delete mode 100644 arguments.go diff --git a/README.md b/README.md index e125538..c168be8 100644 --- a/README.md +++ b/README.md @@ -79,19 +79,19 @@ func (c *GodogCart) resetReserve(interface{}) { c.reserve = 0 } -func (c *GodogCart) thereAreNumGodogsInReserve(args ...*godog.Arg) error { - c.reserve = args[0].Int() +func (c *GodogCart) thereAreNumGodogsInReserve(avail int) error { + c.reserve = avail return nil } -func (c *GodogCart) iEatNum(args ...*godog.Arg) error { - c.Eat(args[0].Int()) +func (c *GodogCart) iEatNum(num int) error { + c.Eat(num) return nil } -func (c *GodogCart) thereShouldBeNumRemaining(args ...*godog.Arg) error { - if c.Available() != args[0].Int() { - return fmt.Errorf("expected %d godogs to be remaining, but there is %d", args[0].Int(), c.Available()) +func (c *GodogCart) thereShouldBeNumRemaining(left int) error { + if c.Available() != left { + return fmt.Errorf("expected %d godogs to be remaining, but there is %d", left, c.Available()) } return nil } diff --git a/arguments.go b/arguments.go deleted file mode 100644 index 024b9ab..0000000 --- a/arguments.go +++ /dev/null @@ -1,143 +0,0 @@ -package godog - -import ( - "fmt" - "strconv" - - "github.com/cucumber/gherkin-go" -) - -// Arg is an argument for StepHandler parsed from -// the regexp submatch to handle the step. -// -// In future versions, it may be replaced with -// an argument injection toolkit using reflect -// package. -type Arg struct { - value interface{} -} - -// StepArgument func creates a step argument. -// used in cases when calling another step from -// within a StepHandler function. -func StepArgument(value interface{}) *Arg { - return &Arg{value: value} -} - -// Float64 converts an argument to float64 -// or panics if unable to convert it -func (a *Arg) Float64() float64 { - s, ok := a.value.(string) - a.must(ok, "string") - v, err := strconv.ParseFloat(s, 64) - if err == nil { - return v - } - panic(fmt.Sprintf(`cannot convert "%s" to float64: %s`, s, err)) -} - -// Float32 converts an argument to float32 -// or panics if unable to convert it -func (a *Arg) Float32() float32 { - s, ok := a.value.(string) - a.must(ok, "string") - v, err := strconv.ParseFloat(s, 32) - if err == nil { - return float32(v) - } - panic(fmt.Sprintf(`cannot convert "%s" to float32: %s`, s, err)) -} - -// Int converts an argument to int -// or panics if unable to convert it -func (a *Arg) Int() int { - s, ok := a.value.(string) - a.must(ok, "string") - v, err := strconv.ParseInt(s, 10, 0) - if err == nil { - return int(v) - } - panic(fmt.Sprintf(`cannot convert "%s" to int: %s`, s, err)) -} - -// Int64 converts an argument to int64 -// or panics if unable to convert it -func (a *Arg) Int64() int64 { - s, ok := a.value.(string) - a.must(ok, "string") - v, err := strconv.ParseInt(s, 10, 64) - if err == nil { - return v - } - panic(fmt.Sprintf(`cannot convert "%s" to int64: %s`, s, err)) -} - -// Int32 converts an argument to int32 -// or panics if unable to convert it -func (a *Arg) Int32() int32 { - s, ok := a.value.(string) - a.must(ok, "string") - v, err := strconv.ParseInt(s, 10, 32) - if err == nil { - return int32(v) - } - panic(fmt.Sprintf(`cannot convert "%s" to int32: %s`, s, err)) -} - -// Int16 converts an argument to int16 -// or panics if unable to convert it -func (a *Arg) Int16() int16 { - s, ok := a.value.(string) - a.must(ok, "string") - v, err := strconv.ParseInt(s, 10, 16) - if err == nil { - return int16(v) - } - panic(fmt.Sprintf(`cannot convert "%s" to int16: %s`, s, err)) -} - -// Int8 converts an argument to int8 -// or panics if unable to convert it -func (a *Arg) Int8() int8 { - s, ok := a.value.(string) - a.must(ok, "string") - v, err := strconv.ParseInt(s, 10, 8) - if err == nil { - return int8(v) - } - panic(fmt.Sprintf(`cannot convert "%s" to int8: %s`, s, err)) -} - -// String converts an argument to string -func (a *Arg) String() string { - s, ok := a.value.(string) - a.must(ok, "string") - return s -} - -// Bytes converts an argument string to bytes -func (a *Arg) Bytes() []byte { - s, ok := a.value.(string) - a.must(ok, "string") - return []byte(s) -} - -// DocString converts an argument to *gherkin.DocString node -func (a *Arg) DocString() *gherkin.DocString { - s, ok := a.value.(*gherkin.DocString) - a.must(ok, "*gherkin.DocString") - return s -} - -// DataTable converts an argument to *gherkin.DataTable node -func (a *Arg) DataTable() *gherkin.DataTable { - s, ok := a.value.(*gherkin.DataTable) - a.must(ok, "*gherkin.DataTable") - return s -} - -func (a *Arg) must(ok bool, expected string) { - if !ok { - panic(fmt.Sprintf(`cannot convert "%v" of type "%T" to type "%s"`, a.value, a.value, expected)) - } -} diff --git a/example/ls_test.go b/example/ls_test.go index 55ab370..60b2181 100644 --- a/example/ls_test.go +++ b/example/ls_test.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/DATA-DOG/godog" + "github.com/cucumber/gherkin-go" ) type lsFeature struct { @@ -24,29 +25,29 @@ func lsFeatureContext(s godog.Suite) { s.Step(`^I should get output:$`, c.iShouldGetOutput) } -func (f *lsFeature) iAmInDirectory(args ...*godog.Arg) error { - f.dir = os.TempDir() + "/" + args[0].String() +func (f *lsFeature) iAmInDirectory(name string) error { + f.dir = os.TempDir() + "/" + name if err := os.RemoveAll(f.dir); err != nil && !os.IsNotExist(err) { return err } return os.Mkdir(f.dir, 0775) } -func (f *lsFeature) iHaveFileOrDirectoryNamed(args ...*godog.Arg) (err error) { +func (f *lsFeature) iHaveFileOrDirectoryNamed(typ, name string) (err error) { if len(f.dir) == 0 { return fmt.Errorf("the directory was not chosen yet") } - switch args[0].String() { + switch typ { case "file": - err = ioutil.WriteFile(f.dir+"/"+args[1].String(), []byte{}, 0664) + err = ioutil.WriteFile(f.dir+"/"+name, []byte{}, 0664) case "directory": - err = os.Mkdir(f.dir+"/"+args[1].String(), 0775) + err = os.Mkdir(f.dir+"/"+name, 0775) } return err } -func (f *lsFeature) iShouldGetOutput(args ...*godog.Arg) error { - expected := strings.Split(args[0].DocString().Content, "\n") +func (f *lsFeature) iShouldGetOutput(names *gherkin.DocString) error { + expected := strings.Split(names.Content, "\n") actual := strings.Split(strings.TrimSpace(f.buf.String()), "\n") if len(expected) != len(actual) { return fmt.Errorf("number of expected output lines %d, does not match actual: %d", len(expected), len(actual)) @@ -59,7 +60,7 @@ func (f *lsFeature) iShouldGetOutput(args ...*godog.Arg) error { return nil } -func (f *lsFeature) iRunLs(args ...*godog.Arg) error { +func (f *lsFeature) iRunLs() error { f.buf.Reset() return ls(f.dir, f.buf) } diff --git a/suite.go b/suite.go index f9f3c19..d7126b6 100644 --- a/suite.go +++ b/suite.go @@ -13,28 +13,13 @@ import ( "github.com/cucumber/gherkin-go" ) +var errorInterface = reflect.TypeOf((*error)(nil)).Elem() + type feature struct { *gherkin.Feature Path string `json:"path"` } -// Regexp is an unified type for regular expression -// it can be either a string or a *regexp.Regexp -type Regexp interface{} - -// StepHandler is a func to handle the step -// -// The handler receives all arguments which -// will be matched according to the Regexp -// which is passed with a step registration. -// -// The error in return - represents a reason of failure. -// All consequent scenario steps are skipped. -// -// Returning signals that the step has finished and that -// the feature runner can move on to the next step. -type StepHandler func(...*Arg) error - // ErrUndefined is returned in case if step definition was not found var ErrUndefined = fmt.Errorf("step is undefined") @@ -47,9 +32,133 @@ var ErrUndefined = fmt.Errorf("step is undefined") // when step is matched and is either failed // or successful type StepDef struct { - Args []*Arg - Handler StepHandler + args []interface{} + hv reflect.Value Expr *regexp.Regexp + Handler interface{} +} + +func (sd *StepDef) run() error { + typ := sd.hv.Type() + if len(sd.args) < typ.NumIn() { + return fmt.Errorf("func expects %d arguments, which is more than %d matched from step", typ.NumIn(), len(sd.args)) + } + var values []reflect.Value + for i := 0; i < typ.NumIn(); i++ { + param := typ.In(i) + switch param.Kind() { + case reflect.Int: + s, err := sd.shouldBeString(i) + if err != nil { + return err + } + v, err := strconv.ParseInt(s, 10, 0) + if err != nil { + return fmt.Errorf(`cannot convert argument %d: "%s" to int: %s`, i, s, err) + } + values = append(values, reflect.ValueOf(int(v))) + case reflect.Int64: + s, err := sd.shouldBeString(i) + if err != nil { + return err + } + v, err := strconv.ParseInt(s, 10, 64) + if err != nil { + return fmt.Errorf(`cannot convert argument %d: "%s" to int64: %s`, i, s, err) + } + values = append(values, reflect.ValueOf(int64(v))) + case reflect.Int32: + s, err := sd.shouldBeString(i) + if err != nil { + return err + } + v, err := strconv.ParseInt(s, 10, 32) + if err != nil { + return fmt.Errorf(`cannot convert argument %d: "%s" to int32: %s`, i, s, err) + } + values = append(values, reflect.ValueOf(int32(v))) + case reflect.Int16: + s, err := sd.shouldBeString(i) + if err != nil { + return err + } + v, err := strconv.ParseInt(s, 10, 16) + if err != nil { + return fmt.Errorf(`cannot convert argument %d: "%s" to int16: %s`, i, s, err) + } + values = append(values, reflect.ValueOf(int16(v))) + case reflect.Int8: + s, err := sd.shouldBeString(i) + if err != nil { + return err + } + v, err := strconv.ParseInt(s, 10, 8) + if err != nil { + return fmt.Errorf(`cannot convert argument %d: "%s" to int8: %s`, i, s, err) + } + values = append(values, reflect.ValueOf(int8(v))) + case reflect.String: + s, err := sd.shouldBeString(i) + if err != nil { + return err + } + values = append(values, reflect.ValueOf(s)) + case reflect.Float64: + s, err := sd.shouldBeString(i) + if err != nil { + return err + } + v, err := strconv.ParseFloat(s, 64) + if err != nil { + return fmt.Errorf(`cannot convert argument %d: "%s" to float64: %s`, i, s, err) + } + values = append(values, reflect.ValueOf(v)) + case reflect.Float32: + s, err := sd.shouldBeString(i) + if err != nil { + return err + } + v, err := strconv.ParseFloat(s, 32) + if err != nil { + return fmt.Errorf(`cannot convert argument %d: "%s" to float32: %s`, i, s, err) + } + values = append(values, reflect.ValueOf(float32(v))) + case reflect.Ptr: + arg := sd.args[i] + switch param.Elem().String() { + case "gherkin.DocString": + v, ok := arg.(*gherkin.DocString) + if !ok { + return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *gherkin.DocString`, i, arg, arg) + } + values = append(values, reflect.ValueOf(v)) + case "gherkin.DataTable": + v, ok := arg.(*gherkin.DataTable) + if !ok { + return fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to *gherkin.DocString`, i, arg, arg) + } + values = append(values, reflect.ValueOf(v)) + default: + return fmt.Errorf("the argument %d type %T is not supported", i, arg) + } + default: + return fmt.Errorf("the argument %d type %s is not supported", i, param.Kind()) + } + } + ret := sd.hv.Call(values)[0].Interface() + if nil == ret { + return nil + } + return ret.(error) +} + +func (sd *StepDef) shouldBeString(idx int) (string, error) { + arg := sd.args[idx] + s, ok := arg.(string) + if !ok { + return "", fmt.Errorf(`cannot convert argument %d: "%v" of type "%T" to string`, idx, arg, arg) + } + return s, nil } // Suite is an interface which allows various contexts @@ -64,14 +173,36 @@ type StepDef struct { // executions are catching panic error since it may // be a context specific error. type Suite interface { + // Run the test suite Run() - Step(expr Regexp, h StepHandler) - // suite events + + // Registers a step which will execute stepFunc + // on step expr match + // + // expr can be either a string or a *regexp.Regexp + // stepFunc is a func to handle the step, arguments + // are set from matched step + Step(expr interface{}, h interface{}) + + // BeforeSuite registers a func to run on initial + // suite startup BeforeSuite(f func()) + + // BeforeScenario registers a func to run before + // every *gherkin.Scenario or *gherkin.ScenarioOutline BeforeScenario(f func(interface{})) + + // BeforeStep register a handler before every step BeforeStep(f func(*gherkin.Step)) + + // AfterStep register a handler after every step AfterStep(f func(*gherkin.Step, error)) + + // AfterScenario registers a func to run after + // every *gherkin.Scenario or *gherkin.ScenarioOutline AfterScenario(f func(interface{}, error)) + + // AfterSuite runs func int the end of tests AfterSuite(f func()) } @@ -117,7 +248,7 @@ func New() Suite { // // If none of the StepHandlers are matched, then // ErrUndefined error will be returned. -func (s *suite) Step(expr Regexp, h StepHandler) { +func (s *suite) Step(expr interface{}, stepFunc interface{}) { var regex *regexp.Regexp switch t := expr.(type) { @@ -131,9 +262,21 @@ func (s *suite) Step(expr Regexp, h StepHandler) { panic(fmt.Sprintf("expecting expr to be a *regexp.Regexp or a string, got type: %T", expr)) } + v := reflect.ValueOf(stepFunc) + typ := v.Type() + if typ.Kind() != reflect.Func { + panic(fmt.Sprintf("expected handler to be func, but got: %T", stepFunc)) + } + if typ.NumOut() != 1 { + panic(fmt.Sprintf("expected handler to return an error, but it has more values in return: %d", typ.NumOut())) + } + if typ.Out(0).Kind() != reflect.Interface || !typ.Out(0).Implements(errorInterface) { + panic(fmt.Sprintf("expected handler to return an error interface, but we have: %s", typ.Out(0).Kind())) + } s.stepHandlers = append(s.stepHandlers, &StepDef{ - Handler: h, + Handler: stepFunc, Expr: regex, + hv: v, }) } @@ -262,14 +405,14 @@ func (s *suite) run() { func (s *suite) matchStep(step *gherkin.Step) *StepDef { for _, h := range s.stepHandlers { if m := h.Expr.FindStringSubmatch(step.Text); len(m) > 0 { - var args []*Arg - for _, a := range m[1:] { - args = append(args, &Arg{value: a}) + var args []interface{} + for _, m := range m[1:] { + args = append(args, m) } if step.Argument != nil { - args = append(args, &Arg{value: step.Argument}) + args = append(args, step.Argument) } - h.Args = args + h.args = args return h } } @@ -293,7 +436,7 @@ func (s *suite) runStep(step *gherkin.Step) (err error) { } }() - if err = match.Handler(match.Args...); err != nil { + if err = match.run(); err != nil { s.fmt.Failed(step, match, err) } else { s.fmt.Passed(step, match) diff --git a/suite_test.go b/suite_test.go index 57e5c10..cc9272b 100644 --- a/suite_test.go +++ b/suite_test.go @@ -2,6 +2,7 @@ package godog import ( "fmt" + "strconv" "strings" "github.com/cucumber/gherkin-go" @@ -56,12 +57,12 @@ func (s *suiteContext) ResetBeforeEachScenario(interface{}) { s.events = []*firedEvent{} } -func (s *suiteContext) followingStepsShouldHave(args ...*Arg) error { - var expected = strings.Split(args[1].DocString().Content, "\n") +func (s *suiteContext) followingStepsShouldHave(status string, steps *gherkin.DocString) error { + var expected = strings.Split(steps.Content, "\n") var actual, unmatched []string var matched []int - switch args[0].String() { + switch status { case "passed": for _, st := range s.fmt.passed { actual = append(actual, st.step.Text) @@ -79,11 +80,11 @@ func (s *suiteContext) followingStepsShouldHave(args ...*Arg) error { actual = append(actual, st.step.Text) } default: - return fmt.Errorf("unexpected step status wanted: %s", args[0].String()) + return fmt.Errorf("unexpected step status wanted: %s", status) } if len(expected) > len(actual) { - return fmt.Errorf("number of expected %s steps: %d is less than actual %s steps: %d", args[0].String(), len(expected), args[0].String(), len(actual)) + return fmt.Errorf("number of expected %s steps: %d is less than actual %s steps: %d", status, len(expected), status, len(actual)) } for _, a := range actual { @@ -111,10 +112,10 @@ func (s *suiteContext) followingStepsShouldHave(args ...*Arg) error { } } - return fmt.Errorf("the steps: %s - is not %s", strings.Join(unmatched, ", "), args[0].String()) + return fmt.Errorf("the steps: %s - is not %s", strings.Join(unmatched, ", "), status) } -func (s *suiteContext) iAmListeningToSuiteEvents(args ...*Arg) error { +func (s *suiteContext) iAmListeningToSuiteEvents() error { s.testedSuite.BeforeSuite(func() { s.events = append(s.events, &firedEvent{"BeforeSuite", []interface{}{}}) }) @@ -136,43 +137,41 @@ func (s *suiteContext) iAmListeningToSuiteEvents(args ...*Arg) error { return nil } -func (s *suiteContext) aFailingStep(...*Arg) error { +func (s *suiteContext) aFailingStep() error { return fmt.Errorf("intentional failure") } // parse a given feature file body as a feature -func (s *suiteContext) aFeatureFile(args ...*Arg) error { - name := args[0].String() - body := args[1].DocString().Content - ft, err := gherkin.ParseFeature(strings.NewReader(body)) +func (s *suiteContext) aFeatureFile(name string, body *gherkin.DocString) error { + ft, err := gherkin.ParseFeature(strings.NewReader(body.Content)) s.testedSuite.features = append(s.testedSuite.features, &feature{Feature: ft, Path: name}) return err } -func (s *suiteContext) featurePath(args ...*Arg) error { - s.testedSuite.paths = append(s.testedSuite.paths, args[0].String()) +func (s *suiteContext) featurePath(path string) error { + s.testedSuite.paths = append(s.testedSuite.paths, path) return nil } -func (s *suiteContext) parseFeatures(args ...*Arg) error { +func (s *suiteContext) parseFeatures() error { return s.testedSuite.parseFeatures() } -func (s *suiteContext) theSuiteShouldHave(args ...*Arg) error { - if s.testedSuite.failed && args[0].String() == "passed" { +func (s *suiteContext) theSuiteShouldHave(state string) error { + if s.testedSuite.failed && state == "passed" { return fmt.Errorf("the feature suite has failed") } - if !s.testedSuite.failed && args[0].String() == "failed" { + if !s.testedSuite.failed && state == "failed" { return fmt.Errorf("the feature suite has passed") } return nil } -func (s *suiteContext) iShouldHaveNumFeatureFiles(args ...*Arg) error { - if len(s.testedSuite.features) != args[0].Int() { - return fmt.Errorf("expected %d features to be parsed, but have %d", args[0].Int(), len(s.testedSuite.features)) +func (s *suiteContext) iShouldHaveNumFeatureFiles(num int, files *gherkin.DocString) error { + if len(s.testedSuite.features) != num { + return fmt.Errorf("expected %d features to be parsed, but have %d", num, len(s.testedSuite.features)) } - expected := strings.Split(args[1].DocString().Content, "\n") + expected := strings.Split(files.Content, "\n") var actual []string for _, ft := range s.testedSuite.features { actual = append(actual, ft.Path) @@ -188,7 +187,7 @@ func (s *suiteContext) iShouldHaveNumFeatureFiles(args ...*Arg) error { return nil } -func (s *suiteContext) iRunFeatureSuite(args ...*Arg) error { +func (s *suiteContext) iRunFeatureSuite() error { if err := s.parseFeatures(); err != nil { return err } @@ -196,31 +195,31 @@ func (s *suiteContext) iRunFeatureSuite(args ...*Arg) error { return nil } -func (s *suiteContext) numScenariosRegistered(args ...*Arg) (err error) { +func (s *suiteContext) numScenariosRegistered(expected int) (err error) { var num int for _, ft := range s.testedSuite.features { num += len(ft.ScenarioDefinitions) } - if num != args[0].Int() { - err = fmt.Errorf("expected %d scenarios to be registered, but got %d", args[0].Int(), num) + if num != expected { + err = fmt.Errorf("expected %d scenarios to be registered, but got %d", expected, num) } return } -func (s *suiteContext) thereWereNumEventsFired(args ...*Arg) error { +func (s *suiteContext) thereWereNumEventsFired(_ string, expected int, typ string) error { var num int for _, event := range s.events { - if event.name == args[2].String() { + if event.name == typ { num++ } } - if num != args[1].Int() { - return fmt.Errorf("expected %d %s events to be fired, but got %d", args[1].Int(), args[2].String(), num) + if num != expected { + return fmt.Errorf("expected %d %s events to be fired, but got %d", expected, typ, num) } return nil } -func (s *suiteContext) thereWasEventTriggeredBeforeScenario(args ...*Arg) error { +func (s *suiteContext) thereWasEventTriggeredBeforeScenario(expected string) error { var found []string for _, event := range s.events { if event.name != "BeforeScenario" { @@ -234,7 +233,7 @@ func (s *suiteContext) thereWasEventTriggeredBeforeScenario(args ...*Arg) error case *gherkin.ScenarioOutline: name = t.Name } - if name == args[0].String() { + if name == expected { return nil } @@ -245,22 +244,20 @@ func (s *suiteContext) thereWasEventTriggeredBeforeScenario(args ...*Arg) error return fmt.Errorf("before scenario event was never triggered or listened") } - return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, args[0].String(), `"`+strings.Join(found, `", "`)+`"`) + return fmt.Errorf(`expected "%s" scenario, but got these fired %s`, expected, `"`+strings.Join(found, `", "`)+`"`) } -func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(args ...*Arg) error { - tbl := args[0].DataTable() +func (s *suiteContext) theseEventsHadToBeFiredForNumberOfTimes(tbl *gherkin.DataTable) error { if len(tbl.Rows[0].Cells) != 2 { return fmt.Errorf("expected two columns for event table row, got: %d", len(tbl.Rows[0].Cells)) } for _, row := range tbl.Rows { - args := []*Arg{ - StepArgument(""), // ignored - StepArgument(row.Cells[1].Value), - StepArgument(row.Cells[0].Value), + num, err := strconv.ParseInt(row.Cells[1].Value, 10, 0) + if err != nil { + return err } - if err := s.thereWereNumEventsFired(args...); err != nil { + if err := s.thereWereNumEventsFired("", int(num), row.Cells[0].Value); err != nil { return err } }