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