From 3e8861a4add7be626cbff218172121f7f8a8e9f2 Mon Sep 17 00:00:00 2001 From: ckoerckel Date: Wed, 25 Sep 2024 13:08:46 +0200 Subject: [PATCH] feat: Add support for step data tables (#154) --- features/datatable.feature | 12 ++++ gobdd.go | 110 ++++++++++++++++++++++++++++++------- gobdd_test.go | 23 ++++++++ 3 files changed, 124 insertions(+), 21 deletions(-) create mode 100644 features/datatable.feature diff --git a/features/datatable.feature b/features/datatable.feature new file mode 100644 index 0000000..24e25f7 --- /dev/null +++ b/features/datatable.feature @@ -0,0 +1,12 @@ +Feature: dataTable feature + Scenario: compare text with dataTable + When I concat all the columns and row together using " - " to separate the columns + |r1c1|r1c2|r1c3| + |r2c1|r2c2|r2c3| + |r3c1|r3c2|r3c3| + Then the result should equal argument: + """ + r1c1 - r1c2 - r1c3 + r2c1 - r2c2 - r2c3 + r3c1 - r3c2 - r3c3 + """ \ No newline at end of file diff --git a/gobdd.go b/gobdd.go index aab3987..407035c 100644 --- a/gobdd.go +++ b/gobdd.go @@ -572,9 +572,17 @@ func (s *Suite) runStep(ctx Context, t *testing.T, step *msgs.Step) { t.Fatalf("cannot find step definition for step: %s%s", step.Keyword, step.Text) } - params := def.expr.FindSubmatch([]byte(step.Text))[1:] + matches := def.expr.FindSubmatch([]byte(step.Text))[1:] + params := make([]interface{}, 0, len(matches)) // defining the slices capacity instead of the length to use append + for _, m := range matches { + params = append(params, m) + } + if step.DocString != nil { - params = append(params, []byte(step.DocString.Content)) + params = append(params, step.DocString.Content) + } + if step.DataTable != nil { + params = append(params, *step.DataTable) } t.Run(fmt.Sprintf("%s %s", strings.TrimSpace(step.Keyword), step.Text), func(t *testing.T) { @@ -589,7 +597,7 @@ func (s *Suite) runStep(ctx Context, t *testing.T, step *msgs.Step) { }) } -func (def *stepDef) run(ctx Context, t TestingT, params [][]byte) { // nolint:interfacer +func (def *stepDef) run(ctx Context, t TestingT, params []interface{}) { // nolint:interfacer defer func() { if r := recover(); r != nil { t.Errorf("%+v", r) @@ -611,38 +619,98 @@ func (def *stepDef) run(ctx Context, t TestingT, params [][]byte) { // nolint:in } inType := d.Type().In(i + 2) - paramType := paramType(v, inType) + + paramType, err := paramType(v, inType) + if err != nil { + t.Fatal(err) + } + in = append(in, paramType) } d.Call(in) } -func paramType(param []byte, inType reflect.Type) reflect.Value { - paramType := reflect.ValueOf(param) - if inType.Kind() == reflect.String { - paramType = reflect.ValueOf(string(paramType.Interface().([]uint8))) +func paramType(param interface{}, inType reflect.Type) (reflect.Value, error) { + switch inType.Kind() { // nolint:exhaustive - the linter does not recognize 'default:' to satisfy exhaustiveness + case reflect.String: + s, err := shouldBeString(param) + return reflect.ValueOf(s), err + case reflect.Int: + v, err := shouldBeInt(param) + return reflect.ValueOf(v), err + case reflect.Float32: + v, err := shouldBeFloat(param, 32) + return reflect.ValueOf(float32(v)), err + case reflect.Float64: + v, err := shouldBeFloat(param, 64) + return reflect.ValueOf(v), err + case reflect.Slice: + // only []byte is supported + if inType != reflect.TypeOf([]byte(nil)) { + return reflect.Value{}, fmt.Errorf("the slice argument type %s is not supported", inType.Kind()) + } + + v, err := shouldBeByteSlice(param) + + return reflect.ValueOf(v), err + case reflect.Struct: + // the only struct supported is the one introduced by cucumber + if inType != reflect.TypeOf(msgs.DataTable{}) { + return reflect.Value{}, fmt.Errorf("the struct argument type %s is not supported", inType.Kind()) + } + + v, err := shouldBeDataTable(param) + + return reflect.ValueOf(v), err + default: + return reflect.Value{}, fmt.Errorf("the type %s is not supported", inType.Kind()) } +} - if inType.Kind() == reflect.Int { - s := paramType.Interface().([]uint8) - p, _ := strconv.Atoi(string(s)) - paramType = reflect.ValueOf(p) +func shouldBeDataTable(input interface{}) (msgs.DataTable, error) { + if v, ok := input.(msgs.DataTable); ok { + return v, nil } - if inType.Kind() == reflect.Float32 { - s := paramType.Interface().([]uint8) - p, _ := strconv.ParseFloat(string(s), 32) - paramType = reflect.ValueOf(float32(p)) + return msgs.DataTable{}, fmt.Errorf("cannot convert %v of type %T to messages.DataTable", input, input) +} + +func shouldBeByteSlice(input interface{}) ([]byte, error) { + if v, ok := input.([]byte); ok { + return v, nil } - if inType.Kind() == reflect.Float64 { - s := paramType.Interface().([]uint8) - p, _ := strconv.ParseFloat(string(s), 32) - paramType = reflect.ValueOf(p) + return nil, fmt.Errorf("cannot convert %v of type %T to []byte", input, input) +} + +func shouldBeInt(input interface{}) (int, error) { + s, err := shouldBeString(input) + if err != nil { + return 0, err } - return paramType + return strconv.Atoi(s) +} + +func shouldBeFloat(input interface{}, bitSize int) (float64, error) { + s, err := shouldBeString(input) + if err != nil { + return 0, err + } + + return strconv.ParseFloat(s, bitSize) +} + +func shouldBeString(input interface{}) (string, error) { + switch v := input.(type) { + case string: + return v, nil + case []byte: + return string(v), nil + default: + return "", fmt.Errorf("cannot convert %v of type %T to string", input, input) + } } func (s *Suite) findStepDef(text string) (stepDef, error) { diff --git a/gobdd_test.go b/gobdd_test.go index 46c1366..9bf3cdc 100644 --- a/gobdd_test.go +++ b/gobdd_test.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "regexp" + "strings" "testing" msgs "github.com/cucumber/messages/go/v24" @@ -84,6 +85,14 @@ func TestArguments(t *testing.T) { suite.Run() } +func TestDatatable(t *testing.T) { + suite := NewSuite(t, WithFeaturesPath("features/datatable.feature")) + suite.AddStep(`I concat all the columns and row together using {text} to separate the columns`, concatTable) + suite.AddStep(`the result should equal argument:`, checkt) + + suite.Run() +} + func TestScenarioOutlineExecutesAllTests(t *testing.T) { c := 0 suite := NewSuite(t, WithFeaturesPath("features/outline.feature")) @@ -299,6 +308,20 @@ func concat(_ StepTest, ctx Context, var1, var2 string) { ctx.Set("stringRes", var1+var2) } +func concatTable(_ StepTest, ctx Context, separator string, table msgs.DataTable) { + rows := make([]string, 0, len(table.Rows)) + for _, row := range table.Rows { + values := make([]string, 0, len(row.Cells)) + for _, cell := range row.Cells { + values = append(values, cell.Value) + } + + rows = append(rows, strings.Join(values, separator)) + } + + ctx.Set("stringRes", strings.Join(rows, "\n")) +} + func checkt(t StepTest, ctx Context, text string) { received, err := ctx.GetString("stringRes") if err != nil {