From df73a4c6f1b58075316ba7449fbfb127b9fbb79d Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sun, 29 Mar 2026 14:42:13 +0000 Subject: commitquery: Reorganize --- commitquery/queries_merge_bases_unit_test.go | 332 +++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 commitquery/queries_merge_bases_unit_test.go (limited to 'commitquery/queries_merge_bases_unit_test.go') diff --git a/commitquery/queries_merge_bases_unit_test.go b/commitquery/queries_merge_bases_unit_test.go new file mode 100644 index 00000000..4db4f548 --- /dev/null +++ b/commitquery/queries_merge_bases_unit_test.go @@ -0,0 +1,332 @@ +package commitquery_test + +import ( + "errors" + "fmt" + "maps" + "slices" + "testing" + + "codeberg.org/lindenii/furgit/commitquery" + giterrors "codeberg.org/lindenii/furgit/errors" + "codeberg.org/lindenii/furgit/internal/testgit" + objectid "codeberg.org/lindenii/furgit/object/id" + "codeberg.org/lindenii/furgit/object/store/memory" + "codeberg.org/lindenii/furgit/object/tree" + objecttype "codeberg.org/lindenii/furgit/object/type" +) + +// commitBody serializes one minimal commit body. +func commitBody(tree objectid.ObjectID, parents ...objectid.ObjectID) []byte { + buf := fmt.Appendf(nil, "tree %s\n", tree.String()) + for _, parent := range parents { + buf = append(buf, fmt.Appendf(nil, "parent %s\n", parent.String())...) + } + + buf = append(buf, []byte("\nmsg\n")...) + + return buf +} + +// tagBody serializes one minimal annotated tag body. +func tagBody(target objectid.ObjectID, targetType objecttype.Type) []byte { + targetName, ok := targetType.Name() + if !ok { + panic("invalid tag target type") + } + + return fmt.Appendf(nil, "object %s\ntype %s\ntag t\n\nmsg\n", target.String(), targetName) +} + +// toSet converts one slice of object IDs into a set. +func toSet(ids []objectid.ObjectID) map[objectid.ObjectID]struct{} { + set := make(map[objectid.ObjectID]struct{}, len(ids)) + for _, id := range ids { + set[id] = struct{}{} + } + + return set +} + +// containsID reports whether one set contains one object ID. +func containsID(set map[objectid.ObjectID]struct{}, id objectid.ObjectID) bool { + _, ok := set[id] + + return ok +} + +// mustSerializeTree serializes one tree or fails the test. +func mustSerializeTree(tb testing.TB, tree *tree.Tree) []byte { + tb.Helper() + + body, err := tree.SerializeWithoutHeader() + if err != nil { + tb.Fatalf("SerializeWithoutHeader: %v", err) + } + + return body +} + +// TestQueryLinearHistory reports one linear-history merge base. +func TestQueryLinearHistory(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := memory.New(algo) + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("f"), + ID: blob, + }}})) + base := store.AddObject(objecttype.TypeCommit, commitBody(tree)) + left := store.AddObject(objecttype.TypeCommit, commitBody(tree, base)) + right := store.AddObject(objecttype.TypeCommit, commitBody(tree, left)) + + query := commitquery.New(store, nil) + + got, err := query.MergeBases(left, right) + if err != nil { + t.Fatalf("query.All(): %v", err) + } + + if !slices.Equal(got, []objectid.ObjectID{left}) { + t.Fatalf("Query(left, right)=%v, want [%s]", got, left) + } + + first, ok, err := query.MergeBase(left, right) + if err != nil { + t.Fatalf("Base(left, right): %v", err) + } + + if !ok { + t.Fatal("Base(left, right) unexpectedly reported no base") + } + + if first != left { + t.Fatalf("Base(left, right)=%s, want %s", first, left) + } + }) +} + +func TestQueryPeelsAnnotatedTags(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := memory.New(algo) + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) + leftTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("left"), + ID: blob, + }}})) + rightTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("right"), + ID: blob, + }}})) + base := store.AddObject(objecttype.TypeCommit, commitBody(leftTree)) + left := store.AddObject(objecttype.TypeCommit, commitBody(leftTree, base)) + right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree, base)) + tag := store.AddObject(objecttype.TypeTag, tagBody(right, objecttype.TypeCommit)) + + query := commitquery.New(store, nil) + + got, err := query.MergeBases(left, tag) + if err != nil { + t.Fatalf("query.All(): %v", err) + } + + if !slices.Equal(got, []objectid.ObjectID{base}) { + t.Fatalf("Query(left, tag)=%v, want [%s]", got, base) + } + }) +} + +func TestQueryCrissCrossReturnsAllBestCommonAncestors(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := memory.New(algo) + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) + rootTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("root"), + ID: blob, + }}})) + base1Tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("base1"), + ID: blob, + }}})) + base2Tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("base2"), + ID: blob, + }}})) + leftTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("left"), + ID: blob, + }}})) + rightTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("right"), + ID: blob, + }}})) + root := store.AddObject(objecttype.TypeCommit, commitBody(rootTree)) + base1 := store.AddObject(objecttype.TypeCommit, commitBody(base1Tree, root)) + base2 := store.AddObject(objecttype.TypeCommit, commitBody(base2Tree, root)) + left := store.AddObject(objecttype.TypeCommit, commitBody(leftTree, base1, base2)) + right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree, base2, base1)) + + query := commitquery.New(store, nil) + + all, err := query.MergeBases(left, right) + if err != nil { + t.Fatalf("query.All(): %v", err) + } + + got := toSet(all) + + want := map[objectid.ObjectID]struct{}{base1: {}, base2: {}} + if !maps.Equal(got, want) { + t.Fatalf("Query(left, right)=%v, want %v", slices.Collect(maps.Keys(got)), slices.Collect(maps.Keys(want))) + } + + first, ok, err := query.MergeBase(left, right) + if err != nil { + t.Fatalf("Base(left, right): %v", err) + } + + if !ok { + t.Fatal("Base(left, right) unexpectedly reported no base") + } + + if !containsID(want, first) { + t.Fatalf("Base(left, right)=%s, want one of %v", first, slices.Collect(maps.Keys(want))) + } + }) +} + +func TestQueryReturnsNoResultWhenNoCommonAncestorExists(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := memory.New(algo) + leftBlob := store.AddObject(objecttype.TypeBlob, []byte("left\n")) + leftTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("left"), + ID: leftBlob, + }}})) + rightBlob := store.AddObject(objecttype.TypeBlob, []byte("right\n")) + rightTree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("right"), + ID: rightBlob, + }}})) + left := store.AddObject(objecttype.TypeCommit, commitBody(leftTree)) + right := store.AddObject(objecttype.TypeCommit, commitBody(rightTree)) + + query := commitquery.New(store, nil) + + got, err := query.MergeBases(left, right) + if err != nil { + t.Fatalf("query.All(): %v", err) + } + + if len(got) != 0 { + t.Fatalf("Query(left, right)=%v, want no results", got) + } + + _, ok, err := query.MergeBase(left, right) + if err != nil { + t.Fatalf("Base(left, right): %v", err) + } + + if ok { + t.Fatal("Base(left, right) unexpectedly reported a base") + } + }) +} + +func TestQueryRejectsNonCommitAfterPeel(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := memory.New(algo) + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("f"), + ID: blob, + }}})) + commit := store.AddObject(objecttype.TypeCommit, commitBody(tree)) + tagToTree := store.AddObject(objecttype.TypeTag, tagBody(tree, objecttype.TypeTree)) + + query := commitquery.New(store, nil) + + _, err := query.MergeBases(commit, tagToTree) + if err == nil { + t.Fatal("expected error") + } + + typeErr, ok := errors.AsType[*giterrors.ObjectTypeError](err) + if !ok { + t.Fatalf("expected ObjectTypeError, got %T (%v)", err, err) + } + + if typeErr.Got != objecttype.TypeTree || typeErr.Want != objecttype.TypeCommit { + t.Fatalf("unexpected type error: %+v", typeErr) + } + }) +} + +func TestQueryAllIsRepeatable(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + store := memory.New(algo) + blob := store.AddObject(objecttype.TypeBlob, []byte("blob\n")) + tree := store.AddObject(objecttype.TypeTree, mustSerializeTree(t, &tree.Tree{Entries: []tree.TreeEntry{{ + Mode: tree.FileModeRegular, + Name: []byte("f"), + ID: blob, + }}})) + base := store.AddObject(objecttype.TypeCommit, commitBody(tree)) + left := store.AddObject(objecttype.TypeCommit, commitBody(tree, base)) + right := store.AddObject(objecttype.TypeCommit, commitBody(tree, left)) + + query := commitquery.New(store, nil) + + first, err := query.MergeBases(left, right) + if err != nil { + t.Fatalf("query.MergeBases() first call: %v", err) + } + + again, err := query.MergeBases(left, right) + if err != nil { + t.Fatalf("query.MergeBases() second call: %v", err) + } + + if !slices.Equal(again, first) { + t.Fatalf("second All()=%v, want %v", again, first) + } + + if len(first) == 0 { + t.Fatal("first MergeBases() unexpectedly returned no results") + } + + first[0] = objectid.ObjectID{} + + third, err := query.MergeBases(left, right) + if err != nil { + t.Fatalf("query.MergeBases() third call: %v", err) + } + + if third[0] == (objectid.ObjectID{}) { + t.Fatal("query.MergeBases() exposed internal slice state") + } + }) +} -- cgit v1.3.1-10-gc9f91