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