Merge pull request #190 from ericTsiliacos/what-if-we-diff-some-strings

Attempt to diff long strings when not equal
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..."
+`)