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}