From 6a7fc936c4a969aa05b3941feedafe59f4bd2ffd Mon Sep 17 00:00:00 2001 From: Runxi Yu Date: Sat, 21 Feb 2026 15:54:26 +0800 Subject: *: Add more tests --- object/commit_parse_test.go | 41 ++++++++++++++++ refstore/loose/loose_test.go | 64 +++++++++++++++++++++++++ refstore/packed/packed_test.go | 69 +++++++++++++++++++++++++++ repository/refs_test.go | 72 ++++++++++++++++++++++++++++ repository/stored_test.go | 103 +++++++++++++++++++++++++++++++++++++++++ repository/traversal_test.go | 79 +++++++++++++++++++++++++++++++ 6 files changed, 428 insertions(+) create mode 100644 repository/traversal_test.go diff --git a/object/commit_parse_test.go b/object/commit_parse_test.go index f01052ac..a29ab1fa 100644 --- a/object/commit_parse_test.go +++ b/object/commit_parse_test.go @@ -2,6 +2,7 @@ package object_test import ( "bytes" + "fmt" "testing" "codeberg.org/lindenii/furgit/internal/testgit" @@ -37,3 +38,43 @@ func TestCommitParseFromGit(t *testing.T) { } }) } + +func TestCommitParseMultipleParents(t *testing.T) { + t.Parallel() + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) + + _, treeID := testRepo.MakeSingleFileTree(t, "file.txt", []byte("merge-content\n")) + parent1 := testRepo.CommitTree(t, treeID, "parent-one") + parent2 := testRepo.CommitTree(t, treeID, "parent-two", parent1) + + rawCommit := fmt.Sprintf( + "tree %s\nparent %s\nparent %s\nauthor Test Author 1234567890 +0000\ncommitter Test Committer 1234567890 +0000\n\nMerge commit\n", + treeID, + parent1, + parent2, + ) + mergeID := testRepo.HashObject(t, "commit", []byte(rawCommit)) + rawBody := testRepo.CatFile(t, "commit", mergeID) + + commit, err := object.ParseCommit(rawBody, algo) + if err != nil { + t.Fatalf("ParseCommit(merge): %v", err) + } + if commit.Tree != treeID { + t.Fatalf("merge tree = %s, want %s", commit.Tree, treeID) + } + if len(commit.Parents) != 2 { + t.Fatalf("merge parent count = %d, want 2", len(commit.Parents)) + } + if commit.Parents[0] != parent1 { + t.Fatalf("merge parent[0] = %s, want %s", commit.Parents[0], parent1) + } + if commit.Parents[1] != parent2 { + t.Fatalf("merge parent[1] = %s, want %s", commit.Parents[1], parent2) + } + if !bytes.Equal(commit.Message, []byte("Merge commit\n")) { + t.Fatalf("merge message = %q, want %q", commit.Message, "Merge commit\n") + } + }) +} diff --git a/refstore/loose/loose_test.go b/refstore/loose/loose_test.go index 37f31222..8c9d6f98 100644 --- a/refstore/loose/loose_test.go +++ b/refstore/loose/loose_test.go @@ -133,6 +133,70 @@ func TestLooseListPattern(t *testing.T) { }) } +func TestLooseListPatternMatrix(t *testing.T) { + t.Parallel() + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) + _, _, commitID := testRepo.MakeCommit(t, "loose refs pattern matrix") + testRepo.UpdateRef(t, "refs/heads/main", commitID) + testRepo.UpdateRef(t, "refs/heads/feature/one", commitID) + testRepo.UpdateRef(t, "refs/notes/review", commitID) + testRepo.UpdateRef(t, "refs/tags/v1", commitID) + testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") + + store := openLooseStore(t, testRepo.Dir(), algo) + + tests := []struct { + pattern string + want []string + }{ + { + pattern: "refs/heads/*", + want: []string{"refs/heads/main"}, + }, + { + pattern: "refs/heads/*/*", + want: []string{"refs/heads/feature/one"}, + }, + { + pattern: "refs/*/feature/one", + want: []string{"refs/heads/feature/one"}, + }, + { + pattern: "refs/heads/feat?re/one", + want: []string{"refs/heads/feature/one"}, + }, + { + pattern: "refs/tags/v[0-9]", + want: []string{"refs/tags/v1"}, + }, + { + pattern: "refs/*/*", + want: []string{"refs/heads/main", "refs/notes/review", "refs/tags/v1"}, + }, + } + + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + got, err := store.List(tt.pattern) + if err != nil { + t.Fatalf("List(%q): %v", tt.pattern, err) + } + gotNames := make([]string, 0, len(got)) + for _, entry := range got { + gotNames = append(gotNames, entry.Name()) + } + slices.Sort(gotNames) + wantNames := append([]string(nil), tt.want...) + slices.Sort(wantNames) + if !slices.Equal(gotNames, wantNames) { + t.Fatalf("List(%q) names = %v, want %v", tt.pattern, gotNames, wantNames) + } + }) + } + }) +} + func TestLooseMalformedDetachedRef(t *testing.T) { t.Parallel() testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper diff --git a/refstore/packed/packed_test.go b/refstore/packed/packed_test.go index efec6ca7..77d15640 100644 --- a/refstore/packed/packed_test.go +++ b/refstore/packed/packed_test.go @@ -133,6 +133,67 @@ func TestPackedListAndShorten(t *testing.T) { }) } +func TestPackedListPatternMatrix(t *testing.T) { + t.Parallel() + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) + _, _, commitID := testRepo.MakeCommit(t, "packed refs pattern matrix") + testRepo.UpdateRef(t, "refs/heads/main", commitID) + testRepo.UpdateRef(t, "refs/heads/feature/one", commitID) + testRepo.UpdateRef(t, "refs/notes/review", commitID) + testRepo.UpdateRef(t, "refs/tags/v1", commitID) + testRepo.PackRefs(t, "--all", "--prune") + + store := openPackedRefStoreFromRepo(t, testRepo.Dir(), algo) + + tests := []struct { + pattern string + want []string + }{ + { + pattern: "refs/heads/*", + want: []string{"refs/heads/main"}, + }, + { + pattern: "refs/heads/*/*", + want: []string{"refs/heads/feature/one"}, + }, + { + pattern: "refs/*/feature/one", + want: []string{"refs/heads/feature/one"}, + }, + { + pattern: "refs/heads/feat?re/one", + want: []string{"refs/heads/feature/one"}, + }, + { + pattern: "refs/tags/v[0-9]", + want: []string{"refs/tags/v1"}, + }, + { + pattern: "refs/*/*", + want: []string{"refs/heads/main", "refs/notes/review", "refs/tags/v1"}, + }, + } + + for _, tt := range tests { + t.Run(tt.pattern, func(t *testing.T) { + got, err := store.List(tt.pattern) + if err != nil { + t.Fatalf("List(%q): %v", tt.pattern, err) + } + gotNames := refNames(got) + slices.Sort(gotNames) + wantNames := append([]string(nil), tt.want...) + slices.Sort(wantNames) + if !slices.Equal(gotNames, wantNames) { + t.Fatalf("List(%q) names = %v, want %v", tt.pattern, gotNames, wantNames) + } + }) + } + }) +} + func TestPackedParseErrors(t *testing.T) { t.Parallel() testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper @@ -175,6 +236,14 @@ func TestPackedNewValidation(t *testing.T) { } } +func refNames(refs []ref.Ref) []string { + names := make([]string, 0, len(refs)) + for _, entry := range refs { + names = append(names, entry.Name()) + } + return names +} + func stringsOfLen(ch string, n int) string { return string(bytes.Repeat([]byte(ch), n)) } diff --git a/repository/refs_test.go b/repository/refs_test.go index 4418c707..8ebf93a6 100644 --- a/repository/refs_test.go +++ b/repository/refs_test.go @@ -94,3 +94,75 @@ func TestResolveRefErrorSurface(t *testing.T) { } }) } + +func TestListRefsLooseOverridesPacked(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") + _, _, commit1 := repoHarness.MakeCommit(t, "commit-one") + repoHarness.UpdateRef(t, "refs/heads/main", commit1) + repoHarness.UpdateRef(t, "refs/heads/feature", commit1) + repoHarness.PackRefs(t, "--all", "--prune") + + _, _, commit2 := repoHarness.MakeCommit(t, "commit-two") + repoHarness.UpdateRef(t, "refs/heads/main", commit2) + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + mainRef, err := repo.ResolveRefFully("refs/heads/main") + if err != nil { + t.Fatalf("ResolveRefFully(main): %v", err) + } + if mainRef.ID != commit2 { + t.Fatalf("ResolveRefFully(main) id = %s, want %s", mainRef.ID, commit2) + } + + refs, err := repo.ListRefs("refs/heads/*") + if err != nil { + t.Fatalf("ListRefs(refs/heads/*): %v", err) + } + byName := make(map[string]ref.Ref, len(refs)) + for _, entry := range refs { + name := entry.Name() + if _, exists := byName[name]; exists { + t.Fatalf("duplicate ref %q in ListRefs output", name) + } + byName[name] = entry + } + + main, ok := byName["refs/heads/main"] + if !ok { + t.Fatalf("missing refs/heads/main in ListRefs output") + } + mainDetached, ok := main.(ref.Detached) + if !ok { + t.Fatalf("refs/heads/main type = %T, want ref.Detached", main) + } + if mainDetached.ID != commit2 { + t.Fatalf("refs/heads/main id = %s, want %s", mainDetached.ID, commit2) + } + + feature, ok := byName["refs/heads/feature"] + if !ok { + t.Fatalf("missing refs/heads/feature in ListRefs output") + } + featureDetached, ok := feature.(ref.Detached) + if !ok { + t.Fatalf("refs/heads/feature type = %T, want ref.Detached", feature) + } + if featureDetached.ID != commit1 { + t.Fatalf("refs/heads/feature id = %s, want %s", featureDetached.ID, commit1) + } + }) +} diff --git a/repository/stored_test.go b/repository/stored_test.go index da1d1392..3768d450 100644 --- a/repository/stored_test.go +++ b/repository/stored_test.go @@ -161,3 +161,106 @@ func TestResolveTreeEntryErrors(t *testing.T) { }) }) } + +func TestResolveTreeEntryDeepPath(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + const depth = 50 + + repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + leafBlobID := repoHarness.HashObject(t, "blob", []byte("deep-content\n")) + currentTree := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", leafBlobID)) + + parts := make([][]byte, 0, depth+1) + for i := depth - 1; i >= 0; i-- { + name := fmt.Sprintf("level%02d", i) + currentTree = repoHarness.Mktree(t, fmt.Sprintf("040000 tree %s\t%s\n", currentTree, name)) + parts = append([][]byte{[]byte(name)}, parts...) + } + parts = append(parts, []byte("leaf.txt")) + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + rootTree, err := repo.ReadStoredTree(currentTree) + if err != nil { + t.Fatalf("ReadStoredTree(root): %v", err) + } + + entry, err := repo.ResolveTreeEntry(rootTree, parts) + if err != nil { + t.Fatalf("ResolveTreeEntry(deep): %v", err) + } + if entry.Mode != object.FileModeRegular { + t.Fatalf("ResolveTreeEntry(deep) mode = %o, want %o", entry.Mode, object.FileModeRegular) + } + if entry.ID != leafBlobID { + t.Fatalf("ResolveTreeEntry(deep) id = %s, want %s", entry.ID, leafBlobID) + } + }) +} + +func TestReadStoredTreeMixedModes(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + normalID := repoHarness.HashObject(t, "blob", []byte("normal-file\n")) + execID := repoHarness.HashObject(t, "blob", []byte("#!/bin/sh\necho hi\n")) + symID := repoHarness.HashObject(t, "blob", []byte("normal.txt")) + nestedBlobID := repoHarness.HashObject(t, "blob", []byte("nested\n")) + nestedTreeID := repoHarness.Mktree(t, fmt.Sprintf("100644 blob %s\tleaf.txt\n", nestedBlobID)) + + rootTreeID := repoHarness.Mktree(t, + fmt.Sprintf( + "100644 blob %s\tnormal.txt\n100755 blob %s\trun.sh\n120000 blob %s\tlink.txt\n040000 tree %s\tdir\n", + normalID, + execID, + symID, + nestedTreeID, + ), + ) + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + rootTree, err := repo.ReadStoredTree(rootTreeID) + if err != nil { + t.Fatalf("ReadStoredTree(root): %v", err) + } + + expect := map[string]object.FileMode{ + "normal.txt": object.FileModeRegular, + "run.sh": object.FileModeExecutable, + "link.txt": object.FileModeSymlink, + "dir": object.FileModeDir, + } + + for name, wantMode := range expect { + entry := rootTree.Tree().Entry([]byte(name)) + if entry == nil { + t.Fatalf("Entry(%q) returned nil", name) + } + if entry.Mode != wantMode { + t.Fatalf("Entry(%q) mode = %o, want %o", name, entry.Mode, wantMode) + } + } + }) +} diff --git a/repository/traversal_test.go b/repository/traversal_test.go new file mode 100644 index 00000000..61560fde --- /dev/null +++ b/repository/traversal_test.go @@ -0,0 +1,79 @@ +package repository_test + +import ( + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/object" + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/repository" +) + +func TestRepositoryDepthFirstEnumerationFromHEAD(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + repoHarness := testgit.NewRepo(t, testgit.RepoOptions{ + ObjectFormat: algo, + Bare: true, + RefFormat: "files", + }) + + _, _, commit1 := repoHarness.MakeCommit(t, "walk-one") + blob2, tree2 := repoHarness.MakeSingleFileTree(t, "second.txt", []byte("second\n")) + commit2 := repoHarness.CommitTree(t, tree2, "walk-two", commit1) + _ = blob2 + repoHarness.UpdateRef(t, "refs/heads/main", commit2) + repoHarness.SymbolicRef(t, "HEAD", "refs/heads/main") + + repo, err := repository.Open(repoHarness.Dir()) + if err != nil { + t.Fatalf("repository.Open: %v", err) + } + defer func() { _ = repo.Close() }() + + head, err := repo.ResolveRefFully("HEAD") + if err != nil { + t.Fatalf("ResolveRefFully(HEAD): %v", err) + } + + visited := make(map[objectid.ObjectID]bool) + queue := []objectid.ObjectID{head.ID} + objectsRead := 0 + + for len(queue) > 0 { + id := queue[0] + queue = queue[1:] + + if visited[id] { + continue + } + visited[id] = true + + stored, err := repo.ReadStored(id) + if err != nil { + t.Fatalf("ReadStored(%s): %v", id, err) + } + objectsRead++ + + switch obj := stored.Object().(type) { + case *object.Commit: + queue = append(queue, obj.Tree) + queue = append(queue, obj.Parents...) + case *object.Tree: + for _, entry := range obj.Entries { + queue = append(queue, entry.ID) + } + case *object.Tag: + queue = append(queue, obj.Target) + case *object.Blob: + default: + t.Fatalf("unexpected object type: %T", obj) + } + } + + if objectsRead == 0 { + t.Fatalf("no objects were enumerated from HEAD") + } + }) +} -- cgit v1.3.1-10-gc9f91