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}