diff options
| author | 2026-03-06 11:54:21 +0800 | |
|---|---|---|
| committer | 2026-03-06 11:55:56 +0800 | |
| commit | c62c5544fa23378843a3383a9dcd4494e5ea33bc (patch) | |
| tree | 8b825a36767fe0ba3fb44f27cb634047c4c0318f /format/commitgraph/read/read_test.go | |
| parent | format/pack/ingest: Fix delta apply import (diff) | |
| signature | No signature | |
format/commitgraph: Split into ./read and ./ v0.1.60
Diffstat (limited to 'format/commitgraph/read/read_test.go')
| -rw-r--r-- | format/commitgraph/read/read_test.go | 340 |
1 files changed, 340 insertions, 0 deletions
diff --git a/format/commitgraph/read/read_test.go b/format/commitgraph/read/read_test.go new file mode 100644 index 00000000..0efd67ca --- /dev/null +++ b/format/commitgraph/read/read_test.go @@ -0,0 +1,340 @@ +package read_test + +import ( + "errors" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + "codeberg.org/lindenii/furgit/format/commitgraph/bloom" + "codeberg.org/lindenii/furgit/format/commitgraph/read" + "codeberg.org/lindenii/furgit/internal/intconv" + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/objectid" +) + +func fixtureRepoPath(t *testing.T, algo objectid.Algorithm, name string) string { + t.Helper() + + return filepath.Join("testdata", "fixtures", algo.String(), name, "repo.git") +} + +func fixtureRepo(t *testing.T, algo objectid.Algorithm, name string) *testgit.TestRepo { + t.Helper() + + return testgit.NewRepoFromFixture(t, algo, fixtureRepoPath(t, algo, name)) +} + +func TestReadSingleMatchesGit(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := fixtureRepo(t, algo, "single_changed") + + reader := openReader(t, testRepo, read.OpenSingle) + + defer func() { _ = reader.Close() }() + + allIDs := testRepo.RevList(t, "--all") + if len(allIDs) == 0 { + t.Fatal("git rev-list --all returned no commits") + } + + wantCommitCount, err := intconv.IntToUint32(len(allIDs)) + if err != nil { + t.Fatalf("len(allIDs) convert: %v", err) + } + + if got := reader.NumCommits(); got != wantCommitCount { + t.Fatalf("NumCommits() = %d, want %d", got, len(allIDs)) + } + + if !reader.HasBloom() { + t.Fatal("HasBloom() = false, want true") + } + + bloomVersion := reader.BloomVersion() + if bloomVersion == 0 { + t.Fatal("BloomVersion() = 0, want non-zero when HasBloom() is true") + } + + for _, id := range allIDs { + pos, err := reader.Lookup(id) + if err != nil { + t.Fatalf("Lookup(%s): %v", id, err) + } + + gotID, err := reader.OIDAt(pos) + if err != nil { + t.Fatalf("OIDAt(%+v): %v", pos, err) + } + + if gotID != id { + t.Fatalf("OIDAt(Lookup(%s)) = %s, want %s", id, gotID, id) + } + } + + step := len(allIDs) / 24 + if step < 1 { + step = 1 + } + + for i, id := range allIDs { + if i%step != 0 && i != len(allIDs)-1 { + continue + } + + verifyCommitAgainstGit(t, testRepo, reader, id) + } + }) +} + +func TestReadChainMatchesGit(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := fixtureRepo(t, algo, "chain_changed") + + reader := openReader(t, testRepo, read.OpenChain) + + defer func() { _ = reader.Close() }() + + layers := reader.Layers() + if len(layers) < 2 { + t.Fatalf("Layers len = %d, want >= 2", len(layers)) + } + + allIDs := testRepo.RevList(t, "--all") + + wantCommitCount, err := intconv.IntToUint32(len(allIDs)) + if err != nil { + t.Fatalf("len(allIDs) convert: %v", err) + } + + if got := reader.NumCommits(); got != wantCommitCount { + t.Fatalf("NumCommits() = %d, want %d", got, len(allIDs)) + } + + step := len(allIDs) / 20 + if step < 1 { + step = 1 + } + + for i, id := range allIDs { + pos, err := reader.Lookup(id) + if err != nil { + t.Fatalf("Lookup(%s): %v", id, err) + } + + if i%step != 0 && i != len(allIDs)-1 { + continue + } + + gotID, err := reader.OIDAt(pos) + if err != nil { + t.Fatalf("OIDAt(%+v): %v", pos, err) + } + + if gotID != id { + t.Fatalf("OIDAt(Lookup(%s)) = %s, want %s", id, gotID, id) + } + } + }) +} + +func TestBloomUnavailableWithoutChangedPaths(t *testing.T) { + t.Parallel() + + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper + testRepo := fixtureRepo(t, algo, "single_nochanged") + + reader := openReader(t, testRepo, read.OpenSingle) + + defer func() { _ = reader.Close() }() + + head := testRepo.RevParse(t, "HEAD") + + pos, err := reader.Lookup(head) + if err != nil { + t.Fatalf("Lookup(%s): %v", head, err) + } + + _, err = reader.BloomFilterAt(pos) + if err == nil { + t.Fatal("BloomFilterAt() error = nil, want ErrBloomUnavailable") + } + + var unavailable *read.ErrBloomUnavailable + if !errors.As(err, &unavailable) { + t.Fatalf("BloomFilterAt() error type = %T, want *ErrBloomUnavailable", err) + } + + if unavailable.Pos != pos { + t.Fatalf("ErrBloomUnavailable.Pos = %+v, want %+v", unavailable.Pos, pos) + } + }) +} + +func openReader(tb testing.TB, testRepo *testgit.TestRepo, mode read.OpenMode) *read.Reader { + tb.Helper() + + objectsPath := filepath.Join(testRepo.Dir(), "objects") + + root, err := os.OpenRoot(objectsPath) + if err != nil { + tb.Fatalf("os.OpenRoot(%q): %v", objectsPath, err) + } + + reader, err := read.Open(root, testRepo.Algorithm(), mode) + + closeErr := root.Close() + if closeErr != nil { + tb.Fatalf("close objects root: %v", closeErr) + } + + if err != nil { + tb.Fatalf("read.Open(%q): %v", objectsPath, err) + } + + return reader +} + +func verifyCommitAgainstGit(tb testing.TB, testRepo *testgit.TestRepo, reader *read.Reader, id objectid.ObjectID) { + tb.Helper() + + pos, err := reader.Lookup(id) + if err != nil { + tb.Fatalf("Lookup(%s): %v", id, err) + } + + commit, err := reader.CommitAt(pos) + if err != nil { + tb.Fatalf("CommitAt(%+v): %v", pos, err) + } + + if commit.OID != id { + tb.Fatalf("CommitAt(%+v).OID = %s, want %s", pos, commit.OID, id) + } + + treeHex := testRepo.Run(tb, "show", "-s", "--format=%T", id.String()) + + wantTree, err := objectid.ParseHex(testRepo.Algorithm(), treeHex) + if err != nil { + tb.Fatalf("parse tree id %q: %v", treeHex, err) + } + + if commit.TreeOID != wantTree { + tb.Fatalf("CommitAt(%+v).TreeOID = %s, want %s", pos, commit.TreeOID, wantTree) + } + + wantParents := parseOIDLine(tb, testRepo.Algorithm(), testRepo.Run(tb, "show", "-s", "--format=%P", id.String())) + + gotParents := commitParents(tb, reader, commit) + if len(gotParents) != len(wantParents) { + tb.Fatalf("parent count for %s = %d, want %d", id, len(gotParents), len(wantParents)) + } + + for i := range gotParents { + if gotParents[i] != wantParents[i] { + tb.Fatalf("parent %d for %s = %s, want %s", i, id, gotParents[i], wantParents[i]) + } + } + + commitTimeRaw := testRepo.Run(tb, "show", "-s", "--format=%ct", id.String()) + + wantCommitTime, err := strconv.ParseInt(strings.TrimSpace(commitTimeRaw), 10, 64) + if err != nil { + tb.Fatalf("parse commit time %q: %v", commitTimeRaw, err) + } + + if commit.CommitTimeUnix != wantCommitTime { + tb.Fatalf("CommitAt(%+v).CommitTimeUnix = %d, want %d", pos, commit.CommitTimeUnix, wantCommitTime) + } + + filter, err := reader.BloomFilterAt(pos) + if err != nil { + tb.Fatalf("BloomFilterAt(%+v): %v", pos, err) + } + + if filter.HashVersion != uint32(reader.BloomVersion()) { + tb.Fatalf("filter.HashVersion = %d, want %d", filter.HashVersion, reader.BloomVersion()) + } + + assertChangedPathsBloomPositive(tb, testRepo, filter, id) +} + +func commitParents(tb testing.TB, reader *read.Reader, commit read.Commit) []objectid.ObjectID { + tb.Helper() + + out := make([]objectid.ObjectID, 0, 2+len(commit.ExtraParents)) + + if commit.Parent1.Valid { + id, err := reader.OIDAt(commit.Parent1.Pos) + if err != nil { + tb.Fatalf("OIDAt(parent1 %+v): %v", commit.Parent1.Pos, err) + } + + out = append(out, id) + } + + if commit.Parent2.Valid { + id, err := reader.OIDAt(commit.Parent2.Pos) + if err != nil { + tb.Fatalf("OIDAt(parent2 %+v): %v", commit.Parent2.Pos, err) + } + + out = append(out, id) + } + + for _, parentPos := range commit.ExtraParents { + id, err := reader.OIDAt(parentPos) + if err != nil { + tb.Fatalf("OIDAt(extra parent %+v): %v", parentPos, err) + } + + out = append(out, id) + } + + return out +} + +func assertChangedPathsBloomPositive(tb testing.TB, testRepo *testgit.TestRepo, filter *bloom.Filter, commitID objectid.ObjectID) { + tb.Helper() + + changedPaths := testRepo.Run(tb, "diff-tree", "--no-commit-id", "--name-only", "-r", "--root", commitID.String()) + for line := range strings.SplitSeq(strings.TrimSpace(changedPaths), "\n") { + path := strings.TrimSpace(line) + if path == "" { + continue + } + + mightContain, err := filter.MightContain([]byte(path)) + if err != nil { + tb.Fatalf("MightContain(%q): %v", path, err) + } + + if !mightContain { + tb.Fatalf("Bloom filter false negative for commit %s path %q", commitID, path) + } + } +} + +func parseOIDLine(tb testing.TB, algo objectid.Algorithm, line string) []objectid.ObjectID { + tb.Helper() + + toks := strings.Fields(line) + + out := make([]objectid.ObjectID, 0, len(toks)) + for _, tok := range toks { + id, err := objectid.ParseHex(algo, tok) + if err != nil { + tb.Fatalf("parse object id %q: %v", tok, err) + } + + out = append(out, id) + } + + return out +} |
