
* at some point someone changed the return type for nested steps from []string to godog.Steps but they forgot to adjust the type checks. The existing type checks were lax and unable to distinguish []string from godog.Steps but in a couple of places in the code the value is coerced to godog.Steps and so if someone returned []string then the code would blow up. Additionally there were some tests aroudn these types but they also had not been updated but the test was passing for the wrong reason - the particular test expected an error but the cause of the error wasn't the one the code expected. * CHANGELOG.md * use chatgpt to regen the top of the code based on the new tests * use chatgpt to regen the top of the code based on the new tests * corrected the error messages of the param checks to indicate that the problem is the function signature and not the args being passed to the function, also added numerous extra assertions on the precise error messages returned. Now that the precise error is being verified in the test I have improved certain error messages to that more accurate detail is included in the errors * added further constraints to the step arg mapping tests * removed redundant test * include a step error result in the reported error even when the ctx is nil
257 строки
7,5 КиБ
Go
257 строки
7,5 КиБ
Go
package models
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strconv"
|
|
|
|
messages "github.com/cucumber/messages/go/v21"
|
|
|
|
"github.com/cucumber/godog/formatters"
|
|
)
|
|
|
|
var typeOfBytes = reflect.TypeOf([]byte(nil))
|
|
|
|
// matchable errors
|
|
var (
|
|
ErrUnmatchedStepArgumentNumber = errors.New("func expected more arguments than given")
|
|
ErrCannotConvert = errors.New("cannot convert argument")
|
|
ErrUnsupportedParameterType = errors.New("func has unsupported parameter type")
|
|
)
|
|
|
|
// StepDefinition ...
|
|
type StepDefinition struct {
|
|
formatters.StepDefinition
|
|
|
|
Args []interface{}
|
|
HandlerValue reflect.Value
|
|
|
|
// multistep related
|
|
Nested bool
|
|
Undefined []string
|
|
}
|
|
|
|
var typeOfContext = reflect.TypeOf((*context.Context)(nil)).Elem()
|
|
|
|
// Run a step with the matched arguments using reflect
|
|
// Returns one of ...
|
|
// (context, error)
|
|
// (context, godog.Steps)
|
|
func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}) {
|
|
var values []reflect.Value
|
|
|
|
typ := sd.HandlerValue.Type()
|
|
numIn := typ.NumIn()
|
|
hasCtxIn := numIn > 0 && typ.In(0).Implements(typeOfContext)
|
|
ctxOffset := 0
|
|
|
|
if hasCtxIn {
|
|
values = append(values, reflect.ValueOf(ctx))
|
|
ctxOffset = 1
|
|
numIn--
|
|
}
|
|
|
|
if len(sd.Args) < numIn {
|
|
return ctx, fmt.Errorf("%w: expected %d arguments, matched %d from step", ErrUnmatchedStepArgumentNumber, typ.NumIn(), len(sd.Args))
|
|
}
|
|
|
|
for i := 0; i < numIn; i++ {
|
|
param := typ.In(i + ctxOffset)
|
|
switch param.Kind() {
|
|
case reflect.Int:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
v, err := strconv.ParseInt(s, 10, 0)
|
|
if err != nil {
|
|
return ctx, fmt.Errorf(`%w %d: "%s" to int: %s`, ErrCannotConvert, i, s, err)
|
|
}
|
|
values = append(values, reflect.ValueOf(int(v)))
|
|
case reflect.Int64:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
v, err := strconv.ParseInt(s, 10, 64)
|
|
if err != nil {
|
|
return ctx, fmt.Errorf(`%w %d: "%s" to int64: %s`, ErrCannotConvert, i, s, err)
|
|
}
|
|
values = append(values, reflect.ValueOf(v))
|
|
case reflect.Int32:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
v, err := strconv.ParseInt(s, 10, 32)
|
|
if err != nil {
|
|
return ctx, fmt.Errorf(`%w %d: "%s" to int32: %s`, ErrCannotConvert, i, s, err)
|
|
}
|
|
values = append(values, reflect.ValueOf(int32(v)))
|
|
case reflect.Int16:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
v, err := strconv.ParseInt(s, 10, 16)
|
|
if err != nil {
|
|
return ctx, fmt.Errorf(`%w %d: "%s" to int16: %s`, ErrCannotConvert, i, s, err)
|
|
}
|
|
values = append(values, reflect.ValueOf(int16(v)))
|
|
case reflect.Int8:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
v, err := strconv.ParseInt(s, 10, 8)
|
|
if err != nil {
|
|
return ctx, fmt.Errorf(`%w %d: "%s" to int8: %s`, ErrCannotConvert, i, s, err)
|
|
}
|
|
values = append(values, reflect.ValueOf(int8(v)))
|
|
case reflect.String:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
values = append(values, reflect.ValueOf(s))
|
|
case reflect.Float64:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
v, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
return ctx, fmt.Errorf(`%w %d: "%s" to float64: %s`, ErrCannotConvert, i, s, err)
|
|
}
|
|
values = append(values, reflect.ValueOf(v))
|
|
case reflect.Float32:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
v, err := strconv.ParseFloat(s, 32)
|
|
if err != nil {
|
|
return ctx, fmt.Errorf(`%w %d: "%s" to float32: %s`, ErrCannotConvert, i, s, err)
|
|
}
|
|
values = append(values, reflect.ValueOf(float32(v)))
|
|
case reflect.Ptr:
|
|
arg := sd.Args[i]
|
|
switch param.Elem().String() {
|
|
case "messages.PickleDocString":
|
|
if v, ok := arg.(*messages.PickleStepArgument); ok {
|
|
values = append(values, reflect.ValueOf(v.DocString))
|
|
break
|
|
}
|
|
|
|
if v, ok := arg.(*messages.PickleDocString); ok {
|
|
values = append(values, reflect.ValueOf(v))
|
|
break
|
|
}
|
|
|
|
return ctx, fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleDocString`, ErrCannotConvert, i, arg, arg)
|
|
case "messages.PickleTable":
|
|
if v, ok := arg.(*messages.PickleStepArgument); ok {
|
|
values = append(values, reflect.ValueOf(v.DataTable))
|
|
break
|
|
}
|
|
|
|
if v, ok := arg.(*messages.PickleTable); ok {
|
|
values = append(values, reflect.ValueOf(v))
|
|
break
|
|
}
|
|
|
|
return ctx, fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleTable`, ErrCannotConvert, i, arg, arg)
|
|
default:
|
|
// the error here is that the declared function has an unsupported param type - really this ought to be trapped at registration ti,e
|
|
return ctx, fmt.Errorf("%w: the data type of parameter %d type *%s is not supported", ErrUnsupportedParameterType, i, param.Elem().String())
|
|
}
|
|
case reflect.Slice:
|
|
switch param {
|
|
case typeOfBytes:
|
|
s, err := sd.shouldBeString(i)
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
values = append(values, reflect.ValueOf([]byte(s)))
|
|
default:
|
|
// the problem is the function decl is not using a support slice type as the param
|
|
return ctx, fmt.Errorf("%w: the slice parameter %d type []%s is not supported", ErrUnsupportedParameterType, i, param.Elem().Kind())
|
|
}
|
|
case reflect.Struct:
|
|
return ctx, fmt.Errorf("%w: the struct parameter %d type %s is not supported", ErrUnsupportedParameterType, i, param.String())
|
|
default:
|
|
return ctx, fmt.Errorf("%w: the parameter %d type %s is not supported", ErrUnsupportedParameterType, i, param.Kind())
|
|
}
|
|
}
|
|
|
|
res := sd.HandlerValue.Call(values)
|
|
if len(res) == 0 {
|
|
return ctx, nil
|
|
}
|
|
|
|
// Note that the step fn return types were validated at Initialise in test_context.go stepWithKeyword()
|
|
|
|
// single return value may be one of ...
|
|
// error
|
|
// context.Context
|
|
// godog.Steps
|
|
result0 := res[0].Interface()
|
|
if len(res) == 1 {
|
|
|
|
// if the single return value is a context then just return it
|
|
if ctx, ok := result0.(context.Context); ok {
|
|
return ctx, nil
|
|
}
|
|
|
|
// return type is presumably one of nil, "error" or "Steps" so place it into second return position
|
|
return ctx, result0
|
|
}
|
|
|
|
// multi-value value return must be
|
|
// (context, error) and the context value must not be nil
|
|
if ctx, ok := result0.(context.Context); ok {
|
|
return ctx, res[1].Interface()
|
|
}
|
|
|
|
result1 := res[1].Interface()
|
|
errMsg := ""
|
|
if result1 != nil {
|
|
errMsg = fmt.Sprintf(", step def also returned an error: %v", result1)
|
|
}
|
|
|
|
text := sd.StepDefinition.Expr.String()
|
|
|
|
if result0 == nil {
|
|
panic(fmt.Sprintf("step definition '%v' with return type (context.Context, error) must not return <nil> for the context.Context value%s", text, errMsg))
|
|
}
|
|
|
|
panic(fmt.Errorf("step definition '%v' has return type (context.Context, error), but found %v rather than a context.Context value%s", text, result0, errMsg))
|
|
}
|
|
|
|
func (sd *StepDefinition) shouldBeString(idx int) (string, error) {
|
|
arg := sd.Args[idx]
|
|
switch arg := arg.(type) {
|
|
case string:
|
|
return arg, nil
|
|
case *messages.PickleStepArgument:
|
|
if arg.DocString == nil {
|
|
return "", fmt.Errorf(`%w %d: "%v" of type "%T": DocString is not set`, ErrCannotConvert, idx, arg, arg)
|
|
}
|
|
return arg.DocString.Content, nil
|
|
case *messages.PickleDocString:
|
|
return arg.Content, nil
|
|
default:
|
|
return "", fmt.Errorf(`%w %d: "%v" of type "%T" to string`, ErrCannotConvert, idx, arg, arg)
|
|
}
|
|
}
|
|
|
|
// GetInternalStepDefinition ...
|
|
func (sd *StepDefinition) GetInternalStepDefinition() *formatters.StepDefinition {
|
|
if sd == nil {
|
|
return nil
|
|
}
|
|
|
|
return &sd.StepDefinition
|
|
}
|