Attempt to diff long strings when not equal
Our goal in this PR was to improve the UX for Equal() when both expected and
actual are very, very long strings.
Long strings (with more than 50 characters) that are not equal are typically
hard to visually diff when your tests fail. This change attempts to find the
very first mismatch in the strings and to visually point out where the diff
is. It additionally truncates the text, so it is easier to see the exact point.
Assumptions:
* people only need ~5 characters around the diff to identify WHERE it is
* 50 characters is a reasonable threshold for deciding to diff strings in this manner
* Equal() is the only matcher that needs this treatment (for now)
Would love feedback on these assumptions, or anything else.
diff --git a/format/format.go b/format/format.go
index fde341f..f070906 100644
--- a/format/format.go
+++ b/format/format.go
@@ -70,6 +70,74 @@
}
/*
+
+Generates a nicely formatted matcher success / failure message
+
+Much like Message(...), but it attempts to pretty print diffs in strings
+
+Expected
+ <string>: "...aaaaabaaaaa..."
+to equal |
+ <string>: "...aaaaazaaaaa..."
+
+*/
+
+func MessageWithDiff(actual, message, expected string) string {
+ if len(actual) >= truncateThreshold && len(expected) >= truncateThreshold {
+ diffPoint := findFirstMismatch(actual, expected)
+ formattedActual := truncateAndFormat(actual, diffPoint)
+ formattedExpected := truncateAndFormat(expected, diffPoint)
+
+ spacesBeforeFormattedMismatch := findFirstMismatch(formattedActual, formattedExpected)
+
+ tabLength := 4
+ spaceFromMessageToActual := tabLength + len("<string>: ") - len(message)
+ padding := strings.Repeat(" ", spaceFromMessageToActual+spacesBeforeFormattedMismatch) + "|"
+ return Message(formattedActual, message+padding, formattedExpected)
+ }
+ return Message(actual, message, expected)
+}
+
+func truncateAndFormat(str string, index int) string {
+ leftPadding := `...`
+ rightPadding := `...`
+
+ start := index - charactersAroundMismatchToInclude
+ if start < 0 {
+ start = 0
+ leftPadding = ""
+ }
+
+ // slice index must include the mis-matched character
+ lengthOfMismatchedCharacter := 1
+ end := index + charactersAroundMismatchToInclude + lengthOfMismatchedCharacter
+ if end > len(str) {
+ end = len(str)
+ rightPadding = ""
+
+ }
+ return fmt.Sprintf("\"%s\"", leftPadding+str[start:end]+rightPadding)
+}
+
+func findFirstMismatch(a, b string) int {
+ aSlice := strings.Split(a, "")
+ bSlice := strings.Split(b, "")
+
+ for index, str := range aSlice {
+ if str != bSlice[index] {
+ return index
+ }
+ }
+
+ return 0
+}
+
+const (
+ truncateThreshold = 50
+ charactersAroundMismatchToInclude = 5
+)
+
+/*
Pretty prints the passed in object at the passed in indentation level.
Object recurses into deeply nested objects emitting pretty-printed representations of their components.
diff --git a/format/format_test.go b/format/format_test.go
index a27cea2..0814a2b 100644
--- a/format/format_test.go
+++ b/format/format_test.go
@@ -130,6 +130,29 @@
})
})
+ Describe("MessageWithDiff", func() {
+ It("shows the exact point where two long strings differ", func() {
+ stringWithB := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ stringWithZ := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaazaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+ Ω(MessageWithDiff(stringWithB, "to equal", stringWithZ)).Should(Equal(expectedLongStringFailureMessage))
+ })
+
+ It("truncates the start of long strings that differ only at their end", func() {
+ stringWithB := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab"
+ stringWithZ := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaz"
+
+ Ω(MessageWithDiff(stringWithB, "to equal", stringWithZ)).Should(Equal(expectedTruncatedStartStringFailureMessage))
+ })
+
+ It("truncates the end of long strings that differ only at their start", func() {
+ stringWithB := "baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ stringWithZ := "zaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+ Ω(MessageWithDiff(stringWithB, "to equal", stringWithZ)).Should(Equal(expectedTruncatedEndStringFailureMessage))
+ })
+ })
+
Describe("IndentString", func() {
It("should indent the string", func() {
Ω(IndentString("foo\n bar\nbaz", 2)).Should(Equal(" foo\n bar\n baz"))
@@ -526,3 +549,28 @@
})
})
})
+
+var expectedShortStringFailureMessage = strings.TrimSpace(`
+Expected
+ <string>: tim
+to equal
+ <string>: eric
+`)
+var expectedLongStringFailureMessage = strings.TrimSpace(`
+Expected
+ <string>: "...aaaaabaaaaa..."
+to equal |
+ <string>: "...aaaaazaaaaa..."
+`)
+var expectedTruncatedEndStringFailureMessage = strings.TrimSpace(`
+Expected
+ <string>: "baaaaa..."
+to equal |
+ <string>: "zaaaaa..."
+`)
+var expectedTruncatedStartStringFailureMessage = strings.TrimSpace(`
+Expected
+ <string>: "...aaaaab"
+to equal |
+ <string>: "...aaaaaz"
+`)
diff --git a/matchers/equal_matcher.go b/matchers/equal_matcher.go
index d186597..874e6a6 100644
--- a/matchers/equal_matcher.go
+++ b/matchers/equal_matcher.go
@@ -19,6 +19,12 @@
}
func (matcher *EqualMatcher) FailureMessage(actual interface{}) (message string) {
+ actualString, actualOK := actual.(string)
+ expectedString, expectedOK := matcher.Expected.(string)
+ if actualOK && expectedOK {
+ return format.MessageWithDiff(actualString, "to equal", expectedString)
+ }
+
return format.Message(actual, "to equal", matcher.Expected)
}
diff --git a/matchers/equal_matcher_test.go b/matchers/equal_matcher_test.go
index ef0d137..2add0b7 100644
--- a/matchers/equal_matcher_test.go
+++ b/matchers/equal_matcher_test.go
@@ -2,6 +2,8 @@
import (
"errors"
+ "strings"
+
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
. "github.com/onsi/gomega/matchers"
@@ -41,4 +43,36 @@
Ω(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b"}}).ShouldNot(Equal(myCustomType{s: "foo", n: 3, f: 2.0, arr: []string{"a", "b", "c"}}))
})
})
+
+ Describe("failure messages", func() {
+ It("shows the two strings simply when they are short", func() {
+ subject := EqualMatcher{Expected: "eric"}
+
+ failureMessage := subject.FailureMessage("tim")
+ Ω(failureMessage).To(BeEquivalentTo(expectedShortStringFailureMessage))
+ })
+
+ It("shows the exact point where two long strings differ", func() {
+ stringWithB := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+ stringWithZ := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaazaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+
+ subject := EqualMatcher{Expected: stringWithZ}
+
+ failureMessage := subject.FailureMessage(stringWithB)
+ Ω(failureMessage).To(BeEquivalentTo(expectedLongStringFailureMessage))
+ })
+ })
})
+
+var expectedShortStringFailureMessage = strings.TrimSpace(`
+Expected
+ <string>: tim
+to equal
+ <string>: eric
+`)
+var expectedLongStringFailureMessage = strings.TrimSpace(`
+Expected
+ <string>: "...aaaaabaaaaa..."
+to equal |
+ <string>: "...aaaaazaaaaa..."
+`)