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}