Skip to content

Commit 223efc3

Browse files
authored
Fix some type checks on the signatures of nested step handlers (#647)
* 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
1 parent 8edde7f commit 223efc3

8 files changed

+465
-213
lines changed

CHANGELOG.md

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This document is formatted according to the principles of [Keep A CHANGELOG](htt
88

99
## Unreleased
1010

11+
- Improved the type checking of step return types and improved the error messages - ([647](https://github.com/cucumber/godog/pull/647) - [johnlon](https://github.com/johnlon))
1112
- Ambiguous step definitions will now be detected when strict mode is activated - ([636](https://github.com/cucumber/godog/pull/636) - [johnlon](https://github.com/johnlon))
1213
- Provide support for attachments / embeddings including a new example in the examples dir - ([623](https://github.com/cucumber/godog/pull/623) - [johnlon](https://github.com/johnlon))
1314

internal/formatters/fmt_output_test.go

-3
Original file line numberDiff line numberDiff line change
@@ -179,9 +179,6 @@ func fmtOutputTest(fmtName, testName, featureFilePath string) func(*testing.T) {
179179
expected := normalise(string(expectedOutput))
180180
actual := normalise(buf.String())
181181
assert.Equalf(t, expected, actual, "path: %s", expectOutputPath)
182-
if expected != actual {
183-
println("diff")
184-
}
185182
}
186183
}
187184

internal/models/stepdef.go

+42-9
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ var typeOfBytes = reflect.TypeOf([]byte(nil))
1616

1717
// matchable errors
1818
var (
19-
ErrUnmatchedStepArgumentNumber = errors.New("func received more arguments than expected")
19+
ErrUnmatchedStepArgumentNumber = errors.New("func expected more arguments than given")
2020
ErrCannotConvert = errors.New("cannot convert argument")
21-
ErrUnsupportedArgumentType = errors.New("unsupported argument type")
21+
ErrUnsupportedParameterType = errors.New("func has unsupported parameter type")
2222
)
2323

2424
// StepDefinition ...
@@ -36,6 +36,9 @@ type StepDefinition struct {
3636
var typeOfContext = reflect.TypeOf((*context.Context)(nil)).Elem()
3737

3838
// Run a step with the matched arguments using reflect
39+
// Returns one of ...
40+
// (context, error)
41+
// (context, godog.Steps)
3942
func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}) {
4043
var values []reflect.Value
4144

@@ -161,7 +164,8 @@ func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}
161164

162165
return ctx, fmt.Errorf(`%w %d: "%v" of type "%T" to *messages.PickleTable`, ErrCannotConvert, i, arg, arg)
163166
default:
164-
return ctx, fmt.Errorf("%w: the argument %d type %T is not supported %s", ErrUnsupportedArgumentType, i, arg, param.Elem().String())
167+
// the error here is that the declared function has an unsupported param type - really this ought to be trapped at registration ti,e
168+
return ctx, fmt.Errorf("%w: the data type of parameter %d type *%s is not supported", ErrUnsupportedParameterType, i, param.Elem().String())
165169
}
166170
case reflect.Slice:
167171
switch param {
@@ -172,10 +176,13 @@ func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}
172176
}
173177
values = append(values, reflect.ValueOf([]byte(s)))
174178
default:
175-
return ctx, fmt.Errorf("%w: the slice argument %d type %s is not supported", ErrUnsupportedArgumentType, i, param.Kind())
179+
// the problem is the function decl is not using a support slice type as the param
180+
return ctx, fmt.Errorf("%w: the slice parameter %d type []%s is not supported", ErrUnsupportedParameterType, i, param.Elem().Kind())
176181
}
182+
case reflect.Struct:
183+
return ctx, fmt.Errorf("%w: the struct parameter %d type %s is not supported", ErrUnsupportedParameterType, i, param.String())
177184
default:
178-
return ctx, fmt.Errorf("%w: the argument %d type %s is not supported", ErrUnsupportedArgumentType, i, param.Kind())
185+
return ctx, fmt.Errorf("%w: the parameter %d type %s is not supported", ErrUnsupportedParameterType, i, param.Kind())
179186
}
180187
}
181188

@@ -184,17 +191,43 @@ func (sd *StepDefinition) Run(ctx context.Context) (context.Context, interface{}
184191
return ctx, nil
185192
}
186193

194+
// Note that the step fn return types were validated at Initialise in test_context.go stepWithKeyword()
195+
196+
// single return value may be one of ...
197+
// error
198+
// context.Context
199+
// godog.Steps
200+
result0 := res[0].Interface()
187201
if len(res) == 1 {
188-
r := res[0].Interface()
189202

190-
if ctx, ok := r.(context.Context); ok {
203+
// if the single return value is a context then just return it
204+
if ctx, ok := result0.(context.Context); ok {
191205
return ctx, nil
192206
}
193207

194-
return ctx, res[0].Interface()
208+
// return type is presumably one of nil, "error" or "Steps" so place it into second return position
209+
return ctx, result0
210+
}
211+
212+
// multi-value value return must be
213+
// (context, error) and the context value must not be nil
214+
if ctx, ok := result0.(context.Context); ok {
215+
return ctx, res[1].Interface()
216+
}
217+
218+
result1 := res[1].Interface()
219+
errMsg := ""
220+
if result1 != nil {
221+
errMsg = fmt.Sprintf(", step def also returned an error: %v", result1)
222+
}
223+
224+
text := sd.StepDefinition.Expr.String()
225+
226+
if result0 == nil {
227+
panic(fmt.Sprintf("step definition '%v' with return type (context.Context, error) must not return <nil> for the context.Context value%s", text, errMsg))
195228
}
196229

197-
return res[0].Interface().(context.Context), res[1].Interface()
230+
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))
198231
}
199232

200233
func (sd *StepDefinition) shouldBeString(idx int) (string, error) {

0 commit comments

Comments
 (0)