arche / internal/store/delta_test.go

commit 154431fd
  1package store_test
  2
  3import (
  4	"bytes"
  5	"path/filepath"
  6	"testing"
  7	"time"
  8
  9	"arche/internal/object"
 10	"arche/internal/store"
 11
 12	_ "github.com/mattn/go-sqlite3"
 13)
 14
 15func TestDelta_RoundTrip(t *testing.T) {
 16	base := bytes.Repeat([]byte("The quick brown fox jumps over the lazy dog\n"), 50)
 17	target := make([]byte, len(base))
 18	copy(target, base)
 19	copy(target[500:], []byte("The QUICK brown fox jumps over the LAZY dog\n"))
 20	copy(target[1000:], []byte("A completely different line right here      \n"))
 21
 22	delta := store.ComputeDelta(base, target)
 23	got, err := store.ApplyDelta(base, delta)
 24	if err != nil {
 25		t.Fatalf("ApplyDelta: %v", err)
 26	}
 27	if !bytes.Equal(got, target) {
 28		t.Error("roundtrip mismatch")
 29	}
 30}
 31
 32func TestDelta_IdenticalContent(t *testing.T) {
 33	data := bytes.Repeat([]byte("identical repeated data\n"), 30)
 34	delta := store.ComputeDelta(data, data)
 35	got, err := store.ApplyDelta(data, delta)
 36	if err != nil {
 37		t.Fatalf("ApplyDelta: %v", err)
 38	}
 39	if !bytes.Equal(got, data) {
 40		t.Error("roundtrip mismatch for identical content")
 41	}
 42}
 43
 44func TestDelta_EmptyBase(t *testing.T) {
 45	target := []byte("brand new content with no base to compare against")
 46	delta := store.ComputeDelta(nil, target)
 47	got, err := store.ApplyDelta(nil, delta)
 48	if err != nil {
 49		t.Fatalf("ApplyDelta: %v", err)
 50	}
 51	if !bytes.Equal(got, target) {
 52		t.Error("roundtrip mismatch for empty base")
 53	}
 54}
 55
 56func TestDelta_EmptyTarget(t *testing.T) {
 57	base := []byte("content that will be replaced by nothing")
 58	delta := store.ComputeDelta(base, nil)
 59	got, err := store.ApplyDelta(base, delta)
 60	if err != nil {
 61		t.Fatalf("ApplyDelta: %v", err)
 62	}
 63	if len(got) != 0 {
 64		t.Errorf("expected empty target, got %d bytes", len(got))
 65	}
 66}
 67
 68func TestDelta_SizeSavings(t *testing.T) {
 69	base := bytes.Repeat([]byte("line of content that repeats many times\n"), 256)
 70	tail := bytes.Repeat([]byte("line of content that repeats many times\n"), 250)
 71	tail = append(tail, []byte("changed ending section\n")...)
 72	target := tail
 73
 74	delta := store.ComputeDelta(base, target)
 75	if len(delta) >= len(target)/2 {
 76		t.Errorf("delta (%d B) is not significantly smaller than target (%d B)", len(delta), len(target))
 77	}
 78}
 79
 80func openPackStore(t *testing.T) *store.SQLiteStore {
 81	t.Helper()
 82	dir := t.TempDir()
 83	s, err := store.OpenSQLiteStore(
 84		filepath.Join(dir, "store.db"),
 85		filepath.Join(dir, "packs"),
 86		1,
 87		0,
 88		"zstd",
 89	)
 90	if err != nil {
 91		t.Fatalf("OpenSQLiteStore: %v", err)
 92	}
 93	t.Cleanup(func() { s.Close() })
 94	return s
 95}
 96
 97func TestGCRepackWithDelta(t *testing.T) {
 98	s := openPackStore(t)
 99
100	base := bytes.Repeat([]byte("pack file content line repeating many times over\n"), 20)
101	variant := make([]byte, len(base))
102	copy(variant, base)
103	copy(variant[len(variant)-50:], bytes.Repeat([]byte("X"), 50))
104
105	encodeBlob := func(data []byte) ([32]byte, []byte) {
106		id := object.HashBlob(&object.Blob{Content: data})
107		var buf bytes.Buffer
108		object.EncodeBlob(&buf, &object.Blob{Content: data})
109		return id, buf.Bytes()
110	}
111
112	blobID1, rawBlob1 := encodeBlob(base)
113	blobID2, rawBlob2 := encodeBlob(variant)
114
115	tree := &object.Tree{Entries: []object.TreeEntry{
116		{Name: "file1.txt", Mode: object.ModeFile, ObjectID: blobID1},
117		{Name: "file2.txt", Mode: object.ModeFile, ObjectID: blobID2},
118	}}
119	treeID := object.HashTree(tree)
120	var treeBuf bytes.Buffer
121	object.EncodeTree(&treeBuf, tree)
122
123	sig := object.Signature{Name: "Test", Email: "t@x.com", Timestamp: time.Now()}
124	commit := &object.Commit{
125		TreeID:    treeID,
126		Author:    sig,
127		Committer: sig,
128		Message:   "gc repack delta test",
129	}
130	commitID := object.HashCommit(commit)
131	var commitBuf bytes.Buffer
132	object.EncodeCommit(&commitBuf, commit)
133
134	tx, err := s.Begin()
135	if err != nil {
136		t.Fatalf("Begin: %v", err)
137	}
138	for _, pair := range []struct {
139		id   [32]byte
140		kind string
141		raw  []byte
142	}{
143		{blobID1, "blob", rawBlob1},
144		{blobID2, "blob", rawBlob2},
145		{treeID, "tree", treeBuf.Bytes()},
146		{commitID, "commit", commitBuf.Bytes()},
147	} {
148		if err := s.WriteObject(tx, pair.id, pair.kind, pair.raw); err != nil {
149			t.Fatalf("WriteObject %s: %v", pair.kind, err)
150		}
151	}
152	if err := s.Commit(tx); err != nil {
153		t.Fatalf("Commit: %v", err)
154	}
155
156	btx, _ := s.Begin()
157	if err := s.SetBookmark(btx, store.Bookmark{Name: "main", CommitID: commitID}); err != nil {
158		t.Fatalf("SetBookmark: %v", err)
159	}
160	s.Commit(btx) //nolint:errcheck
161
162	if _, err := s.GC(90, func(string, int, int) {}); err != nil {
163		t.Fatalf("GC: %v", err)
164	}
165
166	_, got1, err := s.ReadObject(blobID1)
167	if err != nil {
168		t.Fatalf("ReadObject blob1 after GC: %v", err)
169	}
170	_, got2, err := s.ReadObject(blobID2)
171	if err != nil {
172		t.Fatalf("ReadObject blob2 after GC: %v", err)
173	}
174
175	b1, err := object.DecodeBlob(got1)
176	if err != nil {
177		t.Fatalf("DecodeBlob1: %v", err)
178	}
179	b2, err := object.DecodeBlob(got2)
180	if err != nil {
181		t.Fatalf("DecodeBlob2: %v", err)
182	}
183	if !bytes.Equal(b1.Content, base) {
184		t.Error("blob1 content mismatch after GC repack")
185	}
186	if !bytes.Equal(b2.Content, variant) {
187		t.Error("blob2 content mismatch after GC repack")
188	}
189}