aboutsummaryrefslogtreecommitdiff
path: root/format/commitgraph/read/read_test.go
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-03-06 11:54:21 +0800
committerGravatar Runxi Yu2026-03-06 11:55:56 +0800
commitc62c5544fa23378843a3383a9dcd4494e5ea33bc (patch)
tree8b825a36767fe0ba3fb44f27cb634047c4c0318f /format/commitgraph/read/read_test.go
parentformat/pack/ingest: Fix delta apply import (diff)
signatureNo 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.go340
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
+}