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") }