package ingest_test import ( "bytes" "errors" "io" "os" "path/filepath" "testing" "lindenii.org/go/furgit/internal/format/packidx" "lindenii.org/go/furgit/internal/format/packidx/bloom" "lindenii.org/go/furgit/internal/testgit" "lindenii.org/go/furgit/object/id" "lindenii.org/go/furgit/object/store" "lindenii.org/go/furgit/object/store/packed" "lindenii.org/go/furgit/object/store/packed/internal/ingest" ) // TestWritePackMatchesGit verifies that ingesting a normal pack // matches git's own pack, index, and reverse index. // // The pack is streamed through verbatim, // and the index and reverse index are regenerated deterministically, // so a successful match also confirms that scanning and delta resolution // recovered every object ID, offset, and CRC that git recorded. func TestWritePackMatchesGit(t *testing.T) { t.Parallel() for _, objectFormat := range id.SupportedObjectFormats() { t.Run(objectFormat.String(), func(t *testing.T) { t.Parallel() repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) if err != nil { t.Fatalf("NewRepo: %v", err) } seeded, err := repo.SeedHistory(t) if err != nil { t.Fatalf("SeedHistory: %v", err) } gitPrefix, err := repo.PackObjects(t, seeded.All(), testgit.PackObjectsOptions{ RevIndex: true, Revs: false, Exclude: nil, }) if err != nil { t.Fatalf("PackObjects: %v", err) } stream, err := os.ReadFile(gitPrefix + ".pack") //nolint:gosec if err != nil { t.Fatalf("ReadFile pack: %v", err) } dir, result := writePack(t, objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ ThinBase: nil, Progress: nil, }) if want := filepath.Base(gitPrefix) + ".pack"; result.PackName != want { t.Fatalf("PackName = %q, want %q", result.PackName, want) } for _, artifact := range []struct { kind string ours string want string }{ {"pack", result.PackName, gitPrefix + ".pack"}, {"idx", result.IdxName, gitPrefix + ".idx"}, {"rev", result.RevName, gitPrefix + ".rev"}, } { ours, err := os.ReadFile(filepath.Join(dir, artifact.ours)) //nolint:gosec if err != nil { t.Fatalf("ReadFile %s: %v", artifact.kind, err) } want, err := os.ReadFile(artifact.want) if err != nil { t.Fatalf("ReadFile git %s: %v", artifact.kind, err) } if !bytes.Equal(ours, want) { t.Errorf("%s differs from git: %d bytes vs %d", artifact.kind, len(ours), len(want)) } } }) } } // TestWritePackBloom verifies that ingesting a pack writes a Bloom filter // that reports every object in the pack as present. func TestWritePackBloom(t *testing.T) { t.Parallel() for _, objectFormat := range id.SupportedObjectFormats() { t.Run(objectFormat.String(), func(t *testing.T) { t.Parallel() repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) if err != nil { t.Fatalf("NewRepo: %v", err) } seeded, err := repo.SeedHistory(t) if err != nil { t.Fatalf("SeedHistory: %v", err) } gitPrefix, err := repo.PackObjects(t, seeded.All(), testgit.PackObjectsOptions{ RevIndex: true, Revs: false, Exclude: nil, }) if err != nil { t.Fatalf("PackObjects: %v", err) } stream, err := os.ReadFile(gitPrefix + ".pack") //nolint:gosec if err != nil { t.Fatalf("ReadFile pack: %v", err) } dir, result := writePack(t, objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ ThinBase: nil, Progress: nil, }) if result.BloomName == "" { t.Fatal("BloomName is empty") } bloomBytes, err := os.ReadFile(filepath.Join(dir, result.BloomName)) //nolint:gosec if err != nil { t.Fatalf("ReadFile bloom: %v", err) } filter, err := bloom.Parse(bloomBytes, objectFormat) if err != nil { t.Fatalf("bloom.Parse: %v", err) } idxBytes, err := os.ReadFile(filepath.Join(dir, result.IdxName)) //nolint:gosec if err != nil { t.Fatalf("ReadFile idx: %v", err) } index, err := packidx.Parse(idxBytes, objectFormat.Size()) if err != nil { t.Fatalf("packidx.Parse: %v", err) } if !bytes.Equal(filter.PackHash(), index.PackHash()) { t.Fatalf("filter pack hash %x, want %x", filter.PackHash(), index.PackHash()) } for pos := range index.NumObjects() { if !filter.MayContain(index.OIDAt(pos)) { t.Fatalf("filter rejects object at index position %d", pos) } } }) } } // TestWritePackEmpty verifies that a zero-object pack // succeeds without writing any artifacts. func TestWritePackEmpty(t *testing.T) { t.Parallel() for _, objectFormat := range id.SupportedObjectFormats() { t.Run(objectFormat.String(), func(t *testing.T) { t.Parallel() repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) if err != nil { t.Fatalf("NewRepo: %v", err) } stream, err := repo.PackObjectsStdout(t, nil, testgit.PackObjectsStdoutOptions{ Revs: false, Thin: false, Exclude: nil, }) if err != nil { t.Fatalf("PackObjectsStdout: %v", err) } dir, result := writePack(t, objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ ThinBase: nil, Progress: nil, }) if result.ObjectCount != 0 { t.Fatalf("ObjectCount = %d, want 0", result.ObjectCount) } if result.PackName != "" { t.Fatalf("PackName = %q, want empty", result.PackName) } entries, err := os.ReadDir(dir) if err != nil { t.Fatalf("ReadDir: %v", err) } if len(entries) != 0 { t.Fatalf("empty pack wrote %d files, want 0", len(entries)) } }) } } // TestWritePackIdempotent verifies that ingesting the same pack twice // into one store succeeds and leaves the artifacts in place. func TestWritePackIdempotent(t *testing.T) { t.Parallel() for _, objectFormat := range id.SupportedObjectFormats() { t.Run(objectFormat.String(), func(t *testing.T) { t.Parallel() repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) if err != nil { t.Fatalf("NewRepo: %v", err) } seeded, err := repo.SeedHistory(t) if err != nil { t.Fatalf("SeedHistory: %v", err) } gitPrefix, err := repo.PackObjects(t, seeded.All(), testgit.PackObjectsOptions{ RevIndex: true, Revs: false, Exclude: nil, }) if err != nil { t.Fatalf("PackObjects: %v", err) } stream, err := os.ReadFile(gitPrefix + ".pack") //nolint:gosec if err != nil { t.Fatalf("ReadFile pack: %v", err) } dir := t.TempDir() root, err := os.OpenRoot(dir) if err != nil { t.Fatalf("OpenRoot: %v", err) } t.Cleanup(func() { _ = root.Close() }) first, err := ingest.WritePack(root, objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ ThinBase: nil, Progress: nil, }) if err != nil { t.Fatalf("first WritePack: %v", err) } second, err := ingest.WritePack(root, objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ ThinBase: nil, Progress: nil, }) if err != nil { t.Fatalf("second WritePack: %v", err) } if second.PackName != first.PackName { t.Fatalf("second PackName = %q, want %q", second.PackName, first.PackName) } for _, name := range []string{first.PackName, first.IdxName, first.RevName} { _, err := os.Stat(filepath.Join(dir, name)) if err != nil { t.Fatalf("missing %q after re-write: %v", name, err) } } }) } } // writePack ingests src into a fresh store directory, // returning the directory and the ingest result. func writePack( t *testing.T, objectFormat id.ObjectFormat, src io.Reader, opts store.PackWriteOptions, ) (string, ingest.Result) { t.Helper() dir := t.TempDir() root, err := os.OpenRoot(dir) if err != nil { t.Fatalf("OpenRoot: %v", err) } t.Cleanup(func() { _ = root.Close() }) result, err := ingest.WritePack(root, objectFormat, src, opts) if err != nil { t.Fatalf("WritePack: %v", err) } return dir, result } // TestWritePackThin verifies that a thin pack is completed from the thin base // and that git accepts the resulting self-contained pack. func TestWritePackThin(t *testing.T) { t.Parallel() for _, objectFormat := range id.SupportedObjectFormats() { t.Run(objectFormat.String(), func(t *testing.T) { t.Parallel() repo, seeded := seedHistory(t, objectFormat) thinBase := fullStore(t, repo, objectFormat, seeded) stream := thinStream(t, repo, seeded) dir, result := writePack(t, objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ ThinBase: thinBase, Progress: nil, }) if !result.ThinFixed { t.Fatalf("ThinFixed = false, want true (pack was not thin)") } _, err := repo.VerifyPack(t, filepath.Join(dir, result.IdxName)) if err != nil { t.Fatalf("VerifyPack on completed pack: %v", err) } }) } } // TestWritePackThinWithoutBase verifies that a thin pack is rejected // when no thin base is supplied. func TestWritePackThinWithoutBase(t *testing.T) { t.Parallel() for _, objectFormat := range id.SupportedObjectFormats() { t.Run(objectFormat.String(), func(t *testing.T) { t.Parallel() repo, seeded := seedHistory(t, objectFormat) stream := thinStream(t, repo, seeded) _, err := ingest.WritePack(freshRoot(t), objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ ThinBase: nil, Progress: nil, }) if !errors.Is(err, ingest.ErrThinPackNotPermitted) { t.Fatalf("err = %v, want ErrThinPackNotPermitted", err) } }) } } // TestWritePackThinMissingBase verifies that a thin pack // whose bases are absent from the thin base // reports the missing object IDs. func TestWritePackThinMissingBase(t *testing.T) { t.Parallel() for _, objectFormat := range id.SupportedObjectFormats() { t.Run(objectFormat.String(), func(t *testing.T) { t.Parallel() repo, seeded := seedHistory(t, objectFormat) emptyBase := emptyStore(t, objectFormat) stream := thinStream(t, repo, seeded) _, err := ingest.WritePack(freshRoot(t), objectFormat, bytes.NewReader(stream), store.PackWriteOptions{ ThinBase: emptyBase, Progress: nil, }) missing, ok := errors.AsType[*ingest.ThinBasesMissingError](err) if !ok { t.Fatalf("err = %v, want *ThinBasesMissingError", err) } if len(missing.OIDs) == 0 { t.Fatalf("ThinBasesMissingError reported no object IDs") } }) } } // seedHistory creates one repository with a seeded history. func seedHistory(t *testing.T, objectFormat id.ObjectFormat) (*testgit.Repo, testgit.Seeded) { t.Helper() repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) if err != nil { t.Fatalf("NewRepo: %v", err) } seeded, err := repo.SeedHistory(t) if err != nil { t.Fatalf("SeedHistory: %v", err) } return repo, seeded } // thinStream produces a thin pack of the tip commit excluding its parent, // so its deltas reference the omitted parent objects. func thinStream(t *testing.T, repo *testgit.Repo, seeded testgit.Seeded) []byte { t.Helper() tip := seeded.Commits[len(seeded.Commits)-1] parent := seeded.Commits[len(seeded.Commits)-2] stream, err := repo.PackObjectsStdout(t, []id.ObjectID{tip}, testgit.PackObjectsStdoutOptions{ Revs: true, Thin: true, Exclude: []id.ObjectID{parent}, }) if err != nil { t.Fatalf("PackObjectsStdout: %v", err) } return stream } // fullStore opens a packed store over a pack of every seeded object. func fullStore(t *testing.T, repo *testgit.Repo, objectFormat id.ObjectFormat, seeded testgit.Seeded) *packed.Packed { t.Helper() prefix, err := repo.PackObjects(t, seeded.All(), testgit.PackObjectsOptions{ RevIndex: false, Revs: false, Exclude: nil, }) if err != nil { t.Fatalf("PackObjects: %v", err) } return openStore(t, filepath.Dir(prefix), objectFormat) } // emptyStore opens a packed store over an empty directory. func emptyStore(t *testing.T, objectFormat id.ObjectFormat) *packed.Packed { t.Helper() return openStore(t, t.TempDir(), objectFormat) } // openStore opens a packed store over dir. func openStore(t *testing.T, dir string, objectFormat id.ObjectFormat) *packed.Packed { t.Helper() root, err := os.OpenRoot(dir) if err != nil { t.Fatalf("OpenRoot: %v", err) } t.Cleanup(func() { _ = root.Close() }) packedStore, err := packed.New(root, objectFormat) if err != nil { t.Fatalf("New: %v", err) } t.Cleanup(func() { _ = packedStore.Close() }) return packedStore } // freshRoot opens a writable root over a fresh temporary directory. func freshRoot(t *testing.T) *os.Root { t.Helper() root, err := os.OpenRoot(t.TempDir()) if err != nil { t.Fatalf("OpenRoot: %v", err) } t.Cleanup(func() { _ = root.Close() }) return root }