package merkletrie_test
import (
"bytes"
ctx "context"
"fmt"
"reflect"
"sort"
"strings"
"testing"
"unicode"
"github.com/go-git/go-git/v5/utils/merkletrie"
"github.com/go-git/go-git/v5/utils/merkletrie/internal/fsnoder"
. "gopkg.in/check.v1"
)
func Test(t *testing.T) { TestingT(t) }
type DiffTreeSuite struct{}
var _ = Suite(&DiffTreeSuite{})
type diffTreeTest struct {
from string
to string
expected string
}
func (t diffTreeTest) innerRun(c *C, context string, reverse bool) {
comment := Commentf("\n%s", context)
if reverse {
comment = Commentf("%s [REVERSED]", comment.CheckCommentString())
}
a, err := fsnoder.New(t.from)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\t from = %s", comment.CheckCommentString(), a)
b, err := fsnoder.New(t.to)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\t to = %s", comment.CheckCommentString(), b)
expected, err := newChangesFromString(t.expected)
c.Assert(err, IsNil, comment)
if reverse {
a, b = b, a
expected = expected.reverse()
}
comment = Commentf("%s\n\texpected = %s", comment.CheckCommentString(), expected)
results, err := merkletrie.DiffTree(a, b, fsnoder.HashEqual)
c.Assert(err, IsNil, comment)
obtained, err := newChanges(results)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\tobtained = %s", comment.CheckCommentString(), obtained)
c.Assert(obtained, changesEquals, expected, comment)
}
func (t diffTreeTest) innerRunCtx(c *C, context string, reverse bool) {
comment := Commentf("\n%s", context)
if reverse {
comment = Commentf("%s [REVERSED]", comment.CheckCommentString())
}
a, err := fsnoder.New(t.from)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\t from = %s", comment.CheckCommentString(), a)
b, err := fsnoder.New(t.to)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\t to = %s", comment.CheckCommentString(), b)
expected, err := newChangesFromString(t.expected)
c.Assert(err, IsNil, comment)
if reverse {
a, b = b, a
expected = expected.reverse()
}
comment = Commentf("%s\n\texpected = %s", comment.CheckCommentString(), expected)
results, err := merkletrie.DiffTreeContext(ctx.Background(), a, b, fsnoder.HashEqual)
c.Assert(err, IsNil, comment)
obtained, err := newChanges(results)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\tobtained = %s", comment.CheckCommentString(), obtained)
c.Assert(obtained, changesEquals, expected, comment)
}
func (t diffTreeTest) run(c *C, context string) {
t.innerRun(c, context, false)
t.innerRun(c, context, true)
t.innerRunCtx(c, context, false)
t.innerRunCtx(c, context, true)
}
type change struct {
merkletrie.Action
path string
}
func (c change) String() string {
return fmt.Sprintf("<%s %s>", c.Action, c.path)
}
func (c change) reverse() change {
ret := change{
path: c.path,
}
switch c.Action {
case merkletrie.Insert:
ret.Action = merkletrie.Delete
case merkletrie.Delete:
ret.Action = merkletrie.Insert
case merkletrie.Modify:
ret.Action = merkletrie.Modify
default:
panic(fmt.Sprintf("unknown action type: %d", c.Action))
}
return ret
}
type changes []change
func newChanges(original merkletrie.Changes) (changes, error) {
ret := make(changes, len(original))
for i, c := range original {
action, err := c.Action()
if err != nil {
return nil, err
}
switch action {
case merkletrie.Insert:
ret[i] = change{
Action: merkletrie.Insert,
path: c.To.String(),
}
case merkletrie.Delete:
ret[i] = change{
Action: merkletrie.Delete,
path: c.From.String(),
}
case merkletrie.Modify:
ret[i] = change{
Action: merkletrie.Modify,
path: c.From.String(),
}
default:
panic(fmt.Sprintf("unsupported action %d", action))
}
}
return ret, nil
}
func newChangesFromString(s string) (changes, error) {
ret := make([]change, 0)
s = strings.TrimSpace(s)
s = removeDuplicatedSpace(s)
s = turnSpaceIntoLiteralSpace(s)
if s == "" {
return ret, nil
}
for _, chunk := range strings.Split(s, " ") {
change := change{
path: chunk[1:],
}
switch chunk[0] {
case '+':
change.Action = merkletrie.Insert
case '-':
change.Action = merkletrie.Delete
case '*':
change.Action = merkletrie.Modify
default:
panic(fmt.Sprintf("unsupported action descriptor %q", chunk[0]))
}
ret = append(ret, change)
}
return ret, nil
}
func removeDuplicatedSpace(s string) string {
var buf bytes.Buffer
var lastWasSpace, currentIsSpace bool
for _, r := range s {
currentIsSpace = unicode.IsSpace(r)
if lastWasSpace && currentIsSpace {
continue
}
lastWasSpace = currentIsSpace
buf.WriteRune(r)
}
return buf.String()
}
func turnSpaceIntoLiteralSpace(s string) string {
return strings.Map(
func(r rune) rune {
if unicode.IsSpace(r) {
return ' '
}
return r
}, s)
}
func (cc changes) Len() int { return len(cc) }
func (cc changes) Swap(i, j int) { cc[i], cc[j] = cc[j], cc[i] }
func (cc changes) Less(i, j int) bool { return strings.Compare(cc[i].String(), cc[j].String()) < 0 }
func (cc changes) equals(other changes) bool {
sort.Sort(cc)
sort.Sort(other)
return reflect.DeepEqual(cc, other)
}
func (cc changes) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "len(%d) [", len(cc))
sep := ""
for _, c := range cc {
fmt.Fprintf(&buf, "%s%s", sep, c)
sep = ", "
}
buf.WriteByte(']')
return buf.String()
}
func (cc changes) reverse() changes {
ret := make(changes, len(cc))
for i, c := range cc {
ret[i] = c.reverse()
}
return ret
}
type changesEqualsChecker struct {
*CheckerInfo
}
var changesEquals Checker = &changesEqualsChecker{
&CheckerInfo{Name: "changesEquals", Params: []string{"obtained", "expected"}},
}
func (checker *changesEqualsChecker) Check(params []interface{}, names []string) (result bool, error string) {
a, ok := params[0].(changes)
if !ok {
return false, "first parameter must be a changes"
}
b, ok := params[1].(changes)
if !ok {
return false, "second parameter must be a changes"
}
return a.equals(b), ""
}
func do(c *C, list []diffTreeTest) {
for i, t := range list {
t.run(c, fmt.Sprintf("test #%d:", i))
}
}
func (s *DiffTreeSuite) TestEmptyVsEmpty(c *C) {
do(c, []diffTreeTest{
{"()", "()", ""},
{"A()", "A()", ""},
{"A()", "()", ""},
{"A()", "B()", ""},
})
}
func (s *DiffTreeSuite) TestBasicCases(c *C) {
do(c, []diffTreeTest{
{"()", "()", ""},
{"()", "(a<>)", "+a"},
{"()", "(a<1>)", "+a"},
{"()", "(a())", ""},
{"()", "(a(b()))", ""},
{"()", "(a(b<>))", "+a/b"},
{"()", "(a(b<1>))", "+a/b"},
{"(a<>)", "(a<>)", ""},
{"(a<>)", "(a<1>)", "*a"},
{"(a<>)", "(a())", "-a"},
{"(a<>)", "(a(b()))", "-a"},
{"(a<>)", "(a(b<>))", "-a +a/b"},
{"(a<>)", "(a(b<1>))", "-a +a/b"},
{"(a<>)", "(c())", "-a"},
{"(a<>)", "(c(b()))", "-a"},
{"(a<>)", "(c(b<>))", "-a +c/b"},
{"(a<>)", "(c(b<1>))", "-a +c/b"},
{"(a<>)", "(c(a()))", "-a"},
{"(a<>)", "(c(a<>))", "-a +c/a"},
{"(a<>)", "(c(a<1>))", "-a +c/a"},
{"(a<1>)", "(a<1>)", ""},
{"(a<1>)", "(a<2>)", "*a"},
{"(a<1>)", "(b<1>)", "-a +b"},
{"(a<1>)", "(b<2>)", "-a +b"},
{"(a<1>)", "(a())", "-a"},
{"(a<1>)", "(a(b()))", "-a"},
{"(a<1>)", "(a(b<>))", "-a +a/b"},
{"(a<1>)", "(a(b<1>))", "-a +a/b"},
{"(a<1>)", "(a(b<2>))", "-a +a/b"},
{"(a<1>)", "(c())", "-a"},
{"(a<1>)", "(c(b()))", "-a"},
{"(a<1>)", "(c(b<>))", "-a +c/b"},
{"(a<1>)", "(c(b<1>))", "-a +c/b"},
{"(a<1>)", "(c(b<2>))", "-a +c/b"},
{"(a<1>)", "(c())", "-a"},
{"(a<1>)", "(c(a()))", "-a"},
{"(a<1>)", "(c(a<>))", "-a +c/a"},
{"(a<1>)", "(c(a<1>))", "-a +c/a"},
{"(a<1>)", "(c(a<2>))", "-a +c/a"},
{"(a())", "(a())", ""},
{"(a())", "(b())", ""},
{"(a())", "(a(b()))", ""},
{"(a())", "(b(a()))", ""},
{"(a())", "(a(b<>))", "+a/b"},
{"(a())", "(a(b<1>))", "+a/b"},
{"(a())", "(b(a<>))", "+b/a"},
{"(a())", "(b(a<1>))", "+b/a"},
})
}
func (s *DiffTreeSuite) TestHorizontals(c *C) {
do(c, []diffTreeTest{
{"()", "(a<> b<>)", "+a +b"},
{"()", "(a<> b<1>)", "+a +b"},
{"()", "(a<> b())", "+a"},
{"()", "(a() b<>)", "+b"},
{"()", "(a<1> b<>)", "+a +b"},
{"()", "(a<1> b<1>)", "+a +b"},
{"()", "(a<1> b<2>)", "+a +b"},
{"()", "(a<1> b())", "+a"},
{"()", "(a() b<1>)", "+b"},
{"()", "(a() b())", ""},
{"()", "(a<> b<> c<> d<>)", "+a +b +c +d"},
{"()", "(a<> b<1> c() d<> e<2> f())", "+a +b +d +e"},
})
}
func (s *DiffTreeSuite) TestVerticals(c *C) {
do(c, []diffTreeTest{
{"()", "(z<>)", "+z"},
{"()", "(a(z<>))", "+a/z"},
{"()", "(a(b(z<>)))", "+a/b/z"},
{"()", "(a(b(c(z<>))))", "+a/b/c/z"},
{"()", "(a(b(c(d(z<>)))))", "+a/b/c/d/z"},
{"()", "(a(b(c(d(z<1>)))))", "+a/b/c/d/z"},
})
}
func (s *DiffTreeSuite) TestSingleInserts(c *C) {
do(c, []diffTreeTest{
{"()", "(z<>)", "+z"},
{"(a())", "(a(z<>))", "+a/z"},
{"(a())", "(a(b(z<>)))", "+a/b/z"},
{"(a(b(c())))", "(a(b(c(z<>))))", "+a/b/c/z"},
{"(a<> b<> c<>)", "(a<> b<> c<> z<>)", "+z"},
{"(a(b<> c<> d<>))", "(a(b<> c<> d<> z<>))", "+a/z"},
{"(a(b(c<> d<> e<>)))", "(a(b(c<> d<> e<> z<>)))", "+a/b/z"},
{"(a(b<>) f<>)", "(a(b<>) f<> z<>)", "+z"},
{"(a(b<>) f<>)", "(a(b<> z<>) f<>)", "+a/z"},
})
}
func (s *DiffTreeSuite) TestDebug(c *C) {
do(c, []diffTreeTest{
{"(a(b<>) f<>)", "(a(b<> z<>) f<>)", "+a/z"},
})
}
// root
// / | \
// / | ----
// f d h --------
// /\ / \ |
// e a j b/ g
// | / \ |
// l n k icm
// |
// o
// |
// p/
func (s *DiffTreeSuite) TestCrazy(c *C) {
crazy := "(f(e(l<1>) a(n(o(p())) k<1>)) d<1> h(j(i<1> c<2> m<>) b() g<>))"
do(c, []diffTreeTest{
{
crazy,
"()",
"-d -f/e/l -f/a/k -h/j/i -h/j/c -h/j/m -h/g",
}, {
crazy,
crazy,
"",
}, {
crazy,
"(d<1>)",
"-f/e/l -f/a/k -h/j/i -h/j/c -h/j/m -h/g",
}, {
crazy,
"(d<1> h(b() g<>))",
"-f/e/l -f/a/k -h/j/i -h/j/c -h/j/m",
}, {
crazy,
"(d<1> f(e(l()) a()) h(b() g<>))",
"-f/e/l -f/a/k -h/j/i -h/j/c -h/j/m",
}, {
crazy,
"(d<1> f(e(l<1>) a()) h(b() g<>))",
"-f/a/k -h/j/i -h/j/c -h/j/m",
}, {
crazy,
"(d<2> f(e(l<2>) a(s(t<1>))) h(b() g<> r<> j(i<> c<3> m<>)))",
"+f/a/s/t +h/r -f/a/k *d *f/e/l *h/j/c *h/j/i",
}, {
crazy,
"(f(e(l<2>) a(n(o(p<1>)) k<>)) h(j(i<1> c<2> m<>) b() g<>))",
"*f/e/l +f/a/n/o/p *f/a/k -d",
}, {
crazy,
"(f(e(l<1>) a(n(o(p(r<1>))) k<1>)) d<1> h(j(i<1> c<2> b() m<>) g<1>))",
"+f/a/n/o/p/r *h/g",
},
})
}
func (s *DiffTreeSuite) TestSameNames(c *C) {
do(c, []diffTreeTest{
{
"(a(a(a<>)))",
"(a(a(a<1>)))",
"*a/a/a",
}, {
"(a(b(a<>)))",
"(a(b(a<>)) b(a<>))",
"+b/a",
}, {
"(a(b(a<>)))",
"(a(b()) b(a<>))",
"-a/b/a +b/a",
},
})
}
func (s *DiffTreeSuite) TestIssue275(c *C) {
do(c, []diffTreeTest{
{
"(a(b(c.go<1>) b.go<2>))",
"(a(b(c.go<1> d.go<3>) b.go<2>))",
"+a/b/d.go",
},
})
}
func (s *DiffTreeSuite) TestIssue1057(c *C) {
p1 := "TestAppWithUnicodéPath"
p2 := "TestAppWithUnicodéPath"
c.Assert(p1 == p2, Equals, false)
do(c, []diffTreeTest{
{
fmt.Sprintf("(%s(x.go<1>))", p1),
fmt.Sprintf("(%s(x.go<1>) %s(x.go<1>))", p1, p2),
fmt.Sprintf("+%s/x.go", p2),
},
})
// swap p1 with p2
do(c, []diffTreeTest{
{
fmt.Sprintf("(%s(x.go<1>))", p2),
fmt.Sprintf("(%s(x.go<1>) %s(x.go<1>))", p1, p2),
fmt.Sprintf("+%s/x.go", p1),
},
})
}
func (s *DiffTreeSuite) TestCancel(c *C) {
t := diffTreeTest{"()", "(a<> b<1> c() d<> e<2> f())", "+a +b +d +e"}
comment := Commentf("\n%s", "test cancel:")
a, err := fsnoder.New(t.from)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\t from = %s", comment.CheckCommentString(), a)
b, err := fsnoder.New(t.to)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\t to = %s", comment.CheckCommentString(), b)
expected, err := newChangesFromString(t.expected)
c.Assert(err, IsNil, comment)
comment = Commentf("%s\n\texpected = %s", comment.CheckCommentString(), expected)
context, cancel := ctx.WithCancel(ctx.Background())
cancel()
results, err := merkletrie.DiffTreeContext(context, a, b, fsnoder.HashEqual)
c.Assert(results, IsNil, comment)
c.Assert(err, ErrorMatches, "operation canceled")
}