1package object_test
2
3import (
4 "bytes"
5 "testing"
6 "time"
7
8 "arche/internal/object"
9)
10
11func TestBlobRoundtrip(t *testing.T) {
12 b := &object.Blob{Content: []byte("hello Arche!\n")}
13 var buf bytes.Buffer
14 object.EncodeBlob(&buf, b)
15 got, err := object.DecodeBlob(buf.Bytes())
16 if err != nil {
17 t.Fatalf("DecodeBlob: %v", err)
18 }
19 if !bytes.Equal(got.Content, b.Content) {
20 t.Errorf("content mismatch: got %q, want %q", got.Content, b.Content)
21 }
22}
23
24func TestBlobEmpty(t *testing.T) {
25 b := &object.Blob{Content: []byte{}}
26 var buf bytes.Buffer
27 object.EncodeBlob(&buf, b)
28 got, err := object.DecodeBlob(buf.Bytes())
29 if err != nil {
30 t.Fatalf("DecodeBlob: %v", err)
31 }
32 if !bytes.Equal(got.Content, b.Content) {
33 t.Error("empty blob roundtrip failed")
34 }
35}
36
37func TestBlobHashDeterministic(t *testing.T) {
38 b := &object.Blob{Content: []byte("deterministic")}
39 h1 := object.HashBlob(b)
40 h2 := object.HashBlob(b)
41 if h1 != h2 {
42 t.Error("blob hash is non-deterministic")
43 }
44}
45
46func TestBlobDifferentContentDifferentHash(t *testing.T) {
47 h1 := object.HashBlob(&object.Blob{Content: []byte("a")})
48 h2 := object.HashBlob(&object.Blob{Content: []byte("b")})
49 if h1 == h2 {
50 t.Error("different blobs have same hash")
51 }
52}
53
54func TestBlobNullBytes(t *testing.T) {
55 b := &object.Blob{Content: []byte{0x00, 0x01, 0x02, 0xFF}}
56 var buf bytes.Buffer
57 object.EncodeBlob(&buf, b)
58 got, err := object.DecodeBlob(buf.Bytes())
59 if err != nil {
60 t.Fatalf("DecodeBlob: %v", err)
61 }
62 if !bytes.Equal(got.Content, b.Content) {
63 t.Error("binary content mismatch")
64 }
65}
66
67func TestTreeRoundtrip(t *testing.T) {
68 var id1, id2 [32]byte
69 id1[0] = 0xAA
70 id2[0] = 0xBB
71
72 tree := &object.Tree{
73 Entries: []object.TreeEntry{
74 {Name: "zebra.txt", Mode: object.ModeFile, ObjectID: id2},
75 {
76 Name: "alpha.go",
77 Mode: object.ModeExec,
78 ObjectID: id1,
79 Props: map[string]string{"svn:eol-style": "LF"},
80 },
81 },
82 }
83 var buf bytes.Buffer
84 object.EncodeTree(&buf, tree)
85 got, err := object.DecodeTree(buf.Bytes())
86 if err != nil {
87 t.Fatalf("DecodeTree: %v", err)
88 }
89 if len(got.Entries) != 2 {
90 t.Fatalf("entries: got %d, want 2", len(got.Entries))
91 }
92 if got.Entries[0].Name != "alpha.go" {
93 t.Errorf("first entry: got %q, want alpha.go", got.Entries[0].Name)
94 }
95 if got.Entries[1].Name != "zebra.txt" {
96 t.Errorf("second entry: got %q, want zebra.txt", got.Entries[1].Name)
97 }
98 if got.Entries[0].ObjectID != id1 {
99 t.Error("ObjectID mismatch for alpha.go")
100 }
101 if got.Entries[0].Props["svn:eol-style"] != "LF" {
102 t.Errorf("prop mismatch: got %q", got.Entries[0].Props["svn:eol-style"])
103 }
104}
105
106func TestTreeEmpty(t *testing.T) {
107 tree := &object.Tree{Entries: nil}
108 var buf bytes.Buffer
109 object.EncodeTree(&buf, tree)
110 got, err := object.DecodeTree(buf.Bytes())
111 if err != nil {
112 t.Fatalf("DecodeTree: %v", err)
113 }
114 if len(got.Entries) != 0 {
115 t.Errorf("expected empty tree, got %d entries", len(got.Entries))
116 }
117}
118
119func TestTreeHashDeterministic(t *testing.T) {
120 var id [32]byte
121 id[0] = 0x42
122 tree := &object.Tree{
123 Entries: []object.TreeEntry{
124 {Name: "file.txt", Mode: object.ModeFile, ObjectID: id},
125 },
126 }
127 h1 := object.HashTree(tree)
128 h2 := object.HashTree(tree)
129 if h1 != h2 {
130 t.Error("tree hash is non-deterministic")
131 }
132}
133
134func TestTreeSortedEntriesProduceSameHash(t *testing.T) {
135 var id1, id2 [32]byte
136 id1[0] = 1
137 id2[0] = 2
138 t1 := &object.Tree{Entries: []object.TreeEntry{
139 {Name: "a.go", Mode: object.ModeFile, ObjectID: id1},
140 {Name: "b.go", Mode: object.ModeFile, ObjectID: id2},
141 }}
142 t2 := &object.Tree{Entries: []object.TreeEntry{
143 {Name: "b.go", Mode: object.ModeFile, ObjectID: id2},
144 {Name: "a.go", Mode: object.ModeFile, ObjectID: id1},
145 }}
146 if object.HashTree(t1) != object.HashTree(t2) {
147 t.Error("trees with same entries in different order should produce same hash (encoder sorts)")
148 }
149}
150
151func TestTreeWithSubdir(t *testing.T) {
152 var fileID [32]byte
153 fileID[0] = 0xCC
154 tree := &object.Tree{
155 Entries: []object.TreeEntry{
156 {Name: "src", Mode: object.ModeDir, ObjectID: fileID},
157 {Name: "README.md", Mode: object.ModeFile, ObjectID: fileID},
158 },
159 }
160 var buf bytes.Buffer
161 object.EncodeTree(&buf, tree)
162 got, err := object.DecodeTree(buf.Bytes())
163 if err != nil {
164 t.Fatalf("DecodeTree: %v", err)
165 }
166 if len(got.Entries) != 2 {
167 t.Fatalf("expected 2 entries, got %d", len(got.Entries))
168 }
169}
170
171func TestCommitRoundtrip(t *testing.T) {
172 var treeID [32]byte
173 treeID[0] = 0x11
174 now := time.Unix(1700000000, 0).UTC()
175 c := &object.Commit{
176 TreeID: treeID,
177 Parents: nil,
178 ChangeID: "kptxoyvr",
179 Author: object.Signature{Name: "Alice", Email: "alice@example.com", Timestamp: now},
180 Committer: object.Signature{Name: "Bob", Email: "bob@example.com", Timestamp: now},
181 Message: "initial commit",
182 Phase: object.PhaseDraft,
183 }
184 var buf bytes.Buffer
185 object.EncodeCommit(&buf, c)
186 got, err := object.DecodeCommit(buf.Bytes())
187 if err != nil {
188 t.Fatalf("DecodeCommit: %v", err)
189 }
190 if got.TreeID != c.TreeID {
191 t.Error("TreeID mismatch")
192 }
193 if got.ChangeID != c.ChangeID {
194 t.Errorf("ChangeID: got %q, want %q", got.ChangeID, c.ChangeID)
195 }
196 if got.Author.Name != c.Author.Name {
197 t.Errorf("Author.Name: got %q, want %q", got.Author.Name, c.Author.Name)
198 }
199 if got.Author.Email != c.Author.Email {
200 t.Errorf("Author.Email: got %q, want %q", got.Author.Email, c.Author.Email)
201 }
202 if !got.Author.Timestamp.Equal(c.Author.Timestamp) {
203 t.Errorf("Author.Timestamp: got %v, want %v", got.Author.Timestamp, c.Author.Timestamp)
204 }
205 if got.Message != c.Message {
206 t.Errorf("Message: got %q, want %q", got.Message, c.Message)
207 }
208 if got.Phase != c.Phase {
209 t.Errorf("Phase: got %v, want %v", got.Phase, c.Phase)
210 }
211}
212
213func TestCommitWithParents(t *testing.T) {
214 var p1, p2 [32]byte
215 p1[0] = 1
216 p2[0] = 2
217 now := time.Unix(1700000000, 0).UTC()
218 c := &object.Commit{
219 Parents: [][32]byte{p1, p2},
220 ChangeID: "abcdefgh",
221 Author: object.Signature{Name: "Test", Email: "t@t.com", Timestamp: now},
222 Committer: object.Signature{Name: "Test", Email: "t@t.com", Timestamp: now},
223 Phase: object.PhasePublic,
224 }
225 var buf bytes.Buffer
226 object.EncodeCommit(&buf, c)
227 got, err := object.DecodeCommit(buf.Bytes())
228 if err != nil {
229 t.Fatalf("DecodeCommit: %v", err)
230 }
231 if len(got.Parents) != 2 {
232 t.Fatalf("parents: got %d, want 2", len(got.Parents))
233 }
234 if got.Parents[0] != p1 || got.Parents[1] != p2 {
235 t.Error("parent IDs mismatch")
236 }
237}
238
239func TestCommitHashDeterministic(t *testing.T) {
240 now := time.Unix(1700000000, 0).UTC()
241 c := &object.Commit{
242 ChangeID: "aaaaaaaa",
243 Author: object.Signature{Name: "X", Email: "x@x.com", Timestamp: now},
244 Committer: object.Signature{Name: "X", Email: "x@x.com", Timestamp: now},
245 Message: "msg",
246 Phase: object.PhaseDraft,
247 }
248 h1 := object.HashCommit(c)
249 h2 := object.HashCommit(c)
250 if h1 != h2 {
251 t.Error("commit hash non-deterministic")
252 }
253}
254
255func TestObsoleteMarkerRoundtrip(t *testing.T) {
256 var pred, succ [32]byte
257 pred[0] = 0x01
258 succ[0] = 0x02
259 o := &object.ObsoleteMarker{
260 Predecessor: pred,
261 Successors: [][32]byte{succ},
262 Reason: "rebase",
263 Timestamp: 1700000000,
264 }
265 var buf bytes.Buffer
266 object.EncodeObsolete(&buf, o)
267 got, err := object.DecodeObsolete(buf.Bytes())
268 if err != nil {
269 t.Fatalf("DecodeObsolete: %v", err)
270 }
271 if got.Predecessor != o.Predecessor {
272 t.Error("Predecessor mismatch")
273 }
274 if len(got.Successors) != 1 || got.Successors[0] != succ {
275 t.Error("Successors mismatch")
276 }
277 if got.Reason != o.Reason {
278 t.Errorf("Reason: got %q, want %q", got.Reason, o.Reason)
279 }
280 if got.Timestamp != o.Timestamp {
281 t.Errorf("Timestamp: got %d, want %d", got.Timestamp, o.Timestamp)
282 }
283}
284
285func TestObsoleteMarkerMultipleSuccessors(t *testing.T) {
286 var pred, s1, s2 [32]byte
287 pred[0] = 1
288 s1[0] = 2
289 s2[0] = 3
290 o := &object.ObsoleteMarker{
291 Predecessor: pred,
292 Successors: [][32]byte{s1, s2},
293 Reason: "split",
294 Timestamp: 0,
295 }
296 var buf bytes.Buffer
297 object.EncodeObsolete(&buf, o)
298 got, err := object.DecodeObsolete(buf.Bytes())
299 if err != nil {
300 t.Fatalf("DecodeObsolete: %v", err)
301 }
302 if len(got.Successors) != 2 {
303 t.Fatalf("successors: got %d, want 2", len(got.Successors))
304 }
305}
306
307func TestConflictRoundtrip(t *testing.T) {
308 var c1, c2, b1, b2 [32]byte
309 c1[0] = 1
310 c2[0] = 2
311 b1[0] = 3
312 b2[0] = 4
313 conf := &object.Conflict{
314 Ours: object.ConflictSide{CommitID: c1, BlobID: b1},
315 Theirs: object.ConflictSide{CommitID: c2, BlobID: b2},
316 }
317 var buf bytes.Buffer
318 object.EncodeConflict(&buf, conf)
319 got, err := object.DecodeConflict(buf.Bytes())
320 if err != nil {
321 t.Fatalf("DecodeConflict: %v", err)
322 }
323 if got.Base != nil {
324 t.Error("expected nil Base")
325 }
326 if got.Ours.CommitID != conf.Ours.CommitID {
327 t.Error("Ours.CommitID mismatch")
328 }
329 if got.Theirs.BlobID != conf.Theirs.BlobID {
330 t.Error("Theirs.BlobID mismatch")
331 }
332}
333
334func TestConflictWithBase(t *testing.T) {
335 var cb, bb [32]byte
336 cb[0] = 3
337 bb[0] = 6
338 conf := &object.Conflict{
339 Base: &object.ConflictSide{CommitID: cb, BlobID: bb},
340 Ours: object.ConflictSide{},
341 Theirs: object.ConflictSide{},
342 }
343 var buf bytes.Buffer
344 object.EncodeConflict(&buf, conf)
345 got, err := object.DecodeConflict(buf.Bytes())
346 if err != nil {
347 t.Fatalf("DecodeConflict: %v", err)
348 }
349 if got.Base == nil {
350 t.Fatal("expected non-nil Base")
351 }
352 if got.Base.CommitID != cb {
353 t.Error("Base.CommitID mismatch")
354 }
355 if got.Base.BlobID != bb {
356 t.Error("Base.BlobID mismatch")
357 }
358}
359
360func TestNewChangeIDLength(t *testing.T) {
361 for _, n := range []int{8, 10, 12} {
362 id := object.NewChangeID(n)
363 if len(id) != n {
364 t.Errorf("NewChangeID(%d): got length %d", n, len(id))
365 }
366 }
367}
368
369func TestNewChangeIDAlphabet(t *testing.T) {
370 const alphabet = "abcdefghjkmnpqrstvwxyz"
371 id := object.NewChangeID(8)
372 for _, c := range id {
373 found := false
374 for _, a := range alphabet {
375 if c == a {
376 found = true
377 break
378 }
379 }
380 if !found {
381 t.Errorf("character %q not in unambiguous alphabet", c)
382 }
383 }
384}
385
386func TestFormatStripChangeID(t *testing.T) {
387 raw := "kptxoyvr"
388 formatted := object.FormatChangeID(raw)
389 if formatted != "ch:kptxoyvr" {
390 t.Errorf("FormatChangeID: got %q, want %q", formatted, "ch:kptxoyvr")
391 }
392 stripped := object.StripChangeIDPrefix(formatted)
393 if stripped != raw {
394 t.Errorf("StripChangeIDPrefix: got %q, want %q", stripped, raw)
395 }
396 stripped2 := object.StripChangeIDPrefix(raw)
397 if stripped2 != raw {
398 t.Errorf("StripChangeIDPrefix (no prefix): got %q, want %q", stripped2, raw)
399 }
400}
401
402func TestPhaseString(t *testing.T) {
403 cases := []struct {
404 phase object.Phase
405 want string
406 }{
407 {object.PhaseDraft, "draft"},
408 {object.PhasePublic, "public"},
409 {object.PhaseSecret, "secret"},
410 {object.Phase(99), "unknown"},
411 }
412 for _, tc := range cases {
413 if got := tc.phase.String(); got != tc.want {
414 t.Errorf("Phase(%d).String(): got %q, want %q", tc.phase, got, tc.want)
415 }
416 }
417}