use reflection to set step arguments #9

* 9226bc5 more expressive conversion errors
Этот коммит содержится в:
gedi 2015-06-27 22:40:15 +03:00
родитель d7334fd66e
коммит 2046da1611
5 изменённых файлов: 226 добавлений и 228 удалений

Просмотреть файл

@ -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
}

Просмотреть файл

@ -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))
}
}

Просмотреть файл

@ -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)
}

201
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)

Просмотреть файл

@ -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
}
}