Skip to content
Merged
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
37 changes: 37 additions & 0 deletions _testdata/DiffClosestRow.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
Feature: Diff Closest Row

Scenario: Diff With Vars
Given all rows are deleted in table "users"

And these rows are stored in table "users"
| name | email | age | created_at | deleted_at |
| Jane | abc@aaa.com | 23 | 2021-01-01T00:00:00Z | NULL |
| John | def@bbb.de | 33 | 2021-01-02T00:00:00Z | 2021-01-03T00:00:00Z |
| Junie | hij@ccc.ru | 43 | 2021-01-03T00:00:00Z | 2021-01-03T00:00:00Z |

And variables are set to values
| $id_expected | 99 |

Then these rows are available in table "users"
| id | name | email | age | created_at | deleted_at |
| $id_expected | John | def@bbb.de | 33 | $created_at | 2021-01-03T00:00:00Z |

Scenario: Diff Transposed
Given all rows are deleted in table "users"

And these rows are stored in table "users"
| name | email | age | created_at | deleted_at |
| Jane | abc@aaa.com | 23 | 2021-01-01T00:00:00Z | NULL |
| John | def@bbb.de | 33 | 2021-01-02T00:00:00Z | 2021-01-03T00:00:00Z |
| Junie | hij@ccc.ru | 43 | 2021-01-03T00:00:00Z | 2021-01-03T00:00:00Z |

And variables are set to values
| $id_expected | 2 |

Then these transposed rows are available in table "users"
| id | 1 | $id_expected | 3 |
| name | Jane | John | Junie |
| email | abc@aaa.com | def@bbb.de | hij@ccc.ru |
| age | 23 | 32 | 43 |
| created_at | 2021-01-01T00:00:00Z | 2021-01-02T00:00:00Z | $created_at |
| deleted_at | NULL | 2021-01-03T00:00:00Z | 2021-01-03T00:00:00Z |
129 changes: 84 additions & 45 deletions dbsteps.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,8 +633,8 @@
postCheck []string
dumpAllColumns bool
vs *shared.Vars
lastRowIndex int
lastRowValues []string
lastColNames []string
lastExpected map[string]string
}

func (t *tableQuery) exposeContents(err error) error {
Expand All @@ -652,7 +652,12 @@
} else {
err = fmt.Errorf("%w, rows available in %s:\n%v", err, t.table, dump.table)

if diff := t.diffClosestRow(dump.res, colNames, dump.cnt, t.expectedRow(colNames)); diff != "" {
diffCols := colNames
if len(diffCols) == 0 {
diffCols = t.lastColNames
}

Check notice on line 658 in dbsteps.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 656:658 are not covered by tests.

if diff := t.diffClosestRow(dump.res, diffCols, dump.cnt, t.lastExpected); diff != "" {
err = fmt.Errorf("%w\n%v", err, diff)
}
}
Expand Down Expand Up @@ -716,9 +721,6 @@
}

func (t *tableQuery) receiveRow(index int, row any, _ []string, rawValues []string) (err error) {
t.lastRowIndex = index
t.lastRowValues = append(t.lastRowValues[:0], rawValues...)

qb := t.storage.QueryBuilder().
Select(t.colNames...).
From(t.table)
Expand Down Expand Up @@ -954,11 +956,16 @@

// Iterating rows.
err = m.TableMapper.IterateTable(IterateConfig{
Data: data,
Item: t.row,
SkipDecode: t.skipDecode,
Replaces: replaces,
ReceiveRow: t.receiveRow,
Data: data,
Item: t.row,
SkipDecode: t.skipDecode,
Replaces: replaces,
ReceiveRow: func(index int, row any, colNames []string, rawValues []string) error {
t.lastColNames = append(t.lastColNames[:0], colNames...)
t.lastExpected = t.buildExpectedRow(colNames, rawValues, replaces)

return t.receiveRow(index, row, colNames, rawValues)
},
SkipTimeInfer: m.SkipTimeInfer,
})

Expand Down Expand Up @@ -1255,32 +1262,21 @@
return b.String()
}

func (t *tableQuery) expectedRow(colNames []string) []string {
if len(t.lastRowValues) == len(colNames) {
return t.lastRowValues
}

if t.data == nil || len(t.data) != 2 {
return nil
}

if len(t.data[1]) != len(colNames) {
return nil
}

return t.data[1]
}

func (t *tableQuery) diffClosestRow(res map[string][]string, colNames []string, cnt int, expRow []string) string {
if len(colNames) == 0 {
func (t *tableQuery) diffClosestRow(
res map[string][]string,
colOrder []string,
cnt int,
expected map[string]string,
) string {
if len(colOrder) == 0 {
return ""
}

if cnt == 0 {
return ""
}

if len(expRow) != len(colNames) {
if len(expected) == 0 {
return ""
}

Expand All @@ -1289,7 +1285,7 @@
bestCompared := 0

for rowIdx := 0; rowIdx < cnt; rowIdx++ {
mismatches, compared := rowMismatch(res, colNames, expRow, rowIdx)
mismatches, compared := rowMismatch(res, colOrder, expected, rowIdx)
if compared == 0 {
continue
}
Expand All @@ -1305,10 +1301,10 @@
return ""
}

diff := make([][]string, 0, len(colNames)+1)
diff := make([][]string, 0, len(colOrder)+1)
diff = append(diff, []string{"column", "expected", "received"})

diff = append(diff, diffRow(res, colNames, expRow, bestRow)...)
diff = append(diff, diffRow(res, colOrder, expected, bestRow)...)

if len(diff) == 1 {
return ""
Expand All @@ -1323,37 +1319,48 @@
bestRow+1, matches, bestCompared, formatGherkinTable(diff))
}

func rowMismatch(res map[string][]string, colNames []string, expRow []string, rowIdx int) (int, int) {
func rowMismatch(res map[string][]string, colOrder []string, expected map[string]string, rowIdx int) (int, int) {
mismatches := 0
compared := 0

for i, col := range colNames {
values, ok := res[col]
if !ok || len(values) <= rowIdx {
for _, col := range colOrder {
exp, ok := expected[col]
if !ok {
continue
}

compared++

if expRow[i] != values[rowIdx] {
values, ok := res[col]
if !ok || len(values) <= rowIdx {
mismatches++

continue

Check notice on line 1338 in dbsteps.go

View workflow job for this annotation

GitHub Actions / test (stable)

2 statement(s) on lines 1335:1338 are not covered by tests.
}

if exp != values[rowIdx] {
mismatches++
}
}

return mismatches, compared
}

func diffRow(res map[string][]string, colNames []string, expRow []string, rowIdx int) [][]string {
rows := make([][]string, 0, len(colNames))
func diffRow(res map[string][]string, colOrder []string, expected map[string]string, rowIdx int) [][]string {
rows := make([][]string, 0, len(colOrder))

for i, col := range colNames {
values, ok := res[col]
if !ok || len(values) <= rowIdx {
for _, col := range colOrder {
exp, ok := expected[col]
if !ok {
continue
}

exp := expRow[i]
rcv := values[rowIdx]
var rcv string

values, ok := res[col]
if ok && len(values) > rowIdx {
rcv = values[rowIdx]
}

if exp == rcv {
continue
Expand Down Expand Up @@ -1404,3 +1411,35 @@

return b.String()
}

func (t *tableQuery) buildExpectedRow(colNames []string, raw []string, replaces map[string]string) map[string]string {
if len(raw) == 0 {
return nil
}

Check notice on line 1418 in dbsteps.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 1416:1418 are not covered by tests.

if len(colNames) != len(raw) {
return nil
}

Check notice on line 1422 in dbsteps.go

View workflow job for this annotation

GitHub Actions / test (stable)

1 statement(s) on lines 1420:1422 are not covered by tests.

resolved := make(map[string]string, len(raw))

for i, cell := range raw {
value := strings.TrimSuffix(cell, "::string")

if replaces != nil {
if r, ok := replaces[value]; ok {
resolved[colNames[i]] = r

continue
}
}

if t.vs != nil && t.vs.IsVar(value) {
continue
}

resolved[colNames[i]] = value
}

return resolved
}
70 changes: 70 additions & 0 deletions sqlite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"database/sql"
"os"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -55,3 +56,72 @@ func TestNewManager(t *testing.T) {
t.Fatal(buf.String())
}
}

func TestDiffClosestRowFeature(t *testing.T) {
sqlDB, err := sql.Open("sqlite", ":memory:")
require.NoError(t, err)

fixture, err := os.ReadFile("_testdata/fixture.sql")
require.NoError(t, err)

for _, st := range sqluct.SplitStatements(string(fixture)) {
_, err := sqlDB.Exec(st)
require.NoError(t, err, st)
}

vs := vars.Steps{}

m := dbsteps.NewManager()
m.AddDB(sqlDB)

buf := bytes.NewBuffer(nil)

suite := godog.TestSuite{
Name: "DatabaseDiffClosestRow",
TestSuiteInitializer: nil,
ScenarioInitializer: func(s *godog.ScenarioContext) {
m.RegisterSteps(s)
vs.Register(s)
},
Options: &godog.Options{
Format: "pretty",
Output: buf,
Paths: []string{"_testdata/DiffClosestRow.feature"},
NoColors: true,
Strict: true,
Randomize: time.Now().UTC().UnixNano(),
},
}
status := suite.Run()

if status == 0 {
t.Fatal("expected scenario failure")
}

out := buf.String()
require.Contains(t, out, "Diff vs closest row")
require.Contains(t, out, "Scenario: Diff With Vars")
require.Contains(t, out, "Scenario: Diff Transposed")
require.Contains(t, out, "matched 4/5 columns")
require.Contains(t, out, "matched 5/6 columns")
require.Contains(t, out, "id | 99")
require.Contains(t, out, "age | 32")

if block := diffBlock(out); block != "" {
require.NotContains(t, block, "$created_at")
}
}

func diffBlock(out string) string {
start := strings.Index(out, "Diff vs closest row")
if start == -1 {
return ""
}

block := out[start:]
if next := strings.Index(block, "Scenario: "); next > 0 {
block = block[:next]
}

return block
}
Loading