353 lines
8.9 KiB
Go
353 lines
8.9 KiB
Go
|
package difflib
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"math"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
"testing"
|
||
|
)
|
||
|
|
||
|
func assertAlmostEqual(t *testing.T, a, b float64, places int) {
|
||
|
if math.Abs(a-b) > math.Pow10(-places) {
|
||
|
t.Errorf("%.7f != %.7f", a, b)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func assertEqual(t *testing.T, a, b interface{}) {
|
||
|
if !reflect.DeepEqual(a, b) {
|
||
|
t.Errorf("%v != %v", a, b)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func splitChars(s string) []string {
|
||
|
chars := make([]string, 0, len(s))
|
||
|
// Assume ASCII inputs
|
||
|
for i := 0; i != len(s); i++ {
|
||
|
chars = append(chars, string(s[i]))
|
||
|
}
|
||
|
return chars
|
||
|
}
|
||
|
|
||
|
func TestSequenceMatcherRatio(t *testing.T) {
|
||
|
s := NewMatcher(splitChars("abcd"), splitChars("bcde"))
|
||
|
assertEqual(t, s.Ratio(), 0.75)
|
||
|
assertEqual(t, s.QuickRatio(), 0.75)
|
||
|
assertEqual(t, s.RealQuickRatio(), 1.0)
|
||
|
}
|
||
|
|
||
|
func TestGetOptCodes(t *testing.T) {
|
||
|
a := "qabxcd"
|
||
|
b := "abycdf"
|
||
|
s := NewMatcher(splitChars(a), splitChars(b))
|
||
|
w := &bytes.Buffer{}
|
||
|
for _, op := range s.GetOpCodes() {
|
||
|
fmt.Fprintf(w, "%s a[%d:%d], (%s) b[%d:%d] (%s)\n", string(op.Tag),
|
||
|
op.I1, op.I2, a[op.I1:op.I2], op.J1, op.J2, b[op.J1:op.J2])
|
||
|
}
|
||
|
result := string(w.Bytes())
|
||
|
expected := `d a[0:1], (q) b[0:0] ()
|
||
|
e a[1:3], (ab) b[0:2] (ab)
|
||
|
r a[3:4], (x) b[2:3] (y)
|
||
|
e a[4:6], (cd) b[3:5] (cd)
|
||
|
i a[6:6], () b[5:6] (f)
|
||
|
`
|
||
|
if expected != result {
|
||
|
t.Errorf("unexpected op codes: \n%s", result)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func TestGroupedOpCodes(t *testing.T) {
|
||
|
a := []string{}
|
||
|
for i := 0; i != 39; i++ {
|
||
|
a = append(a, fmt.Sprintf("%02d", i))
|
||
|
}
|
||
|
b := []string{}
|
||
|
b = append(b, a[:8]...)
|
||
|
b = append(b, " i")
|
||
|
b = append(b, a[8:19]...)
|
||
|
b = append(b, " x")
|
||
|
b = append(b, a[20:22]...)
|
||
|
b = append(b, a[27:34]...)
|
||
|
b = append(b, " y")
|
||
|
b = append(b, a[35:]...)
|
||
|
s := NewMatcher(a, b)
|
||
|
w := &bytes.Buffer{}
|
||
|
for _, g := range s.GetGroupedOpCodes(-1) {
|
||
|
fmt.Fprintf(w, "group\n")
|
||
|
for _, op := range g {
|
||
|
fmt.Fprintf(w, " %s, %d, %d, %d, %d\n", string(op.Tag),
|
||
|
op.I1, op.I2, op.J1, op.J2)
|
||
|
}
|
||
|
}
|
||
|
result := string(w.Bytes())
|
||
|
expected := `group
|
||
|
e, 5, 8, 5, 8
|
||
|
i, 8, 8, 8, 9
|
||
|
e, 8, 11, 9, 12
|
||
|
group
|
||
|
e, 16, 19, 17, 20
|
||
|
r, 19, 20, 20, 21
|
||
|
e, 20, 22, 21, 23
|
||
|
d, 22, 27, 23, 23
|
||
|
e, 27, 30, 23, 26
|
||
|
group
|
||
|
e, 31, 34, 27, 30
|
||
|
r, 34, 35, 30, 31
|
||
|
e, 35, 38, 31, 34
|
||
|
`
|
||
|
if expected != result {
|
||
|
t.Errorf("unexpected op codes: \n%s", result)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func ExampleGetUnifiedDiffString() {
|
||
|
a := `one
|
||
|
two
|
||
|
three
|
||
|
four`
|
||
|
b := `zero
|
||
|
one
|
||
|
three
|
||
|
four`
|
||
|
diff := UnifiedDiff{
|
||
|
A: SplitLines(a),
|
||
|
B: SplitLines(b),
|
||
|
FromFile: "Original",
|
||
|
FromDate: "2005-01-26 23:30:50",
|
||
|
ToFile: "Current",
|
||
|
ToDate: "2010-04-02 10:20:52",
|
||
|
Context: 3,
|
||
|
}
|
||
|
result, _ := GetUnifiedDiffString(diff)
|
||
|
fmt.Printf(strings.Replace(result, "\t", " ", -1))
|
||
|
// Output:
|
||
|
// --- Original 2005-01-26 23:30:50
|
||
|
// +++ Current 2010-04-02 10:20:52
|
||
|
// @@ -1,4 +1,4 @@
|
||
|
// +zero
|
||
|
// one
|
||
|
// -two
|
||
|
// three
|
||
|
// four
|
||
|
}
|
||
|
|
||
|
func ExampleGetContextDiffString() {
|
||
|
a := `one
|
||
|
two
|
||
|
three
|
||
|
four`
|
||
|
b := `zero
|
||
|
one
|
||
|
tree
|
||
|
four`
|
||
|
diff := ContextDiff{
|
||
|
A: SplitLines(a),
|
||
|
B: SplitLines(b),
|
||
|
FromFile: "Original",
|
||
|
ToFile: "Current",
|
||
|
Context: 3,
|
||
|
Eol: "\n",
|
||
|
}
|
||
|
result, _ := GetContextDiffString(diff)
|
||
|
fmt.Printf(strings.Replace(result, "\t", " ", -1))
|
||
|
// Output:
|
||
|
// *** Original
|
||
|
// --- Current
|
||
|
// ***************
|
||
|
// *** 1,4 ****
|
||
|
// one
|
||
|
// ! two
|
||
|
// ! three
|
||
|
// four
|
||
|
// --- 1,4 ----
|
||
|
// + zero
|
||
|
// one
|
||
|
// ! tree
|
||
|
// four
|
||
|
}
|
||
|
|
||
|
func rep(s string, count int) string {
|
||
|
return strings.Repeat(s, count)
|
||
|
}
|
||
|
|
||
|
func TestWithAsciiOneInsert(t *testing.T) {
|
||
|
sm := NewMatcher(splitChars(rep("b", 100)),
|
||
|
splitChars("a"+rep("b", 100)))
|
||
|
assertAlmostEqual(t, sm.Ratio(), 0.995, 3)
|
||
|
assertEqual(t, sm.GetOpCodes(),
|
||
|
[]OpCode{{'i', 0, 0, 0, 1}, {'e', 0, 100, 1, 101}})
|
||
|
assertEqual(t, len(sm.bPopular), 0)
|
||
|
|
||
|
sm = NewMatcher(splitChars(rep("b", 100)),
|
||
|
splitChars(rep("b", 50)+"a"+rep("b", 50)))
|
||
|
assertAlmostEqual(t, sm.Ratio(), 0.995, 3)
|
||
|
assertEqual(t, sm.GetOpCodes(),
|
||
|
[]OpCode{{'e', 0, 50, 0, 50}, {'i', 50, 50, 50, 51}, {'e', 50, 100, 51, 101}})
|
||
|
assertEqual(t, len(sm.bPopular), 0)
|
||
|
}
|
||
|
|
||
|
func TestWithAsciiOnDelete(t *testing.T) {
|
||
|
sm := NewMatcher(splitChars(rep("a", 40)+"c"+rep("b", 40)),
|
||
|
splitChars(rep("a", 40)+rep("b", 40)))
|
||
|
assertAlmostEqual(t, sm.Ratio(), 0.994, 3)
|
||
|
assertEqual(t, sm.GetOpCodes(),
|
||
|
[]OpCode{{'e', 0, 40, 0, 40}, {'d', 40, 41, 40, 40}, {'e', 41, 81, 40, 80}})
|
||
|
}
|
||
|
|
||
|
func TestWithAsciiBJunk(t *testing.T) {
|
||
|
isJunk := func(s string) bool {
|
||
|
return s == " "
|
||
|
}
|
||
|
sm := NewMatcherWithJunk(splitChars(rep("a", 40)+rep("b", 40)),
|
||
|
splitChars(rep("a", 44)+rep("b", 40)), true, isJunk)
|
||
|
assertEqual(t, sm.bJunk, map[string]struct{}{})
|
||
|
|
||
|
sm = NewMatcherWithJunk(splitChars(rep("a", 40)+rep("b", 40)),
|
||
|
splitChars(rep("a", 44)+rep("b", 40)+rep(" ", 20)), false, isJunk)
|
||
|
assertEqual(t, sm.bJunk, map[string]struct{}{" ": struct{}{}})
|
||
|
|
||
|
isJunk = func(s string) bool {
|
||
|
return s == " " || s == "b"
|
||
|
}
|
||
|
sm = NewMatcherWithJunk(splitChars(rep("a", 40)+rep("b", 40)),
|
||
|
splitChars(rep("a", 44)+rep("b", 40)+rep(" ", 20)), false, isJunk)
|
||
|
assertEqual(t, sm.bJunk, map[string]struct{}{" ": struct{}{}, "b": struct{}{}})
|
||
|
}
|
||
|
|
||
|
func TestSFBugsRatioForNullSeqn(t *testing.T) {
|
||
|
sm := NewMatcher(nil, nil)
|
||
|
assertEqual(t, sm.Ratio(), 1.0)
|
||
|
assertEqual(t, sm.QuickRatio(), 1.0)
|
||
|
assertEqual(t, sm.RealQuickRatio(), 1.0)
|
||
|
}
|
||
|
|
||
|
func TestSFBugsComparingEmptyLists(t *testing.T) {
|
||
|
groups := NewMatcher(nil, nil).GetGroupedOpCodes(-1)
|
||
|
assertEqual(t, len(groups), 0)
|
||
|
diff := UnifiedDiff{
|
||
|
FromFile: "Original",
|
||
|
ToFile: "Current",
|
||
|
Context: 3,
|
||
|
}
|
||
|
result, err := GetUnifiedDiffString(diff)
|
||
|
assertEqual(t, err, nil)
|
||
|
assertEqual(t, result, "")
|
||
|
}
|
||
|
|
||
|
func TestOutputFormatRangeFormatUnified(t *testing.T) {
|
||
|
// Per the diff spec at http://www.unix.org/single_unix_specification/
|
||
|
//
|
||
|
// Each <range> field shall be of the form:
|
||
|
// %1d", <beginning line number> if the range contains exactly one line,
|
||
|
// and:
|
||
|
// "%1d,%1d", <beginning line number>, <number of lines> otherwise.
|
||
|
// If a range is empty, its beginning line number shall be the number of
|
||
|
// the line just before the range, or 0 if the empty range starts the file.
|
||
|
fm := formatRangeUnified
|
||
|
assertEqual(t, fm(3, 3), "3,0")
|
||
|
assertEqual(t, fm(3, 4), "4")
|
||
|
assertEqual(t, fm(3, 5), "4,2")
|
||
|
assertEqual(t, fm(3, 6), "4,3")
|
||
|
assertEqual(t, fm(0, 0), "0,0")
|
||
|
}
|
||
|
|
||
|
func TestOutputFormatRangeFormatContext(t *testing.T) {
|
||
|
// Per the diff spec at http://www.unix.org/single_unix_specification/
|
||
|
//
|
||
|
// The range of lines in file1 shall be written in the following format
|
||
|
// if the range contains two or more lines:
|
||
|
// "*** %d,%d ****\n", <beginning line number>, <ending line number>
|
||
|
// and the following format otherwise:
|
||
|
// "*** %d ****\n", <ending line number>
|
||
|
// The ending line number of an empty range shall be the number of the preceding line,
|
||
|
// or 0 if the range is at the start of the file.
|
||
|
//
|
||
|
// Next, the range of lines in file2 shall be written in the following format
|
||
|
// if the range contains two or more lines:
|
||
|
// "--- %d,%d ----\n", <beginning line number>, <ending line number>
|
||
|
// and the following format otherwise:
|
||
|
// "--- %d ----\n", <ending line number>
|
||
|
fm := formatRangeContext
|
||
|
assertEqual(t, fm(3, 3), "3")
|
||
|
assertEqual(t, fm(3, 4), "4")
|
||
|
assertEqual(t, fm(3, 5), "4,5")
|
||
|
assertEqual(t, fm(3, 6), "4,6")
|
||
|
assertEqual(t, fm(0, 0), "0")
|
||
|
}
|
||
|
|
||
|
func TestOutputFormatTabDelimiter(t *testing.T) {
|
||
|
diff := UnifiedDiff{
|
||
|
A: splitChars("one"),
|
||
|
B: splitChars("two"),
|
||
|
FromFile: "Original",
|
||
|
FromDate: "2005-01-26 23:30:50",
|
||
|
ToFile: "Current",
|
||
|
ToDate: "2010-04-12 10:20:52",
|
||
|
Eol: "\n",
|
||
|
}
|
||
|
ud, err := GetUnifiedDiffString(diff)
|
||
|
assertEqual(t, err, nil)
|
||
|
assertEqual(t, SplitLines(ud)[:2], []string{
|
||
|
"--- Original\t2005-01-26 23:30:50\n",
|
||
|
"+++ Current\t2010-04-12 10:20:52\n",
|
||
|
})
|
||
|
cd, err := GetContextDiffString(ContextDiff(diff))
|
||
|
assertEqual(t, err, nil)
|
||
|
assertEqual(t, SplitLines(cd)[:2], []string{
|
||
|
"*** Original\t2005-01-26 23:30:50\n",
|
||
|
"--- Current\t2010-04-12 10:20:52\n",
|
||
|
})
|
||
|
}
|
||
|
|
||
|
func TestOutputFormatNoTrailingTabOnEmptyFiledate(t *testing.T) {
|
||
|
diff := UnifiedDiff{
|
||
|
A: splitChars("one"),
|
||
|
B: splitChars("two"),
|
||
|
FromFile: "Original",
|
||
|
ToFile: "Current",
|
||
|
Eol: "\n",
|
||
|
}
|
||
|
ud, err := GetUnifiedDiffString(diff)
|
||
|
assertEqual(t, err, nil)
|
||
|
assertEqual(t, SplitLines(ud)[:2], []string{"--- Original\n", "+++ Current\n"})
|
||
|
|
||
|
cd, err := GetContextDiffString(ContextDiff(diff))
|
||
|
assertEqual(t, err, nil)
|
||
|
assertEqual(t, SplitLines(cd)[:2], []string{"*** Original\n", "--- Current\n"})
|
||
|
}
|
||
|
|
||
|
func TestSplitLines(t *testing.T) {
|
||
|
allTests := []struct {
|
||
|
input string
|
||
|
want []string
|
||
|
}{
|
||
|
{"foo", []string{"foo\n"}},
|
||
|
{"foo\nbar", []string{"foo\n", "bar\n"}},
|
||
|
{"foo\nbar\n", []string{"foo\n", "bar\n", "\n"}},
|
||
|
}
|
||
|
for _, test := range allTests {
|
||
|
assertEqual(t, SplitLines(test.input), test.want)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func benchmarkSplitLines(b *testing.B, count int) {
|
||
|
str := strings.Repeat("foo\n", count)
|
||
|
|
||
|
b.ResetTimer()
|
||
|
|
||
|
n := 0
|
||
|
for i := 0; i < b.N; i++ {
|
||
|
n += len(SplitLines(str))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func BenchmarkSplitLines100(b *testing.B) {
|
||
|
benchmarkSplitLines(b, 100)
|
||
|
}
|
||
|
|
||
|
func BenchmarkSplitLines10000(b *testing.B) {
|
||
|
benchmarkSplitLines(b, 10000)
|
||
|
}
|