diff --git a/.github/workflows/cloc.yml b/.github/workflows/cloc.yml index 3f1fc19..e0c3825 100644 --- a/.github/workflows/cloc.yml +++ b/.github/workflows/cloc.yml @@ -13,11 +13,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: path: pr - name: Checkout base code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.base.sha }} path: base diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 9acf314..f435fe8 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -19,15 +19,15 @@ jobs: name: golangci-lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: - go-version: 1.23.x - - uses: actions/checkout@v2 + go-version: stable + - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v6.1.0 + uses: golangci/golangci-lint-action@v8.0.0 with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. - version: v1.61.0 + version: v2.4.0 # Optional: working directory, useful for monorepos # working-directory: somedir diff --git a/.github/workflows/gorelease.yml b/.github/workflows/gorelease.yml index c031db4..531f975 100644 --- a/.github/workflows/gorelease.yml +++ b/.github/workflows/gorelease.yml @@ -9,29 +9,19 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: 1.23.x + GO_VERSION: stable jobs: gorelease: runs-on: ubuntu-latest steps: - - name: Install Go stable - if: env.GO_VERSION != 'tip' - uses: actions/setup-go@v4 + - name: Install Go + uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} - - name: Install Go tip - if: env.GO_VERSION == 'tip' - run: | - curl -sL https://storage.googleapis.com/go-build-snap/go/linux-amd64/$(git ls-remote https://github.com/golang/go.git HEAD | awk '{print $1;}').tar.gz -o gotip.tar.gz - ls -lah gotip.tar.gz - mkdir -p ~/sdk/gotip - tar -C ~/sdk/gotip -xzf gotip.tar.gz - ~/sdk/gotip/bin/go version - echo "PATH=$HOME/go/bin:$HOME/sdk/gotip/bin/:$PATH" >> $GITHUB_ENV - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Gorelease cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: | ~/go/bin/gorelease diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index deab48e..978f679 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -15,36 +15,25 @@ concurrency: env: GO111MODULE: "on" RUN_BASE_COVERAGE: "on" # Runs test for PR base in case base test coverage is missing. - COV_GO_VERSION: 1.22.x # Version of Go to collect coverage + COV_GO_VERSION: stable # Version of Go to collect coverage TARGET_DELTA_COV: 90 # Target coverage of changed lines, in percents jobs: test: strategy: matrix: - go-version: [ 1.16.x, 1.22.x, 1.23.x ] + go-version: [ stable, oldstable ] runs-on: ubuntu-latest steps: - - name: Install Go stable - if: matrix.go-version != 'tip' - uses: actions/setup-go@v4 + - name: Install Go + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go-version }} - - name: Install Go tip - if: matrix.go-version == 'tip' - run: | - curl -sL https://storage.googleapis.com/go-build-snap/go/linux-amd64/$(git ls-remote https://github.com/golang/go.git HEAD | awk '{print $1;}').tar.gz -o gotip.tar.gz - ls -lah gotip.tar.gz - mkdir -p ~/sdk/gotip - tar -C ~/sdk/gotip -xzf gotip.tar.gz - ~/sdk/gotip/bin/go version - echo "PATH=$HOME/go/bin:$HOME/sdk/gotip/bin/:$PATH" >> $GITHUB_ENV - - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Go cache - uses: actions/cache@v3 + uses: actions/cache@v4 with: # In order: # * Module download cache @@ -59,7 +48,7 @@ jobs: - name: Restore base test coverage id: base-coverage if: matrix.go-version == env.COV_GO_VERSION && github.event.pull_request.base.sha != '' - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: | unit-base.txt @@ -91,8 +80,9 @@ jobs: curl -sLO https://github.com/vearutop/gocovdiff/releases/download/v1.4.2/linux_amd64.tar.gz && tar xf linux_amd64.tar.gz && rm linux_amd64.tar.gz gocovdiff_hash=$(git hash-object ./gocovdiff) [ "$gocovdiff_hash" == "c37862c73a677e5a9c069470287823ab5bbf0244" ] || (echo "::error::unexpected hash for gocovdiff, possible tampering: $gocovdiff_hash" && exit 1) - git fetch origin master ${{ github.event.pull_request.base.sha }} - REP=$(./gocovdiff -mod github.com/$GITHUB_REPOSITORY -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV}) + # Fetch PR diff from GitHub API. + curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" -H "Accept: application/vnd.github.v3.diff" https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }} > pull_request.diff + REP=$(./gocovdiff -diff pull_request.diff -mod github.com/$GITHUB_REPOSITORY -cov unit.coverprofile -gha-annotations gha-unit.txt -delta-cov-file delta-cov-unit.txt -target-delta-cov ${TARGET_DELTA_COV}) echo "${REP}" cat gha-unit.txt DIFF=$(test -e unit-base.txt && ./gocovdiff -mod github.com/$GITHUB_REPOSITORY -func-cov unit.txt -func-base-cov unit-base.txt || echo "Missing base coverage file") @@ -130,7 +120,7 @@ jobs: - name: Upload code coverage if: matrix.go-version == env.COV_GO_VERSION - uses: codecov/codecov-action@v1 + uses: codecov/codecov-action@v5 with: - file: ./unit.coverprofile + files: ./unit.coverprofile flags: unittests diff --git a/.golangci.yml b/.golangci.yml index 646b41f..b7dfe05 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,71 +1,86 @@ -# See https://github.com/golangci/golangci-lint/blob/master/.golangci.example.yml +# See https://golangci-lint.run/docs/linters/configuration/ +version: "2" run: tests: true - -linters-settings: - errcheck: - check-type-assertions: true - check-blank: true - gocyclo: - min-complexity: 20 - dupl: - threshold: 100 - misspell: - locale: US - unused: - check-exported: false - unparam: - check-exported: true - cyclop: - max-complexity: 15 - linters: - enable-all: true + default: all disable: + - embeddedstructfieldcheck + - noinlineerr + - wsl_v5 + - funcorder + - copyloopvar - gosec - - nilnil - - lll - - gochecknoglobals - - gomnd - - wrapcheck - - paralleltest + - depguard + - dupword + - errname + - exhaustruct - forbidigo - forcetypeassert - - varnamelen - - tagliatelle - - errname + - gochecknoglobals + - intrange - ireturn - - exhaustruct + - lll + - mnd + - nilnil - nonamedreturns - - testableexamples - - dupword - - depguard + - paralleltest + - recvcheck - tagalign - - execinquery - - mnd + - tagliatelle + - testableexamples - testifylint - -issues: - exclude-use-default: false - exclude-rules: - - linters: - - gomnd - - mnd - - goconst - - err113 - - noctx - - funlen - - dupl - - structcheck - - unused - - unparam - - nosnakecase - path: "_test.go" - - linters: - - errcheck # Error checking omitted for brevity. - - gosec - path: "example_" - - linters: - - revive - text: "unused-parameter: parameter" - + - varnamelen + - wrapcheck + settings: + cyclop: + max-complexity: 15 + dupl: + threshold: 100 + errcheck: + check-type-assertions: true + check-blank: true + gocyclo: + min-complexity: 20 + misspell: + locale: US + unparam: + check-exported: true + exclusions: + generated: lax + rules: + - linters: + - dupl + - err113 + - funlen + - goconst + - mnd + - noctx + - nosnakecase + - structcheck + - unparam + - unused + path: _test.go + - linters: + - errcheck + - gosec + path: example_ + - linters: + - revive + text: 'unused-parameter: parameter' + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile index 853cc8c..7eda2f0 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -#GOLANGCI_LINT_VERSION := "v1.61.0" # Optional configuration to pinpoint golangci-lint version. +#GOLANGCI_LINT_VERSION := "v2.3.1" # Optional configuration to pinpoint golangci-lint version. # The head of Makefile determines location of dev-go to include standard targets. GO ?= go diff --git a/_testdata/Database.feature b/_testdata/Database.feature index f7af2e4..2eacf8b 100644 --- a/_testdata/Database.feature +++ b/_testdata/Database.feature @@ -9,8 +9,8 @@ Feature: Database Query """ And these rows are stored in table "my_table" of database "my_db" - | id | foo | bar | created_at | deleted_at | - | 1 | foo-1 | abc | 2021-01-01T00:00:00Z | NULL | + | id | created_at | deleted_at | foo | bar | + | 1 | 2021-01-01T00:00:00Z | NULL | foo-1 | abc | Then only these rows are available in table "my_table" of database "my_db" | id | foo | bar | created_at | deleted_at | diff --git a/_testdata/rows.csv b/_testdata/rows.csv index 010bcca..287e1f8 100644 --- a/_testdata/rows.csv +++ b/_testdata/rows.csv @@ -1,4 +1,4 @@ -id,foo,bar,created_at,deleted_at -1,foo-1,abc,2021-01-01T00:00:00Z,NULL -2,foo-1,def,2021-01-02T00:00:00Z,2021-01-03T00:00:00Z -3,foo-2,hij,2021-01-03T00:00:00Z,2021-01-03T00:00:00Z +id,created_at,deleted_at,foo,bar +1,2021-01-01T00:00:00Z,NULL,foo-1,abc +2,2021-01-02T00:00:00Z,2021-01-03T00:00:00Z,foo-1,def +3,2021-01-03T00:00:00Z,2021-01-03T00:00:00Z,foo-2,hij diff --git a/dbsteps.go b/dbsteps.go index 08b05d8..498b6b4 100644 --- a/dbsteps.go +++ b/dbsteps.go @@ -130,6 +130,7 @@ import ( "github.com/bool64/sqluct" "github.com/cucumber/godog" "github.com/godogx/resource" + "github.com/godogx/vars" "github.com/jmoiron/sqlx" "github.com/stretchr/testify/assert" "github.com/swaggest/form/v5" @@ -140,7 +141,10 @@ const Default = "default" // RegisterSteps adds database manager context to test suite. func (m *Manager) RegisterSteps(s *godog.ScenarioContext) { - m.lock.Register(s) + if m.lock != nil { + m.lock.Register(s) + } + m.registerPrerequisites(s) m.registerAssertions(s) } @@ -234,8 +238,8 @@ func NewManager() *Manager { return &Manager{ TableMapper: NewTableMapper(), Instances: make(map[string]Instance), + VS: &vars.Steps{}, lock: resource.NewLock(nil), - Vars: &shared.Vars{}, } } @@ -248,8 +252,11 @@ type Manager struct { TableMapper *TableMapper Instances map[string]Instance - // Vars allow sharing vars with other steps. + // Deprecated: use VS.JSONComparer.Vars. Vars *shared.Vars + + // VS allow sharing vars with other steps. + VS *vars.Steps } // Instance provides database instance. @@ -262,8 +269,14 @@ type Instance struct { // They are executed after `there are no rows in table` step. // Example: `"my_table": []string{"ALTER SEQUENCE my_table_id_seq RESTART"}`. PostCleanup map[string][]string +} - vars *shared.Vars +// DisableLocks disable locks between concurrent scenarios. +// By default, if two concurrent scenarios want to access same DB table, they will need to run sequentially. +// This is to avoid race conditions in table cleanups and counted assertions ("only these rows are available"). +// If scenarios do not have mutually exclusive conflicting steps, lock can be disabled for better performance. +func (m *Manager) DisableLocks() { + m.lock = nil } // RegisterJSONTypes registers types of provided values to unmarshal as JSON when decoding from string. @@ -293,19 +306,14 @@ func (m *Manager) instance(ctx context.Context, tableName, dbName string) (Insta return Instance{}, nil, ctx, fmt.Errorf("%w %s", errUnknownDatabase, dbName) } - row, found := instance.Tables[tableName] - if !found { - return Instance{}, nil, ctx, fmt.Errorf("%w %s in database %s", errUnknownTable, tableName, dbName) - } + row := instance.Tables[tableName] // Locking per table. - _, err := m.lock.Acquire(ctx, dbName+"::"+tableName) - if err != nil { - return Instance{}, nil, ctx, err - } - - if m.Vars != nil { - ctx, instance.vars = m.Vars.Fork(ctx) + if m.lock != nil { + _, err := m.lock.Acquire(ctx, dbName+"::"+tableName) + if err != nil { + return Instance{}, nil, ctx, err + } } return instance, row, ctx, nil @@ -398,21 +406,37 @@ func (m *Manager) givenRowsFromThisFileAreStoredInTableOfDatabase(ctx context.Co } func (m *Manager) givenTheseRowsAreStoredInTableOfDatabase(ctx context.Context, tableName, dbName string, data [][]string) (context.Context, error) { + if len(data) < 2 { + return ctx, errRowRequired + } + instance, row, ctx, err := m.instance(ctx, tableName, dbName) if err != nil { return ctx, err } - // Reading rows. - rows, err := m.TableMapper.SliceFromTable(data, row) + var ( + stmt squirrel.InsertBuilder + storage = instance.Storage + colNames = data[0] + ) + + ctx, err = m.VS.ReplaceTable(ctx, data) if err != nil { - return ctx, fmt.Errorf("failed to map rows table: %w", err) + return ctx, err } - colNames := data[0] + // Reading rows. + if row != nil { + rows, err := m.TableMapper.SliceFromTable(data, row) + if err != nil { + return ctx, fmt.Errorf("failed to map rows table: %w", err) + } - storage := instance.Storage - stmt := storage.InsertStmt(tableName, rows, sqluct.Columns(colNames...)) + stmt = storage.InsertStmt(tableName, rows, sqluct.Columns(colNames...)) + } else { + stmt = m.prepareInsert(storage.InsertStmt(tableName, row).Columns(colNames...), data) + } // Inserting rows. _, err = storage.Exec(ctx, stmt) @@ -428,12 +452,36 @@ func (m *Manager) givenTheseRowsAreStoredInTableOfDatabase(ctx context.Context, return ctx, err } +func (m *Manager) prepareInsert(stmt squirrel.InsertBuilder, data [][]string) squirrel.InsertBuilder { + for i, r := range data { + if i == 0 { + continue + } + + vals := make([]interface{}, 0, len(r)) + + for _, v := range r { + if v == null { + vals = append(vals, nil) + + continue + } + + vals = append(vals, vars.Infer(v)) + } + + stmt = stmt.Values(vals...) + } + + return stmt +} + type testingT struct { Err error } func (t *testingT) Errorf(format string, args ...interface{}) { - t.Err = fmt.Errorf(format, args...) //nolint:goerr113 + t.Err = fmt.Errorf(format, args...) //nolint:err113 } type tableQuery struct { @@ -445,11 +493,11 @@ type tableQuery struct { colNames []string skipWhereCols []string postCheck []string - vars *shared.Vars + vs *shared.Vars } func (t *tableQuery) exposeContents(err error) error { - qb := t.storage.SelectStmt(t.table, t.row).Limit(50) + qb := t.storage.SelectStmt(t.table, nil).Columns("*").Limit(50) var colNames []string @@ -501,13 +549,15 @@ func (m *Manager) makeTableQuery(ctx context.Context, tableName, dbName string, return nil, ctx, err } + ctx, vs := m.VS.Vars(ctx) + t := tableQuery{ storage: instance.Storage, mapper: m.TableMapper, table: tableName, data: data, row: row, - vars: instance.vars, + vs: vs, } if t.data != nil { @@ -519,12 +569,35 @@ func (m *Manager) makeTableQuery(ctx context.Context, tableName, dbName string, return &t, ctx, nil } -func (t *tableQuery) receiveRow(index int, row interface{}, _ []string, rawValues []string) error { +func (t *tableQuery) receiveRow(index int, row interface{}, _ []string, rawValues []string) (err error) { + defer func() { + if err != nil { + err = fmt.Errorf("row %d: %w", index, err) + } + }() + qb := t.storage.QueryBuilder(). Select(t.colNames...). From(t.table) - eq := t.storage.WhereEq(row, sqluct.Columns(t.colNames...)) + var ( + argsExp, argsRcv map[string]interface{} + eq squirrel.Eq + isMap = false + ) + + if m, ok := row.(map[string]interface{}); ok { + eq = m + argsExp = make(map[string]interface{}, len(m)) + + for k, v := range m { + argsExp[k] = v + } + + isMap = true + } else { + eq = t.storage.WhereEq(row, sqluct.Columns(t.colNames...)) + } for _, sk := range t.skipWhereCols { delete(eq, sk) @@ -540,27 +613,80 @@ func (t *tableQuery) receiveRow(index int, row interface{}, _ []string, rawValue qb = qb.Where(squirrel.Eq{col: eq[col]}) } + if isMap { + argsRcv, err = t.scanMap(qb) + if err != nil { + return err + } + } else { + if argsExp, argsRcv, err = t.scanStruct(row, qb); err != nil { + return err + } + } + + pc := t.postCheck + t.postCheck = t.postCheck[:0] + + return t.doPostCheck(t.colNames, pc, argsExp, argsRcv, rawValues) +} + +func (t *tableQuery) scanMap(qb squirrel.SelectBuilder) ( + argsRcv map[string]interface{}, + err error, +) { + rows, err := t.storage.Query(context.Background(), qb) + if err != nil { + return nil, err + } + + defer func() { + clErr := rows.Close() + if clErr != nil && err == nil { + err = clErr + } + }() + + found := 0 + + for rows.Next() { + found++ + + argsRcv = make(map[string]interface{}) + if err := rows.MapScan(argsRcv); err != nil { + return nil, err + } + } + + if found != 1 { + return nil, fmt.Errorf("%w, expected 1, found %d", errInvalidNumberOfRows, found) + } + + return argsRcv, nil +} + +func (t *tableQuery) scanStruct(row interface{}, qb squirrel.SelectBuilder) ( + argsExp map[string]interface{}, + argsRcv map[string]interface{}, + err error, +) { dest := reflect.New(reflect.TypeOf(row).Elem()).Interface() - err := t.storage.Select(context.Background(), qb, dest) + err = t.storage.Select(context.Background(), qb, dest) if err != nil { query, args, qbErr := qb.ToSql() if qbErr != nil { - return fmt.Errorf("failed to build query: %w", qbErr) + return nil, nil, fmt.Errorf("build query: %w", qbErr) } - return fmt.Errorf("failed to query row %d (%+v) with %q %v: %w", index, row, query, args, err) + return nil, nil, fmt.Errorf("run query (%+v) with %q %v: %w", row, query, args, err) } colOption := sqluct.Columns(t.colNames...) - pc := t.postCheck - t.postCheck = t.postCheck[:0] + argsExp = combine(t.storage.Mapper.ColumnsValues(reflect.ValueOf(row), colOption)) + argsRcv = combine(t.storage.Mapper.ColumnsValues(reflect.ValueOf(dest), colOption)) - return t.doPostCheck(t.colNames, pc, - combine(t.storage.Mapper.ColumnsValues(reflect.ValueOf(row), colOption)), - combine(t.storage.Mapper.ColumnsValues(reflect.ValueOf(dest), colOption)), - rawValues) + return argsExp, argsRcv, nil } func combine(keys []string, vals []interface{}) map[string]interface{} { @@ -585,8 +711,8 @@ func (t *tableQuery) skipDecode(column, value string) bool { // If value looks like a variable name and does not have an associated value yet, // it is removed from decoding and WHERE condition. - if t.vars != nil && t.vars.IsVar(value) { - if _, found := t.vars.Get(value); found { + if t.vs != nil && t.vs.IsVar(value) { + if _, found := t.vs.Get(value); found { return false } @@ -601,14 +727,14 @@ func (t *tableQuery) skipDecode(column, value string) bool { func (t *tableQuery) makeReplaces(onSetErr *error) (map[string]string, error) { replaces := make(map[string]string) - if t.vars == nil { + if t.vs == nil { return nil, nil } - if vars := t.vars.GetAll(); len(vars) > 0 { - replaces = make(map[string]string, len(vars)) + if vs := t.vs.GetAll(); len(vs) > 0 { + replaces = make(map[string]string, len(vs)) - for k, v := range vars { + for k, v := range vs { s, err := t.mapper.Encode(v) if err != nil { return nil, err @@ -618,7 +744,7 @@ func (t *tableQuery) makeReplaces(onSetErr *error) (map[string]string, error) { } } - t.vars.OnSet(func(key string, val interface{}) { + t.vs.OnSet(func(key string, val interface{}) { s, err := t.mapper.Encode(val) if err != nil { *onSetErr = err @@ -688,8 +814,8 @@ func (m *Manager) assertRowsFromFile(ctx context.Context, tableName, dbName stri func (t *tableQuery) doPostCheck(colNames []string, postCheck []string, argsExp, argsRcv map[string]interface{}, rawValues []string) error { for i, name := range colNames { - if t.vars.IsVar(rawValues[i]) { - t.vars.Set(rawValues[i], argsRcv[name]) + if t.vs.IsVar(rawValues[i]) { + t.vs.Set(rawValues[i], argsRcv[name]) } pc := false @@ -786,7 +912,6 @@ func NewTableMapper() *TableMapper { var ( errWrongType = errors.New("failed to assert type *interface{}") errInvalidNumberOfRows = errors.New("invalid number of rows in table") - errUnknownTable = errors.New("unknown table") errUnknownDatabase = errors.New("unknown database") ) diff --git a/dbsteps_test.go b/dbsteps_test.go index c8eba18..5fb8d10 100644 --- a/dbsteps_test.go +++ b/dbsteps_test.go @@ -4,6 +4,7 @@ import ( "bytes" "database/sql" "database/sql/driver" + "fmt" "testing" "time" @@ -35,6 +36,26 @@ func mustParseTime(value string) time.Time { return t } +func TestMap(t *testing.T) { + m := map[string]interface{}{} + m["foo"] = 1 + m["bar"] = true + + one := m + two := m + tri := map[string]interface{}{} + + for k, v := range m { + tri[k] = v + } + + delete(one, "foo") + + fmt.Println(m) + fmt.Println(two) + fmt.Println(tri) +} + func TestManager_RegisterContext(t *testing.T) { type RowKey struct { Foo *string `db:"foo"` @@ -170,6 +191,125 @@ func TestManager_RegisterContext(t *testing.T) { } } +func TestManager_RegisterContext_untyped_row(t *testing.T) { + dbm := dbsteps.NewManager() + db, mock, err := sqlmock.New() + assert.NoError(t, err) + + dbm.Instances = map[string]dbsteps.Instance{ + "my_db": { + Storage: sqluct.NewStorage(sqlx.NewDb(db, "sqlmock")), + }, + } + + // Given there are no rows in table "my_table" of database "my_db" + mock.ExpectExec(`DELETE FROM my_table`). + WillReturnResult(driver.ResultNoRows) + + // And rows from this file are stored in table "my_table" of database "my_db" + // """ + // _testdata/rows.csv + // """ + mock.ExpectExec(`INSERT INTO my_table \(id,created_at,deleted_at,foo,bar\) VALUES .+`). + WithArgs( + 1, mustParseTime("2021-01-01T00:00:00Z"), nil, "foo-1", "abc", + 2, mustParseTime("2021-01-02T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"), "foo-1", "def", + 3, mustParseTime("2021-01-03T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"), "foo-2", "hij", + ). + WillReturnResult(driver.ResultNoRows) + + // And these rows are stored in table "my_table" of database "my_db": + // | id | foo | bar | created_at | deleted_at | + // | 1 | foo-1 | abc | 2021-01-01T00:00:00Z | NULL | + mock.ExpectExec(`INSERT INTO my_table \(id,created_at,deleted_at,foo,bar\) VALUES .+`). + WithArgs( + 1, mustParseTime("2021-01-01T00:00:00Z"), nil, "foo-1", "abc", + ). + WillReturnResult(driver.ResultNoRows) + + // Then only these rows are available in table "my_table" of database "my_db": + // | id | foo | bar | created_at | deleted_at | + // | 1 | foo-1 | abc | 2021-01-01T00:00:00Z | NULL | + // | 2 | foo-1 | def | 2021-01-02T00:00:00Z | 2021-01-03T00:00:00Z | + // | 3 | foo-2 | hij | 2021-01-03T00:00:00Z | 2021-01-03T00:00:00Z | + mock.ExpectQuery(`SELECT COUNT\(1\) AS c FROM my_table`).WillReturnRows(sqlmock.NewRows([]string{"c"}).AddRow(3)) + + mock.ExpectQuery(`SELECT .+ FROM my_table WHERE bar = \$1 AND created_at = \$2 AND deleted_at IS NULL`). + WithArgs( + "abc", mustParseTime("2021-01-01T00:00:00Z"), + ). + WillReturnRows(sqlmock.NewRows([]string{"id", "foo", "bar", "created_at", "deleted_at"}). + AddRow(1, "foo-1", "abc", mustParseTime("2021-01-01T00:00:00Z"), nil)) + + mock.ExpectQuery(`SELECT .+ FROM my_table WHERE foo = \$1 AND bar = \$2 AND created_at = \$3 AND deleted_at = \$4`). + WithArgs( + "foo-1", "def", mustParseTime("2021-01-02T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"), + ). + WillReturnRows(sqlmock.NewRows([]string{"id", "foo", "bar", "created_at", "deleted_at"}). + AddRow(2, "foo-1", "def", mustParseTime("2021-01-02T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"))) + + mock.ExpectQuery(`SELECT .+ FROM my_table WHERE foo = \$1 AND bar = \$2 AND created_at = \$3 AND deleted_at = \$4`). + WithArgs( + "foo-2", "hij", mustParseTime("2021-01-03T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"), + ). + WillReturnRows(sqlmock.NewRows([]string{"id", "foo", "bar", "created_at", "deleted_at"}). + AddRow(3, "foo-2", "hij", mustParseTime("2021-01-03T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"))) + + // Assertion with interpolated variables. + // Then only these rows are available in table "my_table" of database "my_db": + // | id | foo | bar | created_at | deleted_at | + // | | foo-1 | abc | 2021-01-01T00:00:00Z | NULL | + // | | foo-1 | def | 2021-01-02T00:00:00Z | 2021-01-03T00:00:00Z | + // | | foo-2 | hij | 2021-01-03T00:00:00Z | 2021-01-03T00:00:00Z | + mock.ExpectQuery(`SELECT COUNT\(1\) AS c FROM my_table`).WillReturnRows(sqlmock.NewRows([]string{"c"}).AddRow(3)) + + mock.ExpectQuery(`SELECT .+ FROM my_table WHERE id = \$1 AND foo = \$2 AND bar = \$3 AND created_at = \$4 AND deleted_at IS NULL`). + WithArgs( + 1, "foo-1", "abc", mustParseTime("2021-01-01T00:00:00Z"), + ). + WillReturnRows(sqlmock.NewRows([]string{"id", "foo", "bar", "created_at", "deleted_at"}). + AddRow(1, "foo-1", "abc", mustParseTime("2021-01-01T00:00:00Z"), nil)) + + mock.ExpectQuery(`SELECT .+ FROM my_table WHERE id = \$1 AND foo = \$2 AND bar = \$3 AND created_at = \$4 AND deleted_at = \$5`). + WithArgs( + 2, "foo-1", "def", mustParseTime("2021-01-02T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"), + ). + WillReturnRows(sqlmock.NewRows([]string{"id", "foo", "bar", "created_at", "deleted_at"}). + AddRow(2, "foo-1", "def", mustParseTime("2021-01-02T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"))) + + mock.ExpectQuery(`SELECT .+ FROM my_table WHERE id = \$1 AND foo = \$2 AND bar = \$3 AND created_at = \$4 AND deleted_at = \$5`). + WithArgs( + 3, "foo-2", "hij", mustParseTime("2021-01-03T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"), + ). + WillReturnRows(sqlmock.NewRows([]string{"id", "foo", "bar", "created_at", "deleted_at"}). + AddRow(3, "foo-2", "hij", mustParseTime("2021-01-03T00:00:00Z"), mustParseTime("2021-01-03T00:00:00Z"))) + + // And no rows are available in table "my_another_table" of database "my_db" + mock.ExpectQuery(`SELECT COUNT\(1\) AS c FROM my_another_table`).WillReturnRows(sqlmock.NewRows([]string{"c"}).AddRow(0)) + + buf := bytes.NewBuffer(nil) + + suite := godog.TestSuite{ + Name: "DatabaseContext", + TestSuiteInitializer: nil, + ScenarioInitializer: func(s *godog.ScenarioContext) { + dbm.RegisterSteps(s) + }, + Options: &godog.Options{ + Format: "pretty", + Output: buf, + Paths: []string{"_testdata/Database.feature"}, + Strict: true, + Randomize: time.Now().UTC().UnixNano(), + }, + } + status := suite.Run() + + if status != 0 { + t.Fatal(buf.String()) + } +} + func TestManager_RegisterContext_fail(t *testing.T) { type RowKey struct { Foo string `db:"foo"` @@ -200,7 +340,7 @@ func TestManager_RegisterContext_fail(t *testing.T) { createdAt := time.Date(2020, 1, 1, 1, 1, 1, 0, time.UTC) - mock.ExpectQuery(`SELECT id, created_at, deleted_at, foo, bar FROM my_table LIMIT 50`). + mock.ExpectQuery(`SELECT \* FROM my_table LIMIT 50`). WillReturnRows(sqlmock.NewRows([]string{"id", "created_at", "deleted_at", "foo", "bar"}). AddRow(1, createdAt, nil, "my-foo", "bar-1"). AddRow(2, createdAt, nil, "my-foo", "bar-122")) diff --git a/go.mod b/go.mod index cdf6272..f1f947d 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,18 @@ module github.com/godogx/dbsteps -go 1.18 +go 1.23.0 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/Masterminds/squirrel v1.5.4 - github.com/bool64/dev v0.2.36 + github.com/bool64/dev v0.2.41 github.com/bool64/shared v0.1.5 - github.com/bool64/sqluct v0.2.3 + github.com/bool64/sqluct v0.2.8 github.com/cucumber/godog v0.14.1 github.com/godogx/resource v0.1.1 + github.com/godogx/vars v0.1.11 github.com/jmoiron/sqlx v1.4.0 - github.com/stretchr/testify v1.8.2 + github.com/stretchr/testify v1.9.0 github.com/swaggest/form/v5 v5.1.1 ) @@ -20,13 +21,24 @@ require ( github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect github.com/cucumber/messages/go/v21 v21.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gofrs/uuid v4.3.1+incompatible // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gofrs/uuid v4.4.0+incompatible // indirect github.com/hashicorp/go-immutable-radix v1.3.1 // indirect - github.com/hashicorp/go-memdb v1.3.4 // indirect + github.com/hashicorp/go-memdb v1.3.5 // indirect github.com/hashicorp/golang-lru v1.0.2 // indirect + github.com/iancoleman/orderedmap v0.3.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/sergi/go-diff v1.4.0 // indirect + github.com/spf13/pflag v1.0.7 // indirect + github.com/swaggest/assertjson v1.9.0 // indirect + github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 // indirect + github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff // indirect + github.com/yudai/gojsondiff v1.0.0 // indirect + github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 // indirect + golang.org/x/sys v0.13.0 // indirect + golang.org/x/text v0.28.0 // indirect + gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ac710e9..99cc804 100644 --- a/go.sum +++ b/go.sum @@ -7,12 +7,12 @@ github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA4 github.com/bool64/ctxd v1.2.1 h1:hARFteq0zdn4bwfmxLhak3fXFuvtJVKDH2X29VV/2ls= github.com/bool64/ctxd v1.2.1/go.mod h1:ZG6QkeGVLTiUl2mxPpyHmFhDzFZCyocr9hluBV3LYuc= github.com/bool64/dev v0.2.25/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= -github.com/bool64/dev v0.2.36 h1:yU3bbOTujoxhWnt8ig8t94PVmZXIkCaRj9C57OtqJBY= -github.com/bool64/dev v0.2.36/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= +github.com/bool64/dev v0.2.41 h1:aic7tEGMkqyH7kckksR1ddagAZtlSIFHvYBOsqn01+U= +github.com/bool64/dev v0.2.41/go.mod h1:iJbh1y/HkunEPhgebWRNcs8wfGq7sjvJ6W5iabL8ACg= github.com/bool64/shared v0.1.5 h1:fp3eUhBsrSjNCQPcSdQqZxxh9bBwrYiZ+zOKFkM0/2E= github.com/bool64/shared v0.1.5/go.mod h1:081yz68YC9jeFB3+Bbmno2RFWvGKv1lPKkMP6MHJlPs= -github.com/bool64/sqluct v0.2.3 h1:fMF/5hwqbKOLcOeGGHWmbNBmU+UVtXaWVFD3+O0Z0Xk= -github.com/bool64/sqluct v0.2.3/go.mod h1:Ha+dDE4U/O+s2KcFJJ97ZpoHQNPBdmmv8v62OeL/U84= +github.com/bool64/sqluct v0.2.8 h1:ZUBkCm4hekyYNE0HEC8H+z5oQX7LVeDt7Oasrh+fp9Y= +github.com/bool64/sqluct v0.2.8/go.mod h1:DY6C6ehBuKrT8vTQeiKwBdC022bUcfpkDKUSdog1DXQ= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI= github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0= @@ -24,18 +24,24 @@ github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= github.com/godogx/resource v0.1.1 h1:1vbznIn1mUCP+9TzJp9v8QKm54kAMTe38CBLnFBk0j8= github.com/godogx/resource v0.1.1/go.mod h1:OYaiyttuq2KaiJp2yOMekyOFjZJFz3w/D7WPioUVC4Y= +github.com/godogx/vars v0.1.11 h1:Maae8QtGzCoZlTsuaXwZaEz1MGPvNMtC1O18s4Z8fpw= +github.com/godogx/vars v0.1.11/go.mod h1:UL4jj0Z1Xs61nNE/VS4vMYZowISPCpvlYZ85EgdFQ8Q= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI= github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= +github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= -github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c= github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg= +github.com/hashicorp/go-memdb v1.3.5 h1:b3taDMxCBCBVgyRrS1AZVHO14ubMYZB++QpNhBg+Nyo= +github.com/hashicorp/go-memdb v1.3.5/go.mod h1:8IVKKBkVe+fxFgdFOYxzQQNjz+sWCyHCdIC/+5+Vy1Y= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -43,10 +49,13 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/iancoleman/orderedmap v0.3.0 h1:5cbR2grmZR/DiVt+VJopEhtVs9YGInGIxAoMJn+Ichc= +github.com/iancoleman/orderedmap v0.3.0/go.mod h1:XuLcCUkdL5owUCQeF2Ue9uuw1EptkJDkXXS7VoV7XGE= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -58,14 +67,27 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhR github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.15.2 h1:l77YT15o814C2qVL47NOyjV/6RbaP7kKdrvZnxQ3Org= +github.com/onsi/ginkgo v1.15.2/go.mod h1:Dd6YFfwBW84ETqqtL0CPyPXillHgY6XhQH3uuCCTr/o= +github.com/onsi/gomega v1.11.0 h1:+CqWgvj0OZycCaqclBD1pxKHAU+tOkHmQIWvDHq2aug= +github.com/onsi/gomega v1.11.0/go.mod h1:azGKhqFUon9Vuj0YmTfLSmx0FUwqXYSTl5re8lQLTUg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 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.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= +github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -74,15 +96,40 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/swaggest/assertjson v1.9.0 h1:dKu0BfJkIxv/xe//mkCrK5yZbs79jL7OVf9Ija7o2xQ= +github.com/swaggest/assertjson v1.9.0/go.mod h1:b+ZKX2VRiUjxfUIal0HDN85W0nHPAYUbYH5WkkSsFsU= github.com/swaggest/form/v5 v5.1.1 h1:ct6/rOQBGrqWUQ0FUv3vW5sHvTUb31AwTUWj947N6cY= github.com/swaggest/form/v5 v5.1.1/go.mod h1:X1hraaoONee20PMnGNLQpO32f9zbQ0Czfm7iZThuEKg= github.com/swaggest/usecase v1.2.0 h1:cHVFqxIbHfyTXp02JmWXk+ZADaSa87UZP+b3qL5Nz90= +github.com/swaggest/usecase v1.2.0/go.mod h1:oc5+QoAxG3Et5Gl9lRXgEOm00l4VN9gdVQSMIa5EeLY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY= +github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= +github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff h1:7YqG491bE4vstXRz1lD38rbSgbXnirvROz1lZiOnPO8= +github.com/yosuke-furukawa/json5 v0.1.2-0.20201207051438-cf7bb3f354ff/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= +github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA= +github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= +github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= +github.com/yudai/pp v2.0.1+incompatible h1:Q4//iY4pNF6yPLZIigmvcl7k/bPgrcTPIFIcmawg5bI= +github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/table.go b/table.go index 0bce71e..c3ece27 100644 --- a/table.go +++ b/table.go @@ -6,6 +6,7 @@ import ( "reflect" "strings" + "github.com/godogx/vars" "github.com/swaggest/form/v5" ) @@ -106,7 +107,7 @@ func itemType(v interface{}) (reflect.Type, error) { // IterateTable walks gherkin table calling row receiver with mapped row. // If receiver returns error iteration stops and error is propagated. func (m *TableMapper) IterateTable(c IterateConfig) error { - if m.Decoder == nil { + if m.Decoder == nil && c.Item != nil { m.Decoder = form.NewDecoder() } @@ -114,17 +115,27 @@ func (m *TableMapper) IterateTable(c IterateConfig) error { return errRowRequired } - colNames := c.Data[0] - - itemType, err := itemType(c.Item) - if err != nil { - return err + var ( + it reflect.Type + err error + itemBuf reflect.Value + colNames = c.Data[0] + values = make(map[string][]string, len(colNames)) + rowMap = make(map[string]interface{}) + ) + + if c.Item != nil { + it, err = itemType(c.Item) + if err != nil { + return err + } } - values := make(map[string][]string, len(colNames)) - for rowIndex, row := range c.Data[1:] { - itemBuf := reflect.New(itemType) + if c.Item != nil { + itemBuf = reflect.New(it) + } + raw := make([]string, 0, len(colNames)) for i, cell := range row { @@ -141,24 +152,34 @@ func (m *TableMapper) IterateTable(c IterateConfig) error { } if cell != null { + rowMap[colNames[i]] = vars.Infer(cell) values[colNames[i]] = []string{cell} } else { + rowMap[colNames[i]] = nil + delete(values, colNames[i]) } } - val := itemBuf.Interface() - - err := m.Decoder.Decode(val, values) - if err != nil { - return err - } - - err = c.ReceiveRow(rowIndex, itemBuf.Interface(), colNames, raw) - if err != nil { + if err = c.ReceiveRow(rowIndex, m.decode(itemBuf, values, rowMap), colNames, raw); err != nil { return err } } return nil } + +func (m *TableMapper) decode(itemBuf reflect.Value, values map[string][]string, rowMap map[string]interface{}) interface{} { + if !itemBuf.IsValid() { + return rowMap + } + + val := itemBuf.Interface() + + err := m.Decoder.Decode(val, values) + if err != nil { + return err + } + + return itemBuf.Interface() +}