entity_actions_test.go

  1package dag
  2
  3import (
  4	"sort"
  5	"strings"
  6	"testing"
  7
  8	"github.com/stretchr/testify/require"
  9
 10	"github.com/git-bug/git-bug/entity"
 11	"github.com/git-bug/git-bug/repository"
 12)
 13
 14func allEntities(t testing.TB, bugs <-chan entity.StreamedEntity[*Foo]) []*Foo {
 15	t.Helper()
 16
 17	var result []*Foo
 18	for streamed := range bugs {
 19		require.NoError(t, streamed.Err)
 20
 21		result = append(result, streamed.Entity)
 22	}
 23	return result
 24}
 25
 26func TestEntityPushPull(t *testing.T) {
 27	repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
 28
 29	// A --> remote --> B
 30	e := New(def)
 31	e.Append(newOp1(id1, "foo"))
 32
 33	err := e.Commit(repoA)
 34	require.NoError(t, err)
 35
 36	_, err = Push(def, repoA, "remote")
 37	require.NoError(t, err)
 38
 39	err = Pull(def, wrapper, repoB, resolvers, "remote", id1)
 40	require.NoError(t, err)
 41
 42	entities := allEntities(t, ReadAll(def, wrapper, repoB, resolvers))
 43	require.Len(t, entities, 1)
 44
 45	// B --> remote --> A
 46	e = New(def)
 47	e.Append(newOp2(id2, "bar"))
 48
 49	err = e.Commit(repoB)
 50	require.NoError(t, err)
 51
 52	_, err = Push(def, repoB, "remote")
 53	require.NoError(t, err)
 54
 55	err = Pull(def, wrapper, repoA, resolvers, "remote", id1)
 56	require.NoError(t, err)
 57
 58	entities = allEntities(t, ReadAll(def, wrapper, repoB, resolvers))
 59	require.Len(t, entities, 2)
 60}
 61
 62func TestListLocalIds(t *testing.T) {
 63	repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
 64
 65	// A --> remote --> B
 66	e := New(def)
 67	e.Append(newOp1(id1, "foo"))
 68	err := e.Commit(repoA)
 69	require.NoError(t, err)
 70
 71	e = New(def)
 72	e.Append(newOp2(id2, "bar"))
 73	err = e.Commit(repoA)
 74	require.NoError(t, err)
 75
 76	listLocalIds(t, def, repoA, 2)
 77	listLocalIds(t, def, repoB, 0)
 78
 79	_, err = Push(def, repoA, "remote")
 80	require.NoError(t, err)
 81
 82	_, err = Fetch(def, repoB, "remote")
 83	require.NoError(t, err)
 84
 85	listLocalIds(t, def, repoA, 2)
 86	listLocalIds(t, def, repoB, 0)
 87
 88	err = Pull(def, wrapper, repoB, resolvers, "remote", id1)
 89	require.NoError(t, err)
 90
 91	listLocalIds(t, def, repoA, 2)
 92	listLocalIds(t, def, repoB, 2)
 93}
 94
 95func listLocalIds(t *testing.T, def Definition, repo repository.RepoData, expectedCount int) {
 96	ids, err := ListLocalIds(def, repo)
 97	require.NoError(t, err)
 98	require.Len(t, ids, expectedCount)
 99}
