diff --git a/assert/assertion_compare_test.go b/assert/assertion_compare_test.go index a38d88060..86a14be78 100644 --- a/assert/assertion_compare_test.go +++ b/assert/assertion_compare_test.go @@ -432,7 +432,7 @@ func Test_containsValue(t *testing.T) { func TestComparingMsgAndArgsForwarding(t *testing.T) { msgAndArgs := []interface{}{"format %s %x", "this", 0xc001} - expectedOutput := "format this c001\n" + expectedOutput := "format this c001" funcs := []func(t TestingT){ func(t TestingT) { Greater(t, 1, 2, msgAndArgs...) }, func(t TestingT) { GreaterOrEqual(t, 1, 2, msgAndArgs...) }, diff --git a/assert/assertion_order_test.go b/assert/assertion_order_test.go index eefe06127..aa857c8a2 100644 --- a/assert/assertion_order_test.go +++ b/assert/assertion_order_test.go @@ -187,7 +187,7 @@ func TestIsNonDecreasing(t *testing.T) { func TestOrderingMsgAndArgsForwarding(t *testing.T) { msgAndArgs := []interface{}{"format %s %x", "this", 0xc001} - expectedOutput := "format this c001\n" + expectedOutput := "format this c001" collection := []int{1, 2, 1} funcs := []func(t TestingT){ func(t TestingT) { IsIncreasing(t, collection, msgAndArgs...) }, diff --git a/assert/assertions.go b/assert/assertions.go index fa1245b18..011019071 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -19,7 +19,6 @@ import ( "unicode/utf8" "github.com/davecgh/go-spew/spew" - "github.com/pmezard/go-difflib/difflib" yaml "gopkg.in/yaml.v3" ) @@ -244,6 +243,8 @@ func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } + + failureMessage = brightRed(failureMessage) content := []labeledContent{ {"Error Trace", strings.Join(CallerInfo(), "\n\t\t\t")}, {"Error", failureMessage}, @@ -253,10 +254,12 @@ func Fail(t TestingT, failureMessage string, msgAndArgs ...interface{}) bool { if n, ok := t.(interface { Name() string }); ok { - content = append(content, labeledContent{"Test", n.Name()}) + testName := blue(n.Name()) + content = append(content, labeledContent{"Test", testName}) } message := messageFromMsgAndArgs(msgAndArgs...) + message = yellow(message) if len(message) > 0 { content = append(content, labeledContent{"Messages", message}) } @@ -320,7 +323,10 @@ func IsType(t TestingT, expectedType interface{}, object interface{}, msgAndArgs } if !ObjectsAreEqual(reflect.TypeOf(object), reflect.TypeOf(expectedType)) { - return Fail(t, fmt.Sprintf("Object expected to be of type %v, but was %v", reflect.TypeOf(expectedType), reflect.TypeOf(object)), msgAndArgs...) + expectedLines := green("expected: %v\n", reflect.TypeOf(expectedType)) + actualLines := red("actual : %v", reflect.TypeOf(object)) + return Fail(t, fmt.Sprintf("Object type mismatch\n%s%s", + expectedLines, actualLines), msgAndArgs...) } return true @@ -345,9 +351,10 @@ func Equal(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) if !ObjectsAreEqual(expected, actual) { diff := diff(expected, actual) expected, actual = formatUnequalValues(expected, actual) - return Fail(t, fmt.Sprintf("Not equal: \n"+ - "expected: %s\n"+ - "actual : %s%s", expected, actual, diff), msgAndArgs...) + expectedLines := green("expected: %s\n", expected) + actualLines := red("actual : %s", actual) + return Fail(t, fmt.Sprintf("Not equal: \n%s%s%s", + expectedLines, actualLines, diff), msgAndArgs...) } return true @@ -379,9 +386,9 @@ func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) b } if !samePointers(expected, actual) { - return Fail(t, fmt.Sprintf("Not same: \n"+ - "expected: %p %#v\n"+ - "actual : %p %#v", expected, expected, actual, actual), msgAndArgs...) + expectedLines := green("expected: %p %#v\n", expected, expected) + actualLines := red("actual : %p %#v", actual, actual) + return Fail(t, fmt.Sprintf("Not same: \n%s%s", expectedLines, actualLines), msgAndArgs...) } return true @@ -466,9 +473,10 @@ func EqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...interfa if !ObjectsAreEqualValues(expected, actual) { diff := diff(expected, actual) expected, actual = formatUnequalValues(expected, actual) - return Fail(t, fmt.Sprintf("Not equal: \n"+ - "expected: %s\n"+ - "actual : %s%s", expected, actual, diff), msgAndArgs...) + expectedLines := green("expected: %s\n", expected) + actualLines := red("actual : %s", actual) + return Fail(t, fmt.Sprintf("Not equal: \n%s%s%s", + expectedLines, actualLines, diff), msgAndArgs...) } return true @@ -487,7 +495,10 @@ func Exactly(t TestingT, expected, actual interface{}, msgAndArgs ...interface{} bType := reflect.TypeOf(actual) if aType != bType { - return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%v != %v", aType, bType), msgAndArgs...) + expectedLines := green("%v", aType) + actualLines := red("%v", bType) + return Fail(t, fmt.Sprintf("Types expected to match exactly\n\t%s != %s", + expectedLines, actualLines), msgAndArgs...) } return Equal(t, expected, actual, msgAndArgs...) @@ -640,11 +651,11 @@ func Len(t TestingT, object interface{}, length int, msgAndArgs ...interface{}) } ok, l := getLen(object) if !ok { - return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", object), msgAndArgs...) + return Fail(t, fmt.Sprintf("\"%#v\" could not be applied builtin len()", object), msgAndArgs...) } if l != length { - return Fail(t, fmt.Sprintf("\"%s\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) + return Fail(t, fmt.Sprintf("\"%#v\" should have %d item(s), but has %d", object, length, l), msgAndArgs...) } return true } @@ -690,8 +701,10 @@ func NotEqual(t TestingT, expected, actual interface{}, msgAndArgs ...interface{ h.Helper() } if err := validateEqualArgs(expected, actual); err != nil { - return Fail(t, fmt.Sprintf("Invalid operation: %#v != %#v (%s)", - expected, actual, err), msgAndArgs...) + expectedLines := green("%#v", expected) + actualLines := red("%#v", actual) + return Fail(t, fmt.Sprintf("Invalid operation: %s\n%s != %s", + err.Error(), expectedLines, actualLines), msgAndArgs...) } if ObjectsAreEqual(expected, actual) { @@ -711,7 +724,7 @@ func NotEqualValues(t TestingT, expected, actual interface{}, msgAndArgs ...inte } if ObjectsAreEqualValues(expected, actual) { - return Fail(t, fmt.Sprintf("Should not be: %#v\n", actual), msgAndArgs...) + return Fail(t, fmt.Sprintf("Should not be: %#v", actual), msgAndArgs...) } return true @@ -796,10 +809,10 @@ func NotContains(t TestingT, s, contains interface{}, msgAndArgs ...interface{}) ok, found := containsElement(s, contains) if !ok { - return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", s), msgAndArgs...) + return Fail(t, fmt.Sprintf("\"%#v\" could not be applied builtin len()", s), msgAndArgs...) } if found { - return Fail(t, fmt.Sprintf("\"%s\" should not contain \"%s\"", s, contains), msgAndArgs...) + return Fail(t, fmt.Sprintf("\"%#v\" should not contain \"%#v\"", s, contains), msgAndArgs...) } return true @@ -828,11 +841,11 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok subsetKind := reflect.TypeOf(subset).Kind() if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", list, listKind), msgAndArgs...) } if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", subset, subsetKind), msgAndArgs...) } subsetValue := reflect.ValueOf(subset) @@ -846,7 +859,7 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok listElement := listValue.MapIndex(subsetKey).Interface() if !ObjectsAreEqual(subsetElement, listElement) { - return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, subsetElement), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v does not contain \"%#v\"", list, subsetElement), msgAndArgs...) } } @@ -857,10 +870,10 @@ func Subset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) (ok element := subsetValue.Index(i).Interface() ok, found := containsElement(list, element) if !ok { - return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...) } if !found { - return Fail(t, fmt.Sprintf("\"%s\" does not contain \"%s\"", list, element), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v does not contain \"%#v\"", list, element), msgAndArgs...) } } @@ -889,11 +902,11 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) subsetKind := reflect.TypeOf(subset).Kind() if listKind != reflect.Array && listKind != reflect.Slice && listKind != reflect.Map { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s", list, listKind), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", list, listKind), msgAndArgs...) } if subsetKind != reflect.Array && subsetKind != reflect.Slice && listKind != reflect.Map { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s", subset, subsetKind), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v has an unsupported type %s", subset, subsetKind), msgAndArgs...) } subsetValue := reflect.ValueOf(subset) @@ -911,21 +924,21 @@ func NotSubset(t TestingT, list, subset interface{}, msgAndArgs ...interface{}) } } - return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v is a subset of %#v", subset, list), msgAndArgs...) } for i := 0; i < subsetValue.Len(); i++ { element := subsetValue.Index(i).Interface() ok, found := containsElement(list, element) if !ok { - return Fail(t, fmt.Sprintf("\"%s\" could not be applied builtin len()", list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v could not be applied builtin len()", list), msgAndArgs...) } if !found { return true } } - return Fail(t, fmt.Sprintf("%q is a subset of %q", subset, list), msgAndArgs...) + return Fail(t, fmt.Sprintf("%#v is a subset of %#v", subset, list), msgAndArgs...) } // ElementsMatch asserts that the specified listA(array, slice...) is equal to specified @@ -958,7 +971,10 @@ func ElementsMatch(t TestingT, listA, listB interface{}, msgAndArgs ...interface func isList(t TestingT, list interface{}, msgAndArgs ...interface{}) (ok bool) { kind := reflect.TypeOf(list).Kind() if kind != reflect.Array && kind != reflect.Slice { - return Fail(t, fmt.Sprintf("%q has an unsupported type %s, expecting array or slice", list, kind), + expectedLines := green("expected: %s or %s\n", reflect.Array, reflect.Slice) + actualLines := red("actual : %s", kind) + return Fail(t, fmt.Sprintf("%#v has an unsupported type\n%s%s", + list, expectedLines, actualLines), msgAndArgs...) } return true @@ -1006,20 +1022,22 @@ func diffLists(listA, listB interface{}) (extraA, extraB []interface{}) { func formatListDiff(listA, listB interface{}, extraA, extraB []interface{}) string { var msg bytes.Buffer + var text string msg.WriteString("elements differ") if len(extraA) > 0 { - msg.WriteString("\n\nextra elements in list A:\n") - msg.WriteString(spewConfig.Sdump(extraA)) + text = green("\n\nextra elements in list A:\n%s", spewConfig.Sdump(extraA)) + msg.WriteString(text) } if len(extraB) > 0 { - msg.WriteString("\n\nextra elements in list B:\n") - msg.WriteString(spewConfig.Sdump(extraB)) + text = red("\nextra elements in list B:\n%s", spewConfig.Sdump(extraB)) + msg.WriteString(text) } - msg.WriteString("\n\nlistA:\n") - msg.WriteString(spewConfig.Sdump(listA)) - msg.WriteString("\n\nlistB:\n") - msg.WriteString(spewConfig.Sdump(listB)) + text = green("\nlistA:\n%s", spewConfig.Sdump(listA)) + msg.WriteString(text) + + text = red("\nlistB:\n%s", spewConfig.Sdump(listB)) + msg.WriteString(text) return msg.String() } @@ -1066,8 +1084,9 @@ func Panics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { h.Helper() } - if funcDidPanic, panicValue, _ := didPanic(f); !funcDidPanic { - return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + if funcDidPanic, _, _ := didPanic(f); !funcDidPanic { + failureMsg := brightRed("func %#v should panic", f) + return Fail(t, failureMsg, msgAndArgs...) } return true @@ -1084,10 +1103,15 @@ func PanicsWithValue(t TestingT, expected interface{}, f PanicTestFunc, msgAndAr funcDidPanic, panicValue, panickedStack := didPanic(f) if !funcDidPanic { - return Fail(t, fmt.Sprintf("func %#v should panic\n\tPanic value:\t%#v", f, panicValue), msgAndArgs...) + failureMsg := brightRed("func %#v should panic", f) + return Fail(t, failureMsg, msgAndArgs...) } if panicValue != expected { - return Fail(t, fmt.Sprintf("func %#v should panic with value:\t%#v\n\tPanic value:\t%#v\n\tPanic stack:\t%s", f, expected, panicValue, panickedStack), msgAndArgs...) + failureMsg := brightRed("func %#v should panic with particular value\n", f) + expectedPanicValue := green("expected: %#v\n", expected) + actualPanicValue := red("actual : %#v\n\nPanic stack:\n%s", panicValue, panickedStack) + message := fmt.Sprintf("%s%s%s", failureMsg, expectedPanicValue, actualPanicValue) + return Fail(t, message, msgAndArgs...) } return true @@ -1109,7 +1133,11 @@ func PanicsWithError(t TestingT, errString string, f PanicTestFunc, msgAndArgs . } panicErr, ok := panicValue.(error) if !ok || panicErr.Error() != errString { - return Fail(t, fmt.Sprintf("func %#v should panic with error message:\t%#v\n\tPanic value:\t%#v\n\tPanic stack:\t%s", f, errString, panicValue, panickedStack), msgAndArgs...) + failureMsg := brightRed("func %#v should panic with particular value\n", f) + expectedPanicValue := green("expected: %#v\n", errString) + actualPanicValue := red("actual : %#v\n\nPanic stack:\n%s", panicValue, panickedStack) + message := fmt.Sprintf("%s%s%s", failureMsg, expectedPanicValue, actualPanicValue) + return Fail(t, message, msgAndArgs...) } return true @@ -1124,7 +1152,10 @@ func NotPanics(t TestingT, f PanicTestFunc, msgAndArgs ...interface{}) bool { } if funcDidPanic, panicValue, panickedStack := didPanic(f); funcDidPanic { - return Fail(t, fmt.Sprintf("func %#v should not panic\n\tPanic value:\t%v\n\tPanic stack:\t%s", f, panicValue, panickedStack), msgAndArgs...) + failureMsg := brightRed("func %#v should NOT panic\n", f) + panicDetails := red("Panic value: %#v\n\nPanic stack:\n%s", panicValue, panickedStack) + message := fmt.Sprintf("%s%s", failureMsg, panicDetails) + return Fail(t, message, msgAndArgs...) } return true @@ -1287,11 +1318,11 @@ func InDeltaMapValues(t TestingT, expected, actual interface{}, delta float64, m av := actualMap.MapIndex(k) if !ev.IsValid() { - return Fail(t, fmt.Sprintf("missing key %q in expected map", k), msgAndArgs...) + return Fail(t, fmt.Sprintf("missing key %#v in expected map", k), msgAndArgs...) } if !av.IsValid() { - return Fail(t, fmt.Sprintf("missing key %q in actual map", k), msgAndArgs...) + return Fail(t, fmt.Sprintf("missing key %#v in actual map", k), msgAndArgs...) } if !InDelta( @@ -1343,8 +1374,10 @@ func InEpsilon(t TestingT, expected, actual interface{}, epsilon float64, msgAnd return Fail(t, err.Error(), msgAndArgs...) } if actualEpsilon > epsilon { - return Fail(t, fmt.Sprintf("Relative error is too high: %#v (expected)\n"+ - " < %#v (actual)", epsilon, actualEpsilon), msgAndArgs...) + expectedLines := green("expected: %#v\n", epsilon) + actualLines := red("actual : %#v", actualEpsilon) + return Fail(t, fmt.Sprintf("Relative error is too high\n%s%s", + expectedLines, actualLines), msgAndArgs...) } return true @@ -1365,7 +1398,7 @@ func InEpsilonSlice(t TestingT, expected, actual interface{}, epsilon float64, m expectedSlice := reflect.ValueOf(expected) for i := 0; i < actualSlice.Len(); i++ { - result := InEpsilon(t, actualSlice.Index(i).Interface(), expectedSlice.Index(i).Interface(), epsilon) + result := InEpsilon(t, expectedSlice.Index(i).Interface(), actualSlice.Index(i).Interface(), epsilon) if !result { return result } @@ -1428,9 +1461,10 @@ func EqualError(t TestingT, theError error, errString string, msgAndArgs ...inte actual := theError.Error() // don't need to use deep equals here, we know they are both strings if expected != actual { - return Fail(t, fmt.Sprintf("Error message not equal:\n"+ - "expected: %q\n"+ - "actual : %q", expected, actual), msgAndArgs...) + expectedLines := green("expected: %s\n", expected) + actualLines := red("actual : %s", actual) + return Fail(t, fmt.Sprintf("Error message not equal:\n%s%s", + expectedLines, actualLines), msgAndArgs...) } return true } @@ -1682,16 +1716,15 @@ func diff(expected interface{}, actual interface{}) string { a = spewConfig.Sdump(actual) } - diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ - A: difflib.SplitLines(e), - B: difflib.SplitLines(a), + diff, _ := getUnifiedDiffString(unifiedDiff{ + A: splitLines(e), + B: splitLines(a), FromFile: "Expected", FromDate: "", ToFile: "Actual", ToDate: "", Context: 1, }) - return "\n\nDiff:\n" + diff } @@ -1806,10 +1839,11 @@ func ErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { chain := buildErrorChainString(err) - return Fail(t, fmt.Sprintf("Target error should be in err chain:\n"+ - "expected: %q\n"+ - "in chain: %s", expectedText, chain, - ), msgAndArgs...) + expectedLines := green("expected: %s\n", expectedText) + actualLines := red("in chain: %s", chain) + + return Fail(t, fmt.Sprintf("Target error should be in err chain:\n%s%s", + expectedLines, actualLines), msgAndArgs...) } // NotErrorIs asserts that at none of the errors in err's chain matches target. @@ -1822,17 +1856,18 @@ func NotErrorIs(t TestingT, err, target error, msgAndArgs ...interface{}) bool { return true } - var expectedText string + var notExpectedText string if target != nil { - expectedText = target.Error() + notExpectedText = target.Error() } chain := buildErrorChainString(err) - return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n"+ - "found: %q\n"+ - "in chain: %s", expectedText, chain, - ), msgAndArgs...) + notExpectedLines := green("not expected: %s\n", notExpectedText) + actualLines := red("in chain : %s", chain) + + return Fail(t, fmt.Sprintf("Target error should not be in err chain:\n%s%s", + notExpectedLines, actualLines), msgAndArgs...) } // ErrorAs asserts that at least one of the errors in err's chain matches target, and if so, sets target to that error value. @@ -1847,10 +1882,11 @@ func ErrorAs(t TestingT, err error, target interface{}, msgAndArgs ...interface{ chain := buildErrorChainString(err) - return Fail(t, fmt.Sprintf("Should be in error chain:\n"+ - "expected: %q\n"+ - "in chain: %s", target, chain, - ), msgAndArgs...) + expectedLines := green("expected: %#v\n", target) + actualLines := red("in chain: %s", chain) + + return Fail(t, fmt.Sprintf("Should be in error chain:\n%s%s", + expectedLines, actualLines), msgAndArgs...) } func buildErrorChainString(err error) string { diff --git a/assert/assertions_test.go b/assert/assertions_test.go index bdab184c1..516a02486 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -354,7 +354,9 @@ 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\\t\\tError Trace:\\t\\n\\t\\t\\tError:\\s+\\t\\x1b\\[31;1mNot equal: \\n\\t\\t\\t\\s+\\t\\x1b\\[32mexpected: \\\"hi, \\\\nmy name is\\\"\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31mactual : \\\"what,\\\\nmy name is\\\"\\x1b\\[0m\\n\\t\\t\\t\\s+\\t\\n\\t\\t\\t\\s+\\tDiff:\\n\\t\\t\\t\\s+\\t\\x1b\\[32m--- Expected\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\++ Actual\\n\\t\\t\\t\\s+\\t\\x1b\\[0m@@ \\x1b\\[32m-1,2\\x1b\\[0m \\x1b\\[31m\\+1,2\\x1b\\[0m @@\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-hi, \\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\+what,\\n\\t\\t\\t\\s+\\t\\x1b\\[0m my name is\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\n\\t\\t\\tMessages:\\s+\\t\\x1b\\[33m\\x1b\\[0m\\n"}, } { mockT := &bufferT{} Equal(mockT, currCase.equalWant, currCase.equalGot, currCase.msgAndArgs...) @@ -369,10 +371,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\\t\\tError Trace:\\t\\n\\t\\t\\tError:\\s+\\t\\x1b\\[31;1mNot equal: \\n\\t\\t\\t\\s+\\t\\x1b\\[32mexpected: \\\"want\\\"\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31mactual\\s+: \\\"got\\\"\\x1b\\[0m\\n\\t\\t\\t\\s+\\t\\n\\t\\t\\t\\s+\\tDiff:\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-+ Expected\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\++ Actual\\n\\t\\t\\t\\s+\\t\\x1b\\[0m@@ \\x1b\\[32m-1\\x1b\\[0m \\x1b\\[31m\\+1\\x1b\\[0m @@\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-want\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\+got\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[0m\\n\\t\\t\\tMessages:\\s+\\t\\x1b\\[33m\\x1b\\[0m\\n"}, + {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{"hello, %v!", "world"}, want: "\\tassertions\\.go:\\d+: \\n\\t\\t\\tError Trace:\\t\\n\\t\\t\\tError:\\s+\\t\\x1b\\[31;1mNot equal: \\n\\t\\t\\t\\s+\\t\\x1b\\[32mexpected: \\\"want\\\"\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31mactual\\s+: \\\"got\\\"\\x1b\\[0m\\n\\t\\t\\t\\s+\\t\\n\\t\\t\\t\\s+\\tDiff:\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-+ Expected\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\++ Actual\\n\\t\\t\\t\\s+\\t\\x1b\\[0m@@ \\x1b\\[32m-1\\x1b\\[0m \\x1b\\[31m\\+1\\x1b\\[0m @@\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-want\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\+got\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[0m\\n\\t\\t\\tMessages:\\s+\\t\\x1b\\[33mhello, world!\\x1b\\[0m\\n"}, + {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{123}, want: "\\tassertions\\.go:\\d+: \\n\\t\\t\\tError Trace:\\t\\n\\t\\t\\tError:\\s+\\t\\x1b\\[31;1mNot equal: \\n\\t\\t\\t\\s+\\t\\x1b\\[32mexpected: \\\"want\\\"\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31mactual\\s+: \\\"got\\\"\\x1b\\[0m\\n\\t\\t\\t\\s+\\t\\n\\t\\t\\t\\s+\\tDiff:\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-+ Expected\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\++ Actual\\n\\t\\t\\t\\s+\\t\\x1b\\[0m@@ \\x1b\\[32m-1\\x1b\\[0m \\x1b\\[31m\\+1\\x1b\\[0m @@\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-want\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\+got\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[0m\\n\\t\\t\\tMessages:\\s+\\t\\x1b\\[33m123\\x1b\\[0m\\n"}, + {equalWant: "want", equalGot: "got", msgAndArgs: []interface{}{struct{ a string }{"hello"}}, want: "\\tassertions\\.go:\\d+: \\n\\t\\t\\tError Trace:\\t\\n\\t\\t\\tError:\\s+\\t\\x1b\\[31;1mNot equal: \\n\\t\\t\\t\\s+\\t\\x1b\\[32mexpected: \\\"want\\\"\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31mactual\\s+: \\\"got\\\"\\x1b\\[0m\\n\\t\\t\\t\\s+\\t\\n\\t\\t\\t\\s+\\tDiff:\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-+ Expected\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\++ Actual\\n\\t\\t\\t\\s+\\t\\x1b\\[0m@@ \\x1b\\[32m-1\\x1b\\[0m \\x1b\\[31m\\+1\\x1b\\[0m @@\\n\\t\\t\\t\\s+\\t\\x1b\\[32m-want\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[31m\\+got\\n\\t\\t\\t\\s+\\t\\x1b\\[0m\\x1b\\[0m\\n\\t\\t\\tMessages:\\s+\\t\\x1b\\[33m\\{a:hello\\}\\x1b\\[0m\\n"}, } { mockT := &bufferT{} Equal(mockT, currCase.equalWant, currCase.equalGot, currCase.msgAndArgs...) @@ -1952,78 +1954,28 @@ func (d *diffTestingStruct) String() string { } func TestDiff(t *testing.T) { - expected := ` - -Diff: ---- Expected -+++ Actual -@@ -1,3 +1,3 @@ - (struct { foo string }) { -- foo: (string) (len=5) "hello" -+ foo: (string) (len=3) "bar" - } -` + expected := "\n\nDiff:\n\x1b[32m--- Expected\n\x1b[0m\x1b[31m+++ Actual\n\x1b[0m@@ \x1b[32m-1,3\x1b[0m \x1b[31m+1,3\x1b[0m @@\n (struct { foo string }) {\n\x1b[32m- foo: (string) (len=5) \"hello\"\n\x1b[0m\x1b[31m+ foo: (string) (len=3) \"bar\"\n\x1b[0m }\n" actual := diff( struct{ foo string }{"hello"}, struct{ foo string }{"bar"}, ) Equal(t, expected, actual) - expected = ` - -Diff: ---- Expected -+++ Actual -@@ -2,5 +2,5 @@ - (int) 1, -- (int) 2, - (int) 3, -- (int) 4 -+ (int) 5, -+ (int) 7 - } -` + expected = "\n\nDiff:\n\x1b[32m--- Expected\n\x1b[0m\x1b[31m+++ Actual\n\x1b[0m@@ \x1b[32m-2,5\x1b[0m \x1b[31m+2,5\x1b[0m @@\n (int) 1,\n\x1b[32m- (int) 2,\n\x1b[0m (int) 3,\n\x1b[32m- (int) 4\n\x1b[0m\x1b[31m+ (int) 5,\n\x1b[0m\x1b[31m+ (int) 7\n\x1b[0m }\n" actual = diff( []int{1, 2, 3, 4}, []int{1, 3, 5, 7}, ) Equal(t, expected, actual) - expected = ` - -Diff: ---- Expected -+++ Actual -@@ -2,4 +2,4 @@ - (int) 1, -- (int) 2, -- (int) 3 -+ (int) 3, -+ (int) 5 - } -` + expected = "\n\nDiff:\n\x1b[32m--- Expected\n\x1b[0m\x1b[31m+++ Actual\n\x1b[0m@@ \x1b[32m-2,4\x1b[0m \x1b[31m+2,4\x1b[0m @@\n (int) 1,\n\x1b[32m- (int) 2,\n\x1b[0m\x1b[32m- (int) 3\n\x1b[0m\x1b[31m+ (int) 3,\n\x1b[0m\x1b[31m+ (int) 5\n\x1b[0m }\n" actual = diff( []int{1, 2, 3, 4}[0:3], []int{1, 3, 5, 7}[0:3], ) Equal(t, expected, actual) - expected = ` - -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=5) "three": (int) 3 - } -` + expected = "\n\nDiff:\n\x1b[32m--- Expected\n\x1b[0m\x1b[31m+++ Actual\n\x1b[0m@@ \x1b[32m-1,6\x1b[0m \x1b[31m+1,6\x1b[0m @@\n (map[string]int) (len=4) {\n\x1b[32m- (string) (len=4) \"four\": (int) 4,\n\x1b[0m\x1b[31m+ (string) (len=4) \"five\": (int) 5,\n\x1b[0m (string) (len=3) \"one\": (int) 1,\n\x1b[32m- (string) (len=5) \"three\": (int) 3,\n\x1b[0m\x1b[32m- (string) (len=3) \"two\": (int) 2\n\x1b[0m\x1b[31m+ (string) (len=5) \"seven\": (int) 7,\n\x1b[0m\x1b[31m+ (string) (len=5) \"three\": (int) 3\n\x1b[0m }\n" actual = diff( map[string]int{"one": 1, "two": 2, "three": 3, "four": 4}, @@ -2031,17 +1983,7 @@ Diff: ) Equal(t, expected, actual) - expected = ` - -Diff: ---- Expected -+++ Actual -@@ -1,3 +1,3 @@ - (*errors.errorString)({ -- s: (string) (len=19) "some expected error" -+ s: (string) (len=12) "actual error" - }) -` + expected = "\n\nDiff:\n\x1b[32m--- Expected\n\x1b[0m\x1b[31m+++ Actual\n\x1b[0m@@ \x1b[32m-1,3\x1b[0m \x1b[31m+1,3\x1b[0m @@\n (*errors.errorString)({\n\x1b[32m- s: (string) (len=19) \"some expected error\"\n\x1b[0m\x1b[31m+ s: (string) (len=12) \"actual error\"\n\x1b[0m })\n" actual = diff( errors.New("some expected error"), @@ -2049,17 +1991,7 @@ Diff: ) Equal(t, expected, actual) - expected = ` - -Diff: ---- Expected -+++ Actual -@@ -2,3 +2,3 @@ - A: (string) (len=11) "some string", -- B: (int) 10 -+ B: (int) 15 - } -` + expected = "\n\nDiff:\n\x1b[32m--- Expected\n\x1b[0m\x1b[31m+++ Actual\n\x1b[0m@@ \x1b[32m-2,3\x1b[0m \x1b[31m+2,3\x1b[0m @@\n A: (string) (len=11) \"some string\",\n\x1b[32m- B: (int) 10\n\x1b[0m\x1b[31m+ B: (int) 15\n\x1b[0m }\n" actual = diff( diffTestingStruct{A: "some string", B: 10}, @@ -2067,16 +1999,7 @@ Diff: ) Equal(t, expected, actual) - expected = ` - -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 - -` + expected = "\n\nDiff:\n\x1b[32m--- Expected\n\x1b[0m\x1b[31m+++ Actual\n\x1b[0m@@ \x1b[32m-1,2\x1b[0m \x1b[31m+1,2\x1b[0m @@\n\x1b[32m-(time.Time) 2020-09-24 00:00:00 +0000 UTC\n\x1b[0m\x1b[31m+(time.Time) 2020-09-25 00:00:00 +0000 UTC\n\x1b[0m \n" actual = diff( time.Date(2020, 9, 24, 0, 0, 0, 0, time.UTC), @@ -2090,7 +2013,7 @@ func TestTimeEqualityErrorFormatting(t *testing.T) { Equal(mockT, time.Second*2, time.Millisecond) - expectedErr := "\\s+Error Trace:\\s+Error:\\s+Not equal:\\s+\n\\s+expected: 2s\n\\s+actual\\s+: 1ms\n" + expectedErr := `\n\tError Trace:\t\n\tError:\s+\t\x1b\[31;1mNot equal: \n\t\s+\t\x1b\[32mexpected: 2s\n\t\s+\t\x1b\[0m\x1b\[31mactual\s+: 1ms\x1b\[0m\x1b\[0m\n\tMessages:\s+\t\x1b\[33m\x1b\[0m\n` Regexp(t, regexp.MustCompile(expectedErr), mockT.errorString()) } diff --git a/assert/color.go b/assert/color.go new file mode 100644 index 000000000..4ea6e76d9 --- /dev/null +++ b/assert/color.go @@ -0,0 +1,46 @@ +package assert + +import "fmt" + +type color string + +const ( + resetCode color = "\033[0m" + redCode color = "\033[31m" + brightRedCode color = "\033[31;1m" + greenCode color = "\033[32m" + yellowCode color = "\033[33m" + blueCode color = "\033[34m" + + setFormat = "%s%s%s" +) + +// formatStr formats string by argument placing +func formatStr(f string, a ...interface{}) string { + return fmt.Sprintf(f, a...) +} + +// red prints text in red +func red(format string, args ...interface{}) string { + return fmt.Sprintf(setFormat, redCode, formatStr(format, args...), resetCode) +} + +// brightRed prints text in red, brighter and thicker +func brightRed(format string, args ...interface{}) string { + return fmt.Sprintf(setFormat, brightRedCode, formatStr(format, args...), resetCode) +} + +// green prints text in green +func green(format string, args ...interface{}) string { + return fmt.Sprintf(setFormat, greenCode, formatStr(format, args...), resetCode) +} + +// yellow prints text in yellow +func yellow(format string, args ...interface{}) string { + return fmt.Sprintf(setFormat, yellowCode, formatStr(format, args...), resetCode) +} + +// blue prints text in blue +func blue(format string, args ...interface{}) string { + return fmt.Sprintf(setFormat, blueCode, formatStr(format, args...), resetCode) +} diff --git a/assert/diff.go b/assert/diff.go new file mode 100644 index 000000000..f38672dd3 --- /dev/null +++ b/assert/diff.go @@ -0,0 +1,449 @@ +package assert + +import ( + "bufio" + "bytes" + "fmt" + "io" + "strings" +) + +// This code is embedded from https://github.com/pmezard/go-difflib/ with minor changes. +// TODO: Submit PR for original library (unmaintained) or maintain this code. + +type unifiedDiff struct { + A []string // First sequence lines + FromFile string // First file name + FromDate string // First file time + B []string // Second sequence lines + ToFile string // Second file name + ToDate string // Second file time + Eol string // Headers end of line, defaults to LF + Context int // Number of context lines +} + +type sequenceMatcher struct { + a []string + b []string + b2j map[string][]int + IsJunk func(string) bool + autoJunk bool + bJunk map[string]struct{} + matchingBlocks []match + fullBCount map[string]int + bPopular map[string]struct{} + opCodes []opCode +} + +type opCode struct { + Tag byte + I1 int + I2 int + J1 int + J2 int +} + +type match struct { + A int + B int + Size int +} + +func (m *sequenceMatcher) getGroupedOpCodes(n int) [][]opCode { + if n < 0 { + n = 3 + } + codes := m.getOpCodes() + if len(codes) == 0 { + codes = []opCode{opCode{'e', 0, 1, 0, 1}} + } + // Fixup leading and trailing groups if they show no changes. + if codes[0].Tag == 'e' { + c := codes[0] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[0] = opCode{c.Tag, max(i1, i2-n), i2, max(j1, j2-n), j2} + } + if codes[len(codes)-1].Tag == 'e' { + c := codes[len(codes)-1] + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + codes[len(codes)-1] = opCode{c.Tag, i1, min(i2, i1+n), j1, min(j2, j1+n)} + } + nn := n + n + groups := [][]opCode{} + group := []opCode{} + for _, c := range codes { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + // End the current group and start a new one whenever + // there is a large range with no changes. + if c.Tag == 'e' && i2-i1 > nn { + group = append(group, opCode{c.Tag, i1, min(i2, i1+n), + j1, min(j2, j1+n)}) + groups = append(groups, group) + group = []opCode{} + i1, j1 = max(i1, i2-n), max(j1, j2-n) + } + group = append(group, opCode{c.Tag, i1, i2, j1, j2}) + } + if len(group) > 0 && !(len(group) == 1 && group[0].Tag == 'e') { + groups = append(groups, group) + } + return groups +} + +func (m *sequenceMatcher) getOpCodes() []opCode { + if m.opCodes != nil { + return m.opCodes + } + i, j := 0, 0 + matching := m.getMatchingBlocks() + opCodes := make([]opCode, 0, len(matching)) + for _, m := range matching { + // invariant: we've pumped out correct diffs to change + // a[:i] into b[:j], and the next matching block is + // a[ai:ai+size] == b[bj:bj+size]. So we need to pump + // out a diff to change a[i:ai] into b[j:bj], pump out + // the matching block, and move (i,j) beyond the match + ai, bj, size := m.A, m.B, m.Size + tag := byte(0) + if i < ai && j < bj { + tag = 'r' + } else if i < ai { + tag = 'd' + } else if j < bj { + tag = 'i' + } + if tag > 0 { + opCodes = append(opCodes, opCode{tag, i, ai, j, bj}) + } + i, j = ai+size, bj+size + // the list of matching blocks is terminated by a + if size > 0 { + opCodes = append(opCodes, opCode{'e', ai, i, bj, j}) + } + } + m.opCodes = opCodes + return m.opCodes +} + +func (m *sequenceMatcher) getMatchingBlocks() []match { + if m.matchingBlocks != nil { + return m.matchingBlocks + } + + var matchBlocks func(alo, ahi, blo, bhi int, matched []match) []match + matchBlocks = func(alo, ahi, blo, bhi int, matched []match) []match { + match := m.findLongestMatch(alo, ahi, blo, bhi) + i, j, k := match.A, match.B, match.Size + if match.Size > 0 { + if alo < i && blo < j { + matched = matchBlocks(alo, i, blo, j, matched) + } + matched = append(matched, match) + if i+k < ahi && j+k < bhi { + matched = matchBlocks(i+k, ahi, j+k, bhi, matched) + } + } + return matched + } + matched := matchBlocks(0, len(m.a), 0, len(m.b), nil) + + // It's possible that we have adjacent equal blocks in the matching_blocks list now. + nonAdjacent := []match{} + i1, j1, k1 := 0, 0, 0 + for _, b := range matched { + // Is this block adjacent to i1, j1, k1? + i2, j2, k2 := b.A, b.B, b.Size + if i1+k1 == i2 && j1+k1 == j2 { + // Yes, so collapse them -- this just increases the length of + // the first block by the length of the second, and the first + // block so lengthened remains the block to compare against. + k1 += k2 + } else { + // Not adjacent. Remember the first block (k1==0 means it's + // the dummy we started with), and make the second block the + // new block to compare against. + if k1 > 0 { + nonAdjacent = append(nonAdjacent, match{i1, j1, k1}) + } + i1, j1, k1 = i2, j2, k2 + } + } + if k1 > 0 { + nonAdjacent = append(nonAdjacent, match{i1, j1, k1}) + } + + nonAdjacent = append(nonAdjacent, match{len(m.a), len(m.b), 0}) + m.matchingBlocks = nonAdjacent + return m.matchingBlocks +} + +func (m *sequenceMatcher) setSeqs(a, b []string) { + m.setSeq1(a) + m.setSeq2(b) +} + +func (m *sequenceMatcher) setSeq1(a []string) { + if &a == &m.a { + return + } + m.a = a + m.matchingBlocks = nil + m.opCodes = nil +} + +func (m *sequenceMatcher) setSeq2(b []string) { + if &b == &m.b { + return + } + m.b = b + m.matchingBlocks = nil + m.opCodes = nil + m.fullBCount = nil + m.chainB() +} + +func (m *sequenceMatcher) chainB() { + // Populate line -> index mapping + b2j := map[string][]int{} + for i, s := range m.b { + indices := b2j[s] + indices = append(indices, i) + b2j[s] = indices + } + + // Purge junk elements + m.bJunk = map[string]struct{}{} + if m.IsJunk != nil { + junk := m.bJunk + for s, _ := range b2j { + if m.IsJunk(s) { + junk[s] = struct{}{} + } + } + for s, _ := range junk { + delete(b2j, s) + } + } + + // Purge remaining popular elements + popular := map[string]struct{}{} + n := len(m.b) + if m.autoJunk && n >= 200 { + ntest := n/100 + 1 + for s, indices := range b2j { + if len(indices) > ntest { + popular[s] = struct{}{} + } + } + for s, _ := range popular { + delete(b2j, s) + } + } + m.bPopular = popular + m.b2j = b2j +} + +func (m *sequenceMatcher) findLongestMatch(alo, ahi, blo, bhi int) match { + // CAUTION: stripping common prefix or suffix would be incorrect. + // E.g., + // ab + // acab + // Longest matching block is "ab", but if common prefix is + // stripped, it's "a" (tied with "b"). UNIX(tm) diff does so + // strip, so ends up claiming that ab is changed to acab by + // inserting "ca" in the middle. That's minimal but unintuitive: + // "it's obvious" that someone inserted "ac" at the front. + // Windiff ends up at the same place as diff, but by pairing up + // the unique 'b's and then matching the first two 'a's. + besti, bestj, bestsize := alo, blo, 0 + + // find longest junk-free match + // during an iteration of the loop, j2len[j] = length of longest + // junk-free match ending with a[i-1] and b[j] + j2len := map[int]int{} + for i := alo; i != ahi; i++ { + // look at all instances of a[i] in b; note that because + // b2j has no junk keys, the loop is skipped if a[i] is junk + newj2len := map[int]int{} + for _, j := range m.b2j[m.a[i]] { + // a[i] matches b[j] + if j < blo { + continue + } + if j >= bhi { + break + } + k := j2len[j-1] + 1 + newj2len[j] = k + if k > bestsize { + besti, bestj, bestsize = i-k+1, j-k+1, k + } + } + j2len = newj2len + } + + // Extend the best by non-junk elements on each end. In particular, + // "popular" non-junk elements aren't in b2j, which greatly speeds + // the inner loop above, but also means "the best" match so far + // doesn't contain any junk *or* popular non-junk elements. + for besti > alo && bestj > blo && !m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + !m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + // Now that we have a wholly interesting match (albeit possibly + // empty!), we may as well suck up the matching junk on each + // side of it too. Can't think of a good reason not to, and it + // saves post-processing the (possibly considerable) expense of + // figuring out what to do with it. In the case of an empty + // interesting match, this is clearly the right thing to do, + // because no other kind of match is possible in the regions. + for besti > alo && bestj > blo && m.isBJunk(m.b[bestj-1]) && + m.a[besti-1] == m.b[bestj-1] { + besti, bestj, bestsize = besti-1, bestj-1, bestsize+1 + } + for besti+bestsize < ahi && bestj+bestsize < bhi && + m.isBJunk(m.b[bestj+bestsize]) && + m.a[besti+bestsize] == m.b[bestj+bestsize] { + bestsize += 1 + } + + return match{A: besti, B: bestj, Size: bestsize} +} + +func (m *sequenceMatcher) isBJunk(s string) bool { + _, ok := m.bJunk[s] + return ok +} + +func getUnifiedDiffString(diff unifiedDiff) (string, error) { + w := &bytes.Buffer{} + err := writeUnifiedDiff(w, diff) + return string(w.Bytes()), err +} + +func formatRangeUnified(start, stop int) string { + // Per the diff spec at http://www.unix.org/single_unix_specification/ + beginning := start + 1 // lines start numbering with one + length := stop - start + if length == 1 { + return fmt.Sprintf("%d", beginning) + } + if length == 0 { + beginning -= 1 // empty ranges begin at line just before the range + } + return fmt.Sprintf("%d,%d", beginning, length) +} + +func splitLines(s string) []string { + lines := strings.SplitAfter(s, "\n") + lines[len(lines)-1] += "\n" + return lines +} + +func writeUnifiedDiff(writer io.Writer, diff unifiedDiff) error { + buf := bufio.NewWriter(writer) + defer buf.Flush() + wf := func(format string, args ...interface{}) error { + _, err := buf.WriteString(fmt.Sprintf(format, args...)) + return err + } + ws := func(s string) error { + _, err := buf.WriteString(s) + return err + } + + if len(diff.Eol) == 0 { + diff.Eol = "\n" + } + + started := false + m := newMatcher(diff.A, diff.B) + for _, g := range m.getGroupedOpCodes(diff.Context) { + if !started { + started = true + fromDate := "" + if len(diff.FromDate) > 0 { + fromDate = "\t" + diff.FromDate + } + toDate := "" + if len(diff.ToDate) > 0 { + toDate = "\t" + diff.ToDate + } + if diff.FromFile != "" || diff.ToFile != "" { + expected := green("--- %s%s%s", diff.FromFile, fromDate, diff.Eol) + err := wf(expected) + if err != nil { + return err + } + + actual := red("+++ %s%s%s", diff.ToFile, toDate, diff.Eol) + err = wf(actual) + if err != nil { + return err + } + } + } + first, last := g[0], g[len(g)-1] + range1 := formatRangeUnified(first.I1, last.I2) + range2 := formatRangeUnified(first.J1, last.J2) + exp := green("-%s", range1) + act := red("+%s", range2) + if err := wf("@@ %s %s @@%s", exp, act, diff.Eol); err != nil { + return err + } + for _, c := range g { + i1, i2, j1, j2 := c.I1, c.I2, c.J1, c.J2 + if c.Tag == 'e' { + for _, line := range diff.A[i1:i2] { + if err := ws(" " + line); err != nil { + return err + } + } + continue + } + if c.Tag == 'r' || c.Tag == 'd' { + for _, line := range diff.A[i1:i2] { + l := green("-%s", line) + if err := ws(l); err != nil { + return err + } + } + } + if c.Tag == 'r' || c.Tag == 'i' { + for _, line := range diff.B[j1:j2] { + l := red("+%s", line) + if err := ws(l); err != nil { + return err + } + } + } + } + } + return nil +} + +func newMatcher(a, b []string) *sequenceMatcher { + m := sequenceMatcher{autoJunk: true} + m.setSeqs(a, b) + return &m +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +func max(a, b int) int { + if a > b { + return a + } + return b +}