From bc848bae7a83d758609c33c7695549571eea1ee4 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Thu, 22 Feb 2024 10:55:43 +0400 Subject: [PATCH] assert: switch the diff library to go-diff --- assert/assertions.go | 87 +++++++++++++++++++++++++++++++++------ assert/assertions_test.go | 31 +++++--------- go.mod | 1 + go.sum | 15 ++++++- 4 files changed, 99 insertions(+), 35 deletions(-) diff --git a/assert/assertions.go b/assert/assertions.go index 753afa496..d973fe518 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -12,13 +12,15 @@ import ( "regexp" "runtime" "runtime/debug" + "strconv" "strings" "time" "unicode" "unicode/utf8" + "github.com/sergi/go-diff/diffmatchpatch" + "github.com/davecgh/go-spew/spew" - "github.com/pmezard/go-difflib/difflib" "gopkg.in/yaml.v3" ) @@ -145,8 +147,6 @@ func copyExportedFields(expected interface{}) interface{} { // structures. // // This function does no assertion of any kind. -// -// Deprecated: Use [EqualExportedValues] instead. func ObjectsExportedFieldsAreEqual(expected, actual interface{}) bool { expectedCleaned := copyExportedFields(expected) actualCleaned := copyExportedFields(actual) @@ -1808,18 +1808,79 @@ func diff(expected interface{}, actual interface{}) string { e = spewConfig.Sdump(expected) a = spewConfig.Sdump(actual) } + structuredDiff := structuredDiff(e, a) + prettyDiff := prettyDiff(structuredDiff) + return "\n\nDiff:\n" + prettyDiff +} + +func structuredDiff(e string, a string) []diffmatchpatch.Diff { + dmp := diffmatchpatch.New() + fromRunes, toRunes, runesToLines := dmp.DiffLinesToRunes(e, a) + diffs := dmp.DiffMainRunes(fromRunes, toRunes, false) + hydrated := make([]diffmatchpatch.Diff, 0, len(diffs)) + for _, aDiff := range diffs { + chars := strings.FieldsFunc(aDiff.Text, func(r rune) bool { + return string(r) == diffmatchpatch.IndexSeparator + }) + text := make([]string, len(chars)) + + for i, char := range chars { + i1, err := strconv.Atoi(char) + if err == nil { + text[i] = runesToLines[i1] + } + } + for idx, line := range text { + if aDiff.Type == diffmatchpatch.DiffEqual && idx < len(text)-1 { + continue + } + hydrated = append(hydrated, diffmatchpatch.Diff{ + Type: aDiff.Type, + Text: line, + }) + } + } + return hydrated +} + +func prettyDiff(diffs []diffmatchpatch.Diff) string { + var diff strings.Builder - diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(e), - B: difflib.SplitLines(a), - FromFile: "Expected", - FromDate: "", - ToFile: "Actual", - ToDate: "", - Context: 1, - }) + diff.WriteString("--- Expected\n+++ Actual\n") - return "\n\nDiff:\n" + diff + for _, diffChunk := range diffs { + switch diffChunk.Type { + case diffmatchpatch.DiffInsert: + // Make sure the different parts are on separate lines for better readability + // i.e. it makes diffs like +got-expected to go as +got\n-expected\n + if !strings.HasSuffix(diffChunk.Text, "\n") { + diffChunk.Text = diffChunk.Text + "\n" + } + _, _ = fmt.Fprintf(&diff, "+%s", diffChunk.Text) + case diffmatchpatch.DiffDelete: + // Make sure the different parts are on separate lines for better readability + // i.e. it makes diffs like +got-expected to go as +got\n-expected\n + if !strings.HasSuffix(diffChunk.Text, "\n") { + diffChunk.Text = diffChunk.Text + "\n" + } + _, _ = fmt.Fprintf(&diff, "-%s", diffChunk.Text) + default: + if len(diffChunk.Text) == 0 { + continue + } + equalTextByLines := strings.SplitAfter(diffChunk.Text, "\n") + var linesTrimmed []string + for _, line := range equalTextByLines { + if len(line) == 0 { + continue + } + linesTrimmed = append(linesTrimmed, line) + } + // We're not interested in the equal parts, so only keep the last line for some context + _, _ = fmt.Fprintf(&diff, " %s", equalTextByLines[len(linesTrimmed)-1]) + } + } + return diff.String() } func isFunction(arg interface{}) bool { diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 6532f9cfd..a568b9ef7 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -385,11 +385,10 @@ func TestEqualExportedValues(t *testing.T) { Diff: --- Expected +++ Actual - @@ -3,3 +3,3 @@ Exported2: (assert.Nested) { - Exported: (int) 2, + Exported: (int) 1, - notExported: (interface {}) `, + }`, }, { value1: S3{&Nested{1, 2}, &Nested{3, 4}}, @@ -399,11 +398,10 @@ func TestEqualExportedValues(t *testing.T) { Diff: --- Expected +++ Actual - @@ -2,3 +2,3 @@ Exported1: (*assert.Nested)({ - Exported: (int) 1, + Exported: (string) (len=1) "a", - notExported: (interface {}) `, + }`, }, { value1: S4{[]*Nested{ @@ -419,11 +417,10 @@ func TestEqualExportedValues(t *testing.T) { Diff: --- Expected +++ Actual - @@ -7,3 +7,3 @@ (*assert.Nested)({ - Exported: (int) 3, + Exported: (int) 2, - notExported: (interface {}) `, + }`, }, { value1: S{[2]int{1, 2}, Nested{2, 3}, 4, Nested{5, 6}}, @@ -656,7 +653,7 @@ func TestStringEqual(t *testing.T) { msgAndArgs []interface{} want string }{ - {equalWant: "hi, \nmy name is", equalGot: "what,\nmy name is", want: "\tassertions.go:\\d+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"hi, \\\\nmy name is\"\n\\s+actual\\s+: \"what,\\\\nmy name is\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1,2 \\+1,2 @@\n\\s+-hi, \n\\s+\\+what,\n\\s+my name is"}, + {equalWant: "hi, \nmy name is", equalGot: "what,\nmy name is", want: "\tassertions.go:\\d+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"hi, \\\\nmy name is\"\n\\s+actual\\s+: \"what,\\\\nmy name is\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+-hi, \n\\s+\\+what,\n\\s+my name is"}, } { mockT := &bufferT{} Equal(mockT, currCase.equalWant, currCase.equalGot, currCase.msgAndArgs...) @@ -671,10 +668,10 @@ func TestEqualFormatting(t *testing.T) { msgAndArgs []interface{} want string }{ - {equalWant: "want", equalGot: "got", want: "\tassertions.go:\\d+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1 \\+1 @@\n\\s+-want\n\\s+\\+got\n"}, - {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{"hello, %v!", "world"}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1 \\+1 @@\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+hello, world!\n"}, - {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{123}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1 \\+1 @@\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+123\n"}, - {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{struct{ a string }{"hello"}}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+@@ -1 \\+1 @@\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+{a:hello}\n"}, + {equalWant: "want", equalGot: "got", want: "\tassertions.go:\\d+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+-want\n\\s+\\+got\n"}, + {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{"hello, %v!", "world"}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+hello, world!\n"}, + {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{123}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+123\n"}, + {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{struct{ a string }{"hello"}}, want: "\tassertions.go:[0-9]+: \n\t+Error Trace:\t\n\t+Error:\\s+Not equal:\\s+\n\\s+expected: \"want\"\n\\s+actual\\s+: \"got\"\n\\s+Diff:\n\\s+-+ Expected\n\\s+\\++ Actual\n\\s+-want\n\\s+\\+got\n\\s+Messages:\\s+{a:hello}\n"}, } { mockT := &bufferT{} Equal(mockT, currCase.equalWant, currCase.equalGot, currCase.msgAndArgs...) @@ -2301,7 +2298,6 @@ func TestDiff(t *testing.T) { Diff: --- Expected +++ Actual -@@ -1,3 +1,3 @@ (struct { foo string }) { - foo: (string) (len=5) "hello" + foo: (string) (len=3) "bar" @@ -2318,7 +2314,6 @@ Diff: Diff: --- Expected +++ Actual -@@ -2,5 +2,5 @@ (int) 1, - (int) 2, (int) 3, @@ -2338,11 +2333,10 @@ Diff: Diff: --- Expected +++ Actual -@@ -2,4 +2,4 @@ (int) 1, - (int) 2, -- (int) 3 + (int) 3, +- (int) 3 + (int) 5 } ` @@ -2357,14 +2351,13 @@ Diff: Diff: --- Expected +++ Actual -@@ -1,6 +1,6 @@ (map[string]int) (len=4) { - (string) (len=4) "four": (int) 4, + (string) (len=4) "five": (int) 5, (string) (len=3) "one": (int) 1, - (string) (len=5) "three": (int) 3, -- (string) (len=3) "two": (int) 2 + (string) (len=5) "seven": (int) 7, +- (string) (len=3) "two": (int) 2 + (string) (len=5) "three": (int) 3 } ` @@ -2380,7 +2373,6 @@ Diff: Diff: --- Expected +++ Actual -@@ -1,3 +1,3 @@ (*errors.errorString)({ - s: (string) (len=19) "some expected error" + s: (string) (len=12) "actual error" @@ -2398,7 +2390,6 @@ Diff: Diff: --- Expected +++ Actual -@@ -2,3 +2,3 @@ A: (string) (len=11) "some string", - B: (int) 10 + B: (int) 15 @@ -2416,10 +2407,8 @@ Diff: Diff: --- Expected +++ Actual -@@ -1,2 +1,2 @@ -(time.Time) 2020-09-24 00:00:00 +0000 UTC +(time.Time) 2020-09-25 00:00:00 +0000 UTC - ` actual = diff( diff --git a/go.mod b/go.mod index 9133c4a8f..3d48e8fba 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ go 1.17 require ( github.com/davecgh/go-spew v1.1.1 github.com/pmezard/go-difflib v1.0.0 + github.com/sergi/go-diff v1.3.1 github.com/stretchr/objx v0.5.1 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index f218355d1..b6ba4abbe 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,23 @@ +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/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 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/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=