100
101func assertMergeResults(t *testing.T, expected []entity.MergeResult, results <-chan entity.MergeResult) {
102	t.Helper()
103
104	var allResults []entity.MergeResult
105	for result := range results {
106		allResults = append(allResults, result)
107	}
108
109	require.Equal(t, len(expected), len(allResults))
110
111	sort.Slice(allResults, func(i, j int) bool {
112		return allResults[i].Id < allResults[j].Id
113	})
114	sort.Slice(expected, func(i, j int) bool {
115		return expected[i].Id < expected[j].Id
116	})
117
118	for i, result := range allResults {
119		require.NoError(t, result.Err)
120
121		require.Equal(t, expected[i].Id, result.Id)
122		require.Equal(t, expected[i].Status, result.Status)
123
124		switch result.Status {
125		case entity.MergeStatusNew, entity.MergeStatusUpdated:
126			require.NotNil(t, result.Entity)
127			require.Equal(t, expected[i].Id, result.Entity.Id())
128		}
129
130		i++
131	}
132}
133
134func assertEqualRefs(t *testing.T, repoA, repoB repository.RepoData, prefix string) {
135	t.Helper()
136
137	refsA, err := repoA.ListRefs("")
138	require.NoError(t, err)
139
140	var refsAFiltered []string
141	for _, ref := range refsA {
142		if strings.HasPrefix(ref, prefix) {
143			refsAFiltered = append(refsAFiltered, ref)
144		}
145	}
146
147	refsB, err := repoB.ListRefs("")
148	require.NoError(t, err)
149
150	var refsBFiltered []string
151	for _, ref := range refsB {
152		if strings.HasPrefix(ref, prefix) {
153			refsBFiltered = append(refsBFiltered, ref)
154		}
155	}
156
157	require.NotEmpty(t, refsAFiltered)
158	require.Equal(t, refsAFiltered, refsBFiltered)
159
160	for _, ref := range refsAFiltered {
161		commitA, err := repoA.ResolveRef(ref)
162		require.NoError(t, err)
163		commitB, err := repoB.ResolveRef(ref)
164		require.NoError(t, err)
165
166		require.Equal(t, commitA, commitB)
167	}
168}
169
170func assertNotEqualRefs(t *testing.T, repoA, repoB repository.RepoData, prefix string) {
171	t.Helper()
172
173	refsA, err := repoA.ListRefs("")
174	require.NoError(t, err)
175
176	var refsAFiltered []string
177	for _, ref := range refsA {
178		if strings.HasPrefix(ref, prefix) {
179			refsAFiltered = append(refsAFiltered, ref)
180		}
181	}
182
183	refsB, err := repoB.ListRefs("")
184	require.NoError(t, err)
185
186	var refsBFiltered []string
187	for _, ref := range refsB {
188		if strings.HasPrefix(ref, prefix) {
189			refsBFiltered = append(refsBFiltered, ref)
190		}
191	}
192
193	require.NotEmpty(t, refsAFiltered)
194	require.Equal(t, refsAFiltered, refsBFiltered)
195
196	for _, ref := range refsAFiltered {
197		commitA, err := repoA.ResolveRef(ref)
198		require.NoError(t, err)
199		commitB, err := repoB.ResolveRef(ref)
200		require.NoError(t, err)
201
202		require.NotEqual(t, commitA, commitB)
203	}
204}
205
206func TestMerge(t *testing.T) {
207	repoA, repoB, _, id1, id2, resolvers, def := makeTestContextRemote(t)
208
209	// SCENARIO 1
210	// if the remote Entity doesn't exist locally, it's created
211
212	// 2 entities in repoA + push to remote
213	e1A := New(def)
214	e1A.Append(newOp1(id1, "foo"))
215	err := e1A.Commit(repoA)
216	require.NoError(t, err)
217
218	e2A := New(def)
219	e2A.Append(newOp2(id2, "bar"))
220	err = e2A.Commit(repoA)
221	require.NoError(t, err)
222
223	_, err = Push(def, repoA, "remote")
224	require.NoError(t, err)
225
226	// repoB: fetch + merge from remote
227
228	_, err = Fetch(def, repoB, "remote")
229	require.NoError(t, err)
230
231	results := MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
232
233	assertMergeResults(t, []entity.MergeResult{
234		{
235			Id:     e1A.Id(),
236			Status: entity.MergeStatusNew,
237		},
238		{
239			Id:     e2A.Id(),
240			Status: entity.MergeStatusNew,
241		},
242	}, results)
243
244	assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
245
246	// SCENARIO 2
247	// if the remote and local Entity have the same state, nothing is changed
248
249	results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
250
251	assertMergeResults(t, []entity.MergeResult{
252		{
253			Id:     e1A.Id(),
254			Status: entity.MergeStatusNothing,
255		},
256		{
257			Id:     e2A.Id(),
258			Status: entity.MergeStatusNothing,
259		},
260	}, results)
261
262	assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
263
264	// SCENARIO 3
265	// if the local Entity has new commits but the remote don't, nothing is changed
266
267	e1A.Append(newOp1(id1, "barbar"))
268	err = e1A.Commit(repoA)
269	require.NoError(t, err)
270
271	e2A.Append(newOp2(id2, "barbarbar"))
272	err = e2A.Commit(repoA)
273	require.NoError(t, err)
274
275	results = MergeAll(def, wrapper, repoA, resolvers, "remote", id1)
276
277	assertMergeResults(t, []entity.MergeResult{
278		{
279			Id:     e1A.Id(),
280			Status: entity.MergeStatusNothing,
281		},
282		{
283			Id:     e2A.Id(),
284			Status: entity.MergeStatusNothing,
285		},
286	}, results)
287
288	assertNotEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
289
290	// SCENARIO 4
291	// if the remote has new commit, the local bug is updated to match the same history
292	// (fast-forward update)
293
294	_, err = Push(def, repoA, "remote")
295	require.NoError(t, err)
296
297	_, err = Fetch(def, repoB, "remote")
298	require.NoError(t, err)
299
300	results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
301
302	assertMergeResults(t, []entity.MergeResult{
303		{
304			Id:     e1A.Id(),
305			Status: entity.MergeStatusUpdated,
306		},
307		{
308			Id:     e2A.Id(),
309			Status: entity.MergeStatusUpdated,
310		},
311	}, results)
312
313	assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
314
315	// SCENARIO 5
316	// if both local and remote Entity have new commits (that is, we have a concurrent edition),
317	// a merge commit with an empty operationPack is created to join both branch and form a DAG.
318
319	e1A.Append(newOp1(id1, "barbarfoo"))
320	err = e1A.Commit(repoA)
321	require.NoError(t, err)
322
323	e2A.Append(newOp2(id2, "barbarbarfoo"))
324	err = e2A.Commit(repoA)
325	require.NoError(t, err)
326
327	e1B, err := Read(def, wrapper, repoB, resolvers, e1A.Id())
328	require.NoError(t, err)
329
330	e2B, err := Read(def, wrapper, repoB, resolvers, e2A.Id())
331	require.NoError(t, err)
332
333	e1B.Append(newOp1(id1, "barbarfoofoo"))
334	err = e1B.Commit(repoB)
335	require.NoError(t, err)
336
337	e2B.Append(newOp2(id2, "barbarbarfoofoo"))
338	err = e2B.Commit(repoB)
339	require.NoError(t, err)
340
341	_, err = Push(def, repoA, "remote")
342	require.NoError(t, err)
343
344	_, err = Fetch(def, repoB, "remote")
345	require.NoError(t, err)
346
347	results = MergeAll(def, wrapper, repoB, resolvers, "remote", id1)
348
349	assertMergeResults(t, []entity.MergeResult{
350		{
351			Id:     e1A.Id(),
352			Status: entity.MergeStatusUpdated,
353		},
354		{
355			Id:     e2A.Id(),
356			Status: entity.MergeStatusUpdated,
357		},
358	}, results)
359
360	assertNotEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
361
362	_, err = Push(def, repoB, "remote")
363	require.NoError(t, err)
364
365	_, err = Fetch(def, repoA, "remote")
366	require.NoError(t, err)
367
368	results = MergeAll(def, wrapper, repoA, resolvers, "remote", id1)
369
370	assertMergeResults(t, []entity.MergeResult{
371		{
372			Id:     e1A.Id(),
373			Status: entity.MergeStatusUpdated,
374		},
375		{
376			Id:     e2A.Id(),
377			Status: entity.MergeStatusUpdated,
378		},
379	}, results)
380
381	// make sure that the graphs become stable over multiple repo, due to the
382	// fast-forward
383	assertEqualRefs(t, repoA, repoB, "refs/"+def.Namespace)
384}
385
386func TestRemove(t *testing.T) {
387	repoA, _, _, id1, _, resolvers, def := makeTestContextRemote(t)
388
389	e := New(def)
390	e.Append(newOp1(id1, "foo"))
391	require.NoError(t, e.Commit(repoA))
392
393	_, err := Push(def, repoA, "remote")
394	require.NoError(t, err)
395
396	err = Remove(def, repoA, e.Id())
397	require.NoError(t, err)
398
399	_, err = Read(def, wrapper, repoA, resolvers, e.Id())
400	require.Error(t, err)
401
402	_, err = readRemote(def, wrapper, repoA, resolvers, "remote", e.Id())
403	require.Error(t, err)
404
405	// Remove is idempotent
406	err = Remove(def, repoA, e.Id())
407	require.NoError(t, err)
408}
409
410func TestRemoveAll(t *testing.T) {
411	repoA, _, _, id1, _, resolvers, def := makeTestContextRemote(t)
412
413	var ids []entity.Id
414
415	for i := 0; i < 10; i++ {
416		e := New(def)
417		e.Append(newOp1(id1, "foo"))
418		require.NoError(t, e.Commit(repoA))
419		ids = append(ids, e.Id())
420	}
421
422	_, err := Push(def, repoA, "remote")
423	require.NoError(t, err)
424
425	err = RemoveAll(def, repoA)
426	require.NoError(t, err)
427
428	for _, id := range ids {
429		_, err = Read(def, wrapper, repoA, resolvers, id)
430		require.Error(t, err)
431
432		_, err = readRemote(def, wrapper, repoA, resolvers, "remote", id)
433		require.Error(t, err)
434	}
435
436	// Remove is idempotent
437	err = RemoveAll(def, repoA)
438	require.NoError(t, err)
439}