Skip to content

Enhancement: Support for custom types + StepParam type #682

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
## Unreleased

### Added
- Enhancement: Support for custom types + StepParam - ([682](https://github.com/cucumber/godog/pull/682) - [tigh-latte](https://github.com/tigh-latte))
- Step text is added to "step is undefined" error - ([669](https://github.com/cucumber/godog/pull/669) - [vearutop](https://github.com/vearutop))

### Fixed
Expand Down
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,58 @@ Now, the test binary can be compiled with all feature files embedded, and can be

**NOTE:** `godog.Options.FS` is as `fs.FS`, so custom filesystem loaders can be used.

### Step Params

As well as accepting `string`, `int`, etc, as parameters to step functions, godog accepts any type which implements `models.StepParam`:

```go
type StepParam interface {
LoadParam(ctx context.Context) (string, error)
}
```

Let's say we have the file `expected.txt`:
```
hello world!
```

And the type, `TestData`:

```go
type TestData string

func (t TestData) LoadParam(ctx context.Context) (string, error) {
data, err := os.ReadFile(string(t))
if err != nil {
return "", fmt.Errorf("failed to load file: %w", err)
}

return string(data), nil
}
```

And the following step:

```go
ctx.Step(`^the file "([^"]+)" should contain "([^"]+)"$`, theFileShouldContain)

// ...

func theFileShouldContain(ctx context.Context, data TestData, exp string) error {
// assert that `string(data)` == `exp`
}
```

Then, once this step is used in a `.feature` file:

```gherkin
Then the file "expected.txt" should contain "hello world!"
```

The `TestFile` param is noticed to implement `models.StepParam`, and `LoadParam` is called. The value of the receive is the value defined in the step function (so, in this case `expected.txt`), and whatever is returned (in this case `hello world!`) is what is ultimately provided to as a parameter to the step's function.

The param loading occurs _after_ the before scenario and before step hooks are called, so all user defined setup will have already taken place.

## CLI Mode

**NOTE:** The [`godog` CLI has been deprecated](https://github.com/cucumber/godog/discussions/478). It is recommended to use `go test` instead.
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
Expand Down
66 changes: 56 additions & 10 deletions internal/models/stepdef.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
"github.com/cucumber/godog/formatters"
)

var typeOfBytes = reflect.TypeOf([]byte(nil))
var (
typeOfBytes = reflect.TypeOf([]byte(nil))
typeOfTextStepParam = reflect.TypeOf((*StepParam)(nil)).Elem()
)

// matchable errors
var (
Expand Down Expand Up @@ -59,6 +62,16 @@

for i := 0; i < numIn; i++ {
param := typ.In(i + ctxOffset)

m, err := sd.tryStepParam(ctx, param, i)
if err != nil {
return ctx, err
}
if m.IsValid() {
values = append(values, m)
continue
}

switch param.Kind() {
case reflect.Int:
s, err := sd.shouldBeString(i)
Expand All @@ -69,7 +82,7 @@
if err != nil {
return ctx, fmt.Errorf(`%w %d: "%s" to int: %s`, ErrCannotConvert, i, s, err)
}
values = append(values, reflect.ValueOf(int(v)))
values = append(values, reflect.ValueOf(int(v)).Convert(param))
case reflect.Int64:
s, err := sd.shouldBeString(i)
if err != nil {
Expand All @@ -79,7 +92,7 @@
if err != nil {
return ctx, fmt.Errorf(`%w %d: "%s" to int64: %s`, ErrCannotConvert, i, s, err)
}
values = append(values, reflect.ValueOf(v))
values = append(values, reflect.ValueOf(v).Convert(param))
case reflect.Int32:
s, err := sd.shouldBeString(i)
if err != nil {
Expand All @@ -89,7 +102,7 @@
if err != nil {
return ctx, fmt.Errorf(`%w %d: "%s" to int32: %s`, ErrCannotConvert, i, s, err)
}
values = append(values, reflect.ValueOf(int32(v)))
values = append(values, reflect.ValueOf(int32(v)).Convert(param))
case reflect.Int16:
s, err := sd.shouldBeString(i)
if err != nil {
Expand All @@ -99,7 +112,7 @@
if err != nil {
return ctx, fmt.Errorf(`%w %d: "%s" to int16: %s`, ErrCannotConvert, i, s, err)
}
values = append(values, reflect.ValueOf(int16(v)))
values = append(values, reflect.ValueOf(int16(v)).Convert(param))
case reflect.Int8:
s, err := sd.shouldBeString(i)
if err != nil {
Expand All @@ -109,13 +122,13 @@
if err != nil {
return ctx, fmt.Errorf(`%w %d: "%s" to int8: %s`, ErrCannotConvert, i, s, err)
}
values = append(values, reflect.ValueOf(int8(v)))
values = append(values, reflect.ValueOf(int8(v)).Convert(param))
case reflect.String:
s, err := sd.shouldBeString(i)
if err != nil {
return ctx, err
}
values = append(values, reflect.ValueOf(s))
values = append(values, reflect.ValueOf(s).Convert(param))
case reflect.Float64:
s, err := sd.shouldBeString(i)
if err != nil {
Expand All @@ -125,7 +138,7 @@
if err != nil {
return ctx, fmt.Errorf(`%w %d: "%s" to float64: %s`, ErrCannotConvert, i, s, err)
}
values = append(values, reflect.ValueOf(v))
values = append(values, reflect.ValueOf(v).Convert(param))
case reflect.Float32:
s, err := sd.shouldBeString(i)
if err != nil {
Expand All @@ -135,10 +148,12 @@
if err != nil {
return ctx, fmt.Errorf(`%w %d: "%s" to float32: %s`, ErrCannotConvert, i, s, err)
}
values = append(values, reflect.ValueOf(float32(v)))
values = append(values, reflect.ValueOf(float32(v)).Convert(param))
case reflect.Ptr:
arg := sd.Args[i]
switch param.Elem().String() {
elem := param.Elem()

switch elem.String() {
case "messages.PickleDocString":
if v, ok := arg.(*messages.PickleStepArgument); ok {
values = append(values, reflect.ValueOf(v.DocString))
Expand Down Expand Up @@ -167,6 +182,7 @@
// 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:
Expand Down Expand Up @@ -230,6 +246,36 @@
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) tryStepParam(ctx context.Context, param reflect.Type, idx int) (reflect.Value, error) {
var val reflect.Value
if !param.Implements(typeOfTextStepParam) {
return val, nil
}

s, err := sd.shouldBeString(idx)
if err != nil {
return val, err
}

Check warning on line 258 in internal/models/stepdef.go

View check run for this annotation

Codecov / codecov/patch

internal/models/stepdef.go#L257-L258

Added lines #L257 - L258 were not covered by tests

if param.Kind() == reflect.Ptr {
val = reflect.ValueOf(&s).Convert(param)
} else {
val = reflect.ValueOf(s).Convert(param)
}

tm := val.Interface().(StepParam)

text, err := tm.LoadParam(ctx)
if err != nil {
return val, fmt.Errorf("failed to load param for arg[%d]: %w", idx, err)
}

if param.Kind() == reflect.Ptr {
return reflect.ValueOf(&text).Convert(param), nil
}
return reflect.ValueOf(text).Convert(param), nil
}

func (sd *StepDefinition) shouldBeString(idx int) (string, error) {
arg := sd.Args[idx]
switch arg := arg.(type) {
Expand Down
Loading
Loading