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}