aboutsummaryrefslogtreecommitdiff
path: root/format/pack/ingest/ingest_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'format/pack/ingest/ingest_test.go')
-rw-r--r--format/pack/ingest/ingest_test.go337
1 files changed, 337 insertions, 0 deletions
diff --git a/format/pack/ingest/ingest_test.go b/format/pack/ingest/ingest_test.go
new file mode 100644
index 00000000..d1b68cba
--- /dev/null
+++ b/format/pack/ingest/ingest_test.go
@@ -0,0 +1,337 @@
+package ingest_test
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/format/pack/ingest"
+ "codeberg.org/lindenii/furgit/internal/testgit"
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/repository"
+)
+
+// thinBaseDistances enumerates candidate main-history distances for thin-pack
+// probing.
+var thinBaseDistances = [...]int{16, 24, 32, 48, 64, 96, 128, 160, 192, 224, 256, 320}
+
+// pickThinBase selects one main-history base where git emits a truly thin pack
+// for `head ^base`.
+func pickThinBase(t *testing.T, sender *testgit.TestRepo, head objectid.ObjectID) objectid.ObjectID {
+ t.Helper()
+
+ for _, distance := range thinBaseDistances {
+ base := sender.RevParse(t, fmt.Sprintf("refs/heads/main~%d", distance))
+ revs := []string{head.String(), "^" + base.String()}
+ if sender.PackObjectsIsThin(t, revs) {
+ return base
+ }
+ }
+
+ t.Fatalf("failed to find thin base for head %s", head.String())
+
+ return objectid.ObjectID{}
+}
+
+// verifyReindexOracle regenerates idx/rev with upstream git index-pack and
+// compares bytes with files produced by ingest.
+func verifyReindexOracle(t *testing.T, repo *testgit.TestRepo, packPath, idxPath, revPath string) {
+ t.Helper()
+
+ oracleDir := t.TempDir()
+ oracleIdxPath := filepath.Join(oracleDir, "oracle.idx")
+ _ = repo.Run(t, "index-pack", "--rev-index", "-o", oracleIdxPath, packPath)
+ oracleRevPath := strings.TrimSuffix(oracleIdxPath, ".idx") + ".rev"
+
+ gotIdx, err := os.ReadFile(idxPath)
+ if err != nil {
+ t.Fatalf("read idx: %v", err)
+ }
+ wantIdx, err := os.ReadFile(oracleIdxPath)
+ if err != nil {
+ t.Fatalf("read oracle idx: %v", err)
+ }
+ if !bytes.Equal(gotIdx, wantIdx) {
+ t.Fatal("idx bytes differ from git index-pack output")
+ }
+
+ gotRev, err := os.ReadFile(revPath)
+ if err != nil {
+ t.Fatalf("read rev: %v", err)
+ }
+ wantRev, err := os.ReadFile(oracleRevPath)
+ if err != nil {
+ t.Fatalf("read oracle rev: %v", err)
+ }
+ if !bytes.Equal(gotRev, wantRev) {
+ t.Fatal("rev bytes differ from git index-pack output")
+ }
+}
+
+func TestIngestNonThinPackWritesPackIdxRev(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ sender.MakeManyObjectsHistory(t)
+ head := sender.RevParse(t, "refs/heads/main")
+
+ reader := sender.PackObjectsReader(t, []string{head.String()}, false)
+ defer func() {
+ err := reader.Close()
+ if err != nil {
+ t.Fatalf("close pack reader: %v", err)
+ }
+ }()
+
+ receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ packRoot, err := os.OpenRoot(filepath.Join(receiver.Dir(), "objects", "pack"))
+ if err != nil {
+ t.Fatalf("open pack root: %v", err)
+ }
+ defer func() {
+ err = packRoot.Close()
+ if err != nil {
+ t.Fatalf("close pack root: %v", err)
+ }
+ }()
+
+ result, err := ingest.Ingest(reader, packRoot, algo, false, true, nil)
+ if err != nil {
+ t.Fatalf("Ingest: %v", err)
+ }
+ if result.ThinFixed {
+ t.Fatalf("ThinFixed = true, want false")
+ }
+ if result.RevName == "" {
+ t.Fatal("RevName is empty")
+ }
+
+ _, err = packRoot.Stat(result.PackName)
+ if err != nil {
+ t.Fatalf("stat pack: %v", err)
+ }
+ _, err = packRoot.Stat(result.IdxName)
+ if err != nil {
+ t.Fatalf("stat idx: %v", err)
+ }
+ _, err = packRoot.Stat(result.RevName)
+ if err != nil {
+ t.Fatalf("stat rev: %v", err)
+ }
+
+ idxPath := filepath.Join(receiver.Dir(), "objects", "pack", result.IdxName)
+ packPath := filepath.Join(receiver.Dir(), "objects", "pack", result.PackName)
+ revPath := filepath.Join(receiver.Dir(), "objects", "pack", result.RevName)
+ _ = receiver.Run(t, "verify-pack", "-v", idxPath)
+ verifyReindexOracle(t, receiver, packPath, idxPath, revPath)
+
+ receiver.UpdateRef(t, "refs/heads/main", head)
+ wantRaw := sender.Run(t, "rev-list", "--objects", "refs/heads/main")
+ for line := range strings.SplitSeq(strings.TrimSpace(wantRaw), "\n") {
+ line = strings.TrimSpace(line)
+ if line == "" {
+ continue
+ }
+
+ fields := strings.Fields(line)
+ if len(fields) == 0 {
+ continue
+ }
+ _ = receiver.Run(t, "cat-file", "-e", fields[0])
+ }
+ })
+}
+
+func TestIngestThinPackWithoutFixReturnsUnresolved(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ sender.MakeManyObjectsHistory(t)
+ sender.Repack(t, "-a", "-d", "-f", "--window=128", "--depth=128")
+
+ head := sender.RevParse(t, "refs/heads/main")
+ base := pickThinBase(t, sender, head)
+ reader := sender.PackObjectsReader(t, []string{head.String(), "^" + base.String()}, true)
+ defer func() {
+ err := reader.Close()
+ if err != nil {
+ t.Fatalf("close pack reader: %v", err)
+ }
+ }()
+
+ receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ packDir := filepath.Join(receiver.Dir(), "objects", "pack")
+ packRoot, err := os.OpenRoot(packDir)
+ if err != nil {
+ t.Fatalf("open pack root: %v", err)
+ }
+ defer func() {
+ err = packRoot.Close()
+ if err != nil {
+ t.Fatalf("close pack root: %v", err)
+ }
+ }()
+
+ _, err = ingest.Ingest(reader, packRoot, algo, false, true, nil)
+ if err == nil {
+ t.Fatal("Ingest error = nil, want error")
+ }
+
+ var unresolved *ingest.ErrThinPackUnresolved
+ if !errors.As(err, &unresolved) {
+ t.Fatalf("Ingest error type = %T (%v), want *ErrThinPackUnresolved", err, err)
+ }
+
+ matches, err := filepath.Glob(filepath.Join(packDir, "pack-*.pack"))
+ if err != nil {
+ t.Fatalf("glob pack files: %v", err)
+ }
+ if len(matches) != 0 {
+ t.Fatalf("found finalized pack files after failure: %v", matches)
+ }
+ })
+}
+
+func TestIngestThinPackWithFixThin(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ sender.MakeManyObjectsHistory(t)
+ sender.Repack(t, "-a", "-d", "-f", "--window=128", "--depth=128")
+
+ head := sender.RevParse(t, "refs/heads/main")
+ base := pickThinBase(t, sender, head)
+ receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+
+ packRoot, err := os.OpenRoot(filepath.Join(receiver.Dir(), "objects", "pack"))
+ if err != nil {
+ t.Fatalf("open pack root: %v", err)
+ }
+ defer func() {
+ err = packRoot.Close()
+ if err != nil {
+ t.Fatalf("close pack root: %v", err)
+ }
+ }()
+
+ baseReader := sender.PackObjectsReader(t, []string{base.String()}, false)
+ _, err = ingest.Ingest(baseReader, packRoot, algo, false, false, nil)
+ if err != nil {
+ _ = baseReader.Close()
+ t.Fatalf("ingest base pack: %v", err)
+ }
+ err = baseReader.Close()
+ if err != nil {
+ t.Fatalf("close base reader: %v", err)
+ }
+
+ receiverRoot, err := os.OpenRoot(receiver.Dir())
+ if err != nil {
+ t.Fatalf("open receiver root: %v", err)
+ }
+ defer func() {
+ err = receiverRoot.Close()
+ if err != nil {
+ t.Fatalf("close receiver root: %v", err)
+ }
+ }()
+
+ receiverRepo, err := repository.Open(receiverRoot)
+ if err != nil {
+ t.Fatalf("repository.Open(receiver): %v", err)
+ }
+ defer func() {
+ err = receiverRepo.Close()
+ if err != nil {
+ t.Fatalf("close receiver repo: %v", err)
+ }
+ }()
+
+ thinReader := sender.PackObjectsReader(t, []string{head.String(), "^" + base.String()}, true)
+ result, err := ingest.Ingest(thinReader, packRoot, algo, true, true, receiverRepo.Objects())
+ if err != nil {
+ _ = thinReader.Close()
+ t.Fatalf("Ingest(thin): %v", err)
+ }
+ err = thinReader.Close()
+ if err != nil {
+ t.Fatalf("close thin reader: %v", err)
+ }
+ if !result.ThinFixed {
+ t.Fatal("ThinFixed = false, want true")
+ }
+
+ idxPath := filepath.Join(receiver.Dir(), "objects", "pack", result.IdxName)
+ packPath := filepath.Join(receiver.Dir(), "objects", "pack", result.PackName)
+ revPath := filepath.Join(receiver.Dir(), "objects", "pack", result.RevName)
+ _ = receiver.Run(t, "verify-pack", "-v", idxPath)
+ verifyReindexOracle(t, receiver, packPath, idxPath, revPath)
+ receiver.UpdateRef(t, "refs/heads/main", head)
+ _ = receiver.Run(t, "fsck", "--full", "--strict", "--no-progress", "--no-dangling")
+ })
+}
+
+func TestIngestPackTrailerMismatch(t *testing.T) {
+ t.Parallel()
+
+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
+ sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ sender.MakeManyObjectsHistory(t)
+ head := sender.RevParse(t, "refs/heads/main")
+
+ stream := sender.PackObjectsReader(t, []string{head.String()}, false)
+ packBytes, err := io.ReadAll(stream)
+ if err != nil {
+ _ = stream.Close()
+ t.Fatalf("read pack stream: %v", err)
+ }
+ err = stream.Close()
+ if err != nil {
+ t.Fatalf("close stream: %v", err)
+ }
+ if len(packBytes) == 0 {
+ t.Fatal("empty pack stream")
+ }
+
+ packBytes[len(packBytes)-1] ^= 0xff
+
+ receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+ packDir := filepath.Join(receiver.Dir(), "objects", "pack")
+ packRoot, err := os.OpenRoot(packDir)
+ if err != nil {
+ t.Fatalf("open pack root: %v", err)
+ }
+ defer func() {
+ err = packRoot.Close()
+ if err != nil {
+ t.Fatalf("close pack root: %v", err)
+ }
+ }()
+
+ _, err = ingest.Ingest(bytes.NewReader(packBytes), packRoot, algo, false, true, nil)
+ if err == nil {
+ t.Fatal("Ingest error = nil, want error")
+ }
+
+ var mismatch *ingest.ErrPackTrailerMismatch
+ if !errors.As(err, &mismatch) {
+ t.Fatalf("Ingest error type = %T (%v), want *ErrPackTrailerMismatch", err, err)
+ }
+
+ matches, err := filepath.Glob(filepath.Join(packDir, "pack-*.pack"))
+ if err != nil {
+ t.Fatalf("glob pack files: %v", err)
+ }
+ if len(matches) != 0 {
+ t.Fatalf("found finalized pack files after failure: %v", matches)
+ }
+ })
+}