arche / internal/object/encode_test.go

commit 154431fd
  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}