1package store_test
2
3import (
4 "bytes"
5 "path/filepath"
6 "testing"
7
8 "arche/internal/object"
9 "arche/internal/store"
10
11 _ "github.com/mattn/go-sqlite3"
12)
13
14func openTestStore(t *testing.T) *store.SQLiteStore {
15 t.Helper()
16 dir := t.TempDir()
17 packDir := filepath.Join(dir, "packs")
18 s, err := store.OpenSQLiteStore(filepath.Join(dir, "store.db"), packDir, 0, 0, "zstd")
19 if err != nil {
20 t.Fatalf("OpenSQLiteStore: %v", err)
21 }
22 t.Cleanup(func() { s.Close() })
23 return s
24}
25
26func TestStore_WriteReadObject(t *testing.T) {
27 s := openTestStore(t)
28
29 data := []byte("hello world")
30 id := object.HashBlob(&object.Blob{Content: data})
31 var buf bytes.Buffer
32 object.EncodeBlob(&buf, &object.Blob{Content: data})
33 raw := buf.Bytes()
34
35 tx, err := s.Begin()
36 if err != nil {
37 t.Fatalf("Begin: %v", err)
38 }
39 if err := s.WriteObject(tx, id, "blob", raw); err != nil {
40 t.Fatalf("WriteObject: %v", err)
41 }
42 if err := s.Commit(tx); err != nil {
43 t.Fatalf("Commit: %v", err)
44 }
45
46 kind, got, err := s.ReadObject(id)
47 if err != nil {
48 t.Fatalf("ReadObject: %v", err)
49 }
50 if kind != "blob" {
51 t.Errorf("kind: want blob got %q", kind)
52 }
53 if string(got) != string(raw) {
54 t.Errorf("data mismatch")
55 }
56}
57
58func TestStore_HasObject(t *testing.T) {
59 s := openTestStore(t)
60
61 data := []byte("content")
62 id := object.HashBlob(&object.Blob{Content: data})
63 var rawBuf bytes.Buffer
64 object.EncodeBlob(&rawBuf, &object.Blob{Content: data})
65 raw := rawBuf.Bytes()
66
67 ok, err := s.HasObject(id)
68 if err != nil || ok {
69 t.Errorf("should not exist before write; has=%v err=%v", ok, err)
70 }
71
72 tx, _ := s.Begin()
73 s.WriteObject(tx, id, "blob", raw) //nolint:errcheck
74 s.Commit(tx) //nolint:errcheck
75
76 ok, err = s.HasObject(id)
77 if err != nil || !ok {
78 t.Errorf("should exist after write; has=%v err=%v", ok, err)
79 }
80}
81
82func TestStore_WriteObject_Idempotent(t *testing.T) {
83 s := openTestStore(t)
84 var rawBuf2 bytes.Buffer
85 object.EncodeBlob(&rawBuf2, &object.Blob{Content: []byte("dup")})
86 raw := rawBuf2.Bytes()
87 id := object.HashBlob(&object.Blob{Content: []byte("dup")})
88
89 for range 3 {
90 tx, _ := s.Begin()
91 if err := s.WriteObject(tx, id, "blob", raw); err != nil {
92 t.Fatalf("WriteObject iteration: %v", err)
93 }
94 s.Commit(tx) //nolint:errcheck
95 }
96
97 _, got, err := s.ReadObject(id)
98 if err != nil {
99 t.Fatalf("ReadObject: %v", err)
100 }
101 if !bytes.Equal(got, raw) {
102 t.Error("data corrupted after duplicate writes")
103 }
104}
105
106func TestStore_Bookmarks(t *testing.T) {
107 s := openTestStore(t)
108
109 var bmBuf bytes.Buffer
110 object.EncodeBlob(&bmBuf, &object.Blob{Content: []byte("bookmark-obj")})
111 id := object.HashBlob(&object.Blob{Content: []byte("bookmark-obj")})
112 txObj, _ := s.Begin()
113 s.WriteObject(txObj, id, "blob", bmBuf.Bytes()) //nolint:errcheck
114 s.Commit(txObj) //nolint:errcheck
115
116 tx, _ := s.Begin()
117 err := s.SetBookmark(tx, store.Bookmark{Name: "main", CommitID: id})
118 if err != nil {
119 t.Fatalf("SetBookmark: %v", err)
120 }
121 s.Commit(tx) //nolint:errcheck
122
123 got, err := s.GetBookmark("main")
124 if err != nil || got == nil {
125 t.Fatalf("GetBookmark: %v", err)
126 }
127 if got.CommitID != id {
128 t.Errorf("commit ID mismatch")
129 }
130
131 bms, err := s.ListBookmarks()
132 if err != nil || len(bms) != 1 {
133 t.Errorf("ListBookmarks: want 1, got %d err=%v", len(bms), err)
134 }
135
136 tx2, _ := s.Begin()
137 s.DeleteBookmark(tx2, "main") //nolint:errcheck
138 s.Commit(tx2) //nolint:errcheck
139
140 bms, _ = s.ListBookmarks()
141 if len(bms) != 0 {
142 t.Errorf("after delete want 0 bookmarks, got %d", len(bms))
143 }
144}
145
146func TestStore_Phase(t *testing.T) {
147 s := openTestStore(t)
148
149 var id [32]byte
150 id[0] = 0x01
151
152 var phaseBuf bytes.Buffer
153 object.EncodeBlob(&phaseBuf, &object.Blob{Content: []byte("x")})
154 tx, _ := s.Begin()
155 s.WriteObject(tx, id, "blob", phaseBuf.Bytes()) //nolint:errcheck
156 s.Commit(tx) //nolint:errcheck
157
158 phase, _ := s.GetPhase(id)
159 _ = phase
160
161 tx2, _ := s.Begin()
162 if err := s.SetPhase(tx2, id, object.PhasePublic); err != nil {
163 t.Fatalf("SetPhase: %v", err)
164 }
165 s.Commit(tx2) //nolint:errcheck
166
167 phase, err := s.GetPhase(id)
168 if err != nil {
169 t.Fatalf("GetPhase: %v", err)
170 }
171 if phase != object.PhasePublic {
172 t.Errorf("phase: want Public got %v", phase)
173 }
174}
175
176func TestStore_OperationLog(t *testing.T) {
177 s := openTestStore(t)
178
179 tx, _ := s.Begin()
180 _, err := s.InsertOperation(tx, store.Operation{
181 Kind: "snap",
182 Timestamp: 1000,
183 Before: `{"head":"old"}`,
184 After: `{"head":"new"}`,
185 })
186 if err != nil {
187 t.Fatalf("InsertOperation: %v", err)
188 }
189 s.Commit(tx) //nolint:errcheck
190
191 ops, err := s.ListOperations(10)
192 if err != nil || len(ops) == 0 {
193 t.Errorf("ListOperations: want >=1, got %d err=%v", len(ops), err)
194 }
195
196 last, err := s.GetLastOperation()
197 if err != nil || last == nil {
198 t.Fatalf("GetLastOperation: %v", err)
199 }
200 if last.Kind != "snap" {
201 t.Errorf("last op kind: want snap got %q", last.Kind)
202 }
203}
204
205func TestStore_WCache(t *testing.T) {
206 s := openTestStore(t)
207
208 e := store.WCacheEntry{
209 Path: "src/main.go",
210 Inode: 42,
211 MtimeNs: 1234567890,
212 Size: 512,
213 }
214
215 tx, _ := s.Begin()
216 if err := s.SetWCacheEntry(tx, e); err != nil {
217 t.Fatalf("SetWCacheEntry: %v", err)
218 }
219 s.Commit(tx) //nolint:errcheck
220
221 got, err := s.GetWCacheEntry("src/main.go")
222 if err != nil || got == nil {
223 t.Fatalf("GetWCacheEntry: %v", err)
224 }
225 if got.Inode != 42 || got.Size != 512 || got.MtimeNs != 1234567890 {
226 t.Errorf("entry mismatch: %+v", got)
227 }
228
229 entries, err := s.ListWCacheEntries()
230 if err != nil || len(entries) != 1 {
231 t.Errorf("ListWCacheEntries: want 1, got %d err=%v", len(entries), err)
232 }
233
234 tx2, _ := s.Begin()
235 s.DeleteWCacheEntry(tx2, "src/main.go") //nolint:errcheck
236 s.Commit(tx2) //nolint:errcheck
237
238 entries, _ = s.ListWCacheEntries()
239 if len(entries) != 0 {
240 t.Errorf("after delete want 0 entries, got %d", len(entries))
241 }
242}
243
244func TestStore_ChangeCommit(t *testing.T) {
245 s := openTestStore(t)
246
247 var ccBuf bytes.Buffer
248 object.EncodeBlob(&ccBuf, &object.Blob{Content: []byte("change-obj")})
249 commitID := object.HashBlob(&object.Blob{Content: []byte("change-obj")})
250 txObj, _ := s.Begin()
251 s.WriteObject(txObj, commitID, "blob", ccBuf.Bytes()) //nolint:errcheck
252 s.Commit(txObj) //nolint:errcheck
253
254 tx, _ := s.Begin()
255 changeID, err := s.AllocChangeID(tx)
256 if err != nil {
257 t.Fatalf("AllocChangeID: %v", err)
258 }
259 if err := s.SetChangeCommit(tx, changeID, commitID); err != nil {
260 t.Fatalf("SetChangeCommit: %v", err)
261 }
262 s.Commit(tx) //nolint:errcheck
263
264 got, err := s.GetChangeCommit(changeID)
265 if err != nil {
266 t.Fatalf("GetChangeCommit: %v", err)
267 }
268 if got != commitID {
269 t.Errorf("commit ID mismatch")
270 }
271}