github.com/treeverse/lakefs@v1.24.1-0.20240520134607-95648127bfb0/pkg/graveler/ref/merge_base_finder_test.go (about) 1 package ref_test 2 3 import ( 4 "context" 5 "fmt" 6 "testing" 7 8 "github.com/treeverse/lakefs/pkg/graveler" 9 "github.com/treeverse/lakefs/pkg/graveler/ref" 10 "github.com/treeverse/lakefs/pkg/testutil" 11 ) 12 13 type MockCommitGetter struct { 14 byCommitID map[graveler.CommitID]*graveler.Commit 15 visited map[graveler.CommitID]int 16 } 17 18 func (g *MockCommitGetter) GetCommit(_ context.Context, _ *graveler.RepositoryRecord, commitID graveler.CommitID) (*graveler.Commit, error) { 19 if commit, ok := g.byCommitID[commitID]; ok { 20 g.visited[commitID] += 1 21 return commit, nil 22 } 23 return nil, graveler.ErrNotFound 24 } 25 26 func computeGeneration(byCommitID map[graveler.CommitID]*graveler.Commit, commit *graveler.Commit) int { 27 if commit.Generation > 0 { 28 return int(commit.Generation) 29 } 30 if len(commit.Parents) == 0 { 31 return 1 32 } 33 maxGeneration := 0 34 for _, parent := range commit.Parents { 35 parentCommit := byCommitID[parent] 36 parentGeneration := computeGeneration(byCommitID, parentCommit) 37 if parentGeneration > maxGeneration { 38 maxGeneration = parentGeneration 39 } 40 } 41 commit.Generation = graveler.CommitGeneration(maxGeneration + 1) 42 return int(commit.Generation) 43 } 44 45 func newReader(kv map[graveler.CommitID]*graveler.Commit) *MockCommitGetter { 46 for _, v := range kv { 47 v.Generation = graveler.CommitGeneration(computeGeneration(kv, v)) 48 } 49 50 return &MockCommitGetter{ 51 byCommitID: kv, 52 visited: map[graveler.CommitID]int{}, 53 } 54 } 55 56 func TestFindMergeBase(t *testing.T) { 57 cases := []struct { 58 Name string 59 Left graveler.CommitID 60 Right graveler.CommitID 61 Getter func() *MockCommitGetter 62 Expected []string 63 }{ 64 { 65 Name: "root_match", 66 Left: "c7", 67 Right: "c6", 68 Getter: func() *MockCommitGetter { 69 c0 := &graveler.Commit{Message: "c0", Parents: []graveler.CommitID{}} 70 c1 := &graveler.Commit{Message: "c1", Parents: []graveler.CommitID{"c0"}} 71 c2 := &graveler.Commit{Message: "c2", Parents: []graveler.CommitID{"c0"}} 72 c3 := &graveler.Commit{Message: "c3", Parents: []graveler.CommitID{"c1"}} 73 c4 := &graveler.Commit{Message: "c4", Parents: []graveler.CommitID{"c2"}} 74 c5 := &graveler.Commit{Message: "c5", Parents: []graveler.CommitID{"c3"}} 75 c6 := &graveler.Commit{Message: "c6", Parents: []graveler.CommitID{"c4"}} 76 c7 := &graveler.Commit{Message: "c7", Parents: []graveler.CommitID{"c5"}} 77 return newReader(map[graveler.CommitID]*graveler.Commit{ 78 "c0": c0, "c1": c1, "c2": c2, "c3": c3, "c4": c4, "c5": c5, "c6": c6, "c7": c7, 79 }) 80 }, 81 Expected: []string{"c0"}, 82 }, 83 { 84 Name: "close_ancestor", 85 Left: "c3", 86 Right: "c4", 87 Getter: func() *MockCommitGetter { 88 c0 := &graveler.Commit{Message: "c0", Parents: []graveler.CommitID{}} 89 c1 := &graveler.Commit{Message: "c1", Parents: []graveler.CommitID{"c0"}} 90 c2 := &graveler.Commit{Message: "c2", Parents: []graveler.CommitID{"c1"}} 91 c3 := &graveler.Commit{Message: "c3", Parents: []graveler.CommitID{"c2"}} 92 c4 := &graveler.Commit{Message: "c4", Parents: []graveler.CommitID{"c2"}} 93 return newReader(map[graveler.CommitID]*graveler.Commit{ 94 "c0": c0, "c1": c1, "c2": c2, "c3": c3, "c4": c4, 95 }) 96 }, 97 Expected: []string{"c2"}, 98 }, 99 { 100 Name: "criss_cross", 101 Left: "c5", 102 Right: "c6", 103 Getter: func() *MockCommitGetter { 104 c0 := &graveler.Commit{Message: "c0", Parents: []graveler.CommitID{}} 105 c1 := &graveler.Commit{Message: "c1", Parents: []graveler.CommitID{"c0"}} 106 c2 := &graveler.Commit{Message: "c2", Parents: []graveler.CommitID{"c0"}} 107 c3 := &graveler.Commit{Message: "c3", Parents: []graveler.CommitID{"c1", "c2"}} 108 c4 := &graveler.Commit{Message: "c4", Parents: []graveler.CommitID{"c1", "c2"}} 109 c5 := &graveler.Commit{Message: "c5", Parents: []graveler.CommitID{"c3"}} 110 c6 := &graveler.Commit{Message: "c6", Parents: []graveler.CommitID{"c4"}} 111 return newReader(map[graveler.CommitID]*graveler.Commit{ 112 "c0": c0, "c1": c1, "c2": c2, "c3": c3, "c4": c4, "c5": c5, "c6": c6, 113 }) 114 }, 115 Expected: []string{"c1", "c2"}, 116 }, 117 { 118 Name: "contained", 119 Left: "c2", 120 Right: "c1", 121 Getter: func() *MockCommitGetter { 122 c0 := &graveler.Commit{Message: "c0", Parents: []graveler.CommitID{}} 123 c1 := &graveler.Commit{Message: "c1", Parents: []graveler.CommitID{"c0"}} 124 c2 := &graveler.Commit{Message: "c2", Parents: []graveler.CommitID{"c1"}} 125 return newReader(map[graveler.CommitID]*graveler.Commit{ 126 "c0": c0, "c1": c1, "c2": c2, 127 }) 128 }, 129 Expected: []string{"c1"}, 130 }, 131 { 132 Name: "parallel", 133 Left: "c7", 134 Right: "c3", 135 Getter: func() *MockCommitGetter { 136 c0 := &graveler.Commit{Message: "c0", Parents: []graveler.CommitID{}} 137 c1 := &graveler.Commit{Message: "c1", Parents: []graveler.CommitID{"c0"}} 138 c2 := &graveler.Commit{Message: "c2", Parents: []graveler.CommitID{"c1"}} 139 c3 := &graveler.Commit{Message: "c3", Parents: []graveler.CommitID{"c2"}} 140 c4 := &graveler.Commit{Message: "c4", Parents: []graveler.CommitID{}} 141 c5 := &graveler.Commit{Message: "c5", Parents: []graveler.CommitID{"c4"}} 142 c6 := &graveler.Commit{Message: "c6", Parents: []graveler.CommitID{"c5"}} 143 c7 := &graveler.Commit{Message: "c7", Parents: []graveler.CommitID{"c6"}} 144 return newReader(map[graveler.CommitID]*graveler.Commit{ 145 "c0": c0, "c1": c1, "c2": c2, "c3": c3, "c4": c4, "c5": c5, "c6": c6, "c7": c7, 146 }) 147 }, 148 Expected: []string{}, 149 }, 150 { 151 Name: "already_merged", 152 Left: "c3", 153 Right: "c4", 154 Getter: func() *MockCommitGetter { 155 c0 := &graveler.Commit{Message: "c0", Parents: []graveler.CommitID{}} 156 c2 := &graveler.Commit{Message: "c2", Parents: []graveler.CommitID{"c0"}} 157 c1 := &graveler.Commit{Message: "c1", Parents: []graveler.CommitID{"c0", "c2"}} 158 c3 := &graveler.Commit{Message: "c3", Parents: []graveler.CommitID{"c1"}} 159 c4 := &graveler.Commit{Message: "c4", Parents: []graveler.CommitID{"c2"}} 160 return newReader(map[graveler.CommitID]*graveler.Commit{ 161 "c0": c0, "c1": c1, "c2": c2, "c3": c3, "c4": c4, 162 }) 163 }, 164 Expected: []string{"c2"}, 165 }, 166 { 167 Name: "higher ancestor is closer on dag", 168 Left: "x", 169 Right: "y", 170 Getter: func() *MockCommitGetter { 171 c1 := &graveler.Commit{Message: "c1", Parents: []graveler.CommitID{}} 172 c2 := &graveler.Commit{Message: "c2", Parents: []graveler.CommitID{"c1"}} 173 c3 := &graveler.Commit{Message: "c3", Parents: []graveler.CommitID{"c2"}} 174 c4 := &graveler.Commit{Message: "c4", Parents: []graveler.CommitID{"c3"}} 175 x := &graveler.Commit{Message: "x", Parents: []graveler.CommitID{"c4", "c1"}} 176 y := &graveler.Commit{Message: "y", Parents: []graveler.CommitID{"c2"}} 177 return newReader(map[graveler.CommitID]*graveler.Commit{ 178 "c1": c1, "c2": c2, "c3": c3, "c4": c4, "x": x, "y": y, 179 }) 180 }, 181 Expected: []string{"c2"}, 182 }, 183 { 184 Name: "merges in history (from git core tests)", 185 // E---D---C---B---A 186 // \"-_ \ \ 187 // \ `---------G \ 188 // \ \ 189 // F----------------H 190 Left: "g", 191 Right: "h", 192 193 Getter: func() *MockCommitGetter { 194 e := &graveler.Commit{Message: "e", Parents: []graveler.CommitID{}} 195 d := &graveler.Commit{Message: "d", Parents: []graveler.CommitID{"e"}} 196 f := &graveler.Commit{Message: "f", Parents: []graveler.CommitID{"e"}} 197 c := &graveler.Commit{Message: "c", Parents: []graveler.CommitID{"d"}} 198 b := &graveler.Commit{Message: "b", Parents: []graveler.CommitID{"c"}} 199 a := &graveler.Commit{Message: "a", Parents: []graveler.CommitID{"b"}} 200 g := &graveler.Commit{Message: "g", Parents: []graveler.CommitID{"b", "e"}} 201 h := &graveler.Commit{Message: "h", Parents: []graveler.CommitID{"a", "f"}} 202 return newReader(map[graveler.CommitID]*graveler.Commit{ 203 "e": e, "d": d, "f": f, "c": c, "b": b, "a": a, "g": g, "h": h, 204 }) 205 }, 206 Expected: []string{"b"}, 207 }, 208 { 209 Name: "same_node", 210 Left: "c2", 211 Right: "c2", 212 Getter: func() *MockCommitGetter { 213 c0 := &graveler.Commit{Message: "c0", Parents: []graveler.CommitID{}} 214 c1 := &graveler.Commit{Message: "c1", Parents: []graveler.CommitID{"c0"}} 215 c2 := &graveler.Commit{Message: "c2", Parents: []graveler.CommitID{"c0"}} 216 return newReader(map[graveler.CommitID]*graveler.Commit{ 217 "c0": c0, "c1": c1, "c2": c2, 218 }) 219 }, 220 Expected: []string{"c2"}, 221 }, 222 { 223 Name: "no redundant parent access", 224 // ROOT---------R 225 // \ 226 // `---a---c---L 227 // \ / / 228 // `---b---d 229 // 230 // Verifying the fix introduced with https://github.com/treeverse/lakeFS/pull/2968. The following commits tree 231 // will generate multiple accesses to commit 'b' as it is a parent commit for both 'd' and 'c' and per BFS algo, 232 // it will be reached via both paths before ROOT is reached from L. 233 // The above-mentioned fix eliminates that 234 Left: "l", 235 Right: "r", 236 Getter: func() *MockCommitGetter { 237 root := &graveler.Commit{Message: "root", Parents: []graveler.CommitID{}} 238 a := &graveler.Commit{Message: "a", Parents: []graveler.CommitID{"root"}} 239 b := &graveler.Commit{Message: "b", Parents: []graveler.CommitID{"root"}} 240 c := &graveler.Commit{Message: "c", Parents: []graveler.CommitID{"a", "b"}} 241 d := &graveler.Commit{Message: "d", Parents: []graveler.CommitID{"b"}} 242 l := &graveler.Commit{Message: "L", Parents: []graveler.CommitID{"c", "d"}} 243 r := &graveler.Commit{Message: "R", Parents: []graveler.CommitID{"root"}} 244 return newReader(map[graveler.CommitID]*graveler.Commit{ 245 "root": root, "a": a, "b": b, "c": c, "d": d, "l": l, "r": r, 246 }) 247 }, 248 Expected: []string{"root"}, 249 }, 250 { 251 Name: "complex graph with multiple merges and common ancestor in the middle", 252 // ---ROOT--- 253 // / / \ \ 254 // / a b \ 255 // / \ / \ 256 // | ab | 257 // | / \ | 258 // | / /\ \ | 259 // | / / \ \ | 260 // \ / | | \ / 261 // l0 | | r0 262 // /\ / \ /\ 263 // / l1 r1 \ 264 // \ /\ /\ / 265 // l2 \ / r2 266 // /\ / \ /\ 267 // / l3 r3 \ 268 // \ /\ /\ / 269 // l4 \ / r4 270 // /\ / \ /\ 271 // / l5 r6 \ 272 // \ /\ /\ / 273 // l7 \ / r7 274 // /\ / \ /\ 275 // / l8 r8 \ 276 // \ /\ /\ / 277 // l9 \ / r9 278 // \ / \ / 279 // LEFT RIGHT 280 Left: "left", 281 Right: "right", 282 Getter: func() *MockCommitGetter { 283 root := &graveler.Commit{Message: "root", Parents: []graveler.CommitID{}} 284 a := &graveler.Commit{Message: "a", Parents: []graveler.CommitID{"root"}} 285 b := &graveler.Commit{Message: "b", Parents: []graveler.CommitID{"root"}} 286 ab := &graveler.Commit{Message: "ab", Parents: []graveler.CommitID{"a", "b"}} 287 l0 := &graveler.Commit{Message: "l0", Parents: []graveler.CommitID{"root", "ab"}} 288 l1 := &graveler.Commit{Message: "l1", Parents: []graveler.CommitID{"ab", "l0"}} 289 l2 := &graveler.Commit{Message: "l2", Parents: []graveler.CommitID{"l0", "l1"}} 290 l3 := &graveler.Commit{Message: "l3", Parents: []graveler.CommitID{"l1", "l2"}} 291 l4 := &graveler.Commit{Message: "l4", Parents: []graveler.CommitID{"l2", "l3"}} 292 l5 := &graveler.Commit{Message: "l5", Parents: []graveler.CommitID{"l3", "l4"}} 293 l6 := &graveler.Commit{Message: "l6", Parents: []graveler.CommitID{"l4", "l5"}} 294 l7 := &graveler.Commit{Message: "l7", Parents: []graveler.CommitID{"l5", "l6"}} 295 l8 := &graveler.Commit{Message: "l8", Parents: []graveler.CommitID{"l6", "l7"}} 296 l9 := &graveler.Commit{Message: "l9", Parents: []graveler.CommitID{"l7", "l8"}} 297 left := &graveler.Commit{Message: "left", Parents: []graveler.CommitID{"l8", "l9"}} 298 r0 := &graveler.Commit{Message: "r0", Parents: []graveler.CommitID{"root", "ab"}} 299 r1 := &graveler.Commit{Message: "r1", Parents: []graveler.CommitID{"ab", "r0"}} 300 r2 := &graveler.Commit{Message: "r2", Parents: []graveler.CommitID{"r0", "r1"}} 301 r3 := &graveler.Commit{Message: "r3", Parents: []graveler.CommitID{"r1", "r2"}} 302 r4 := &graveler.Commit{Message: "r4", Parents: []graveler.CommitID{"r2", "r3"}} 303 r5 := &graveler.Commit{Message: "r5", Parents: []graveler.CommitID{"r3", "r4"}} 304 r6 := &graveler.Commit{Message: "r6", Parents: []graveler.CommitID{"r4", "r5"}} 305 r7 := &graveler.Commit{Message: "r7", Parents: []graveler.CommitID{"r5", "r6"}} 306 r8 := &graveler.Commit{Message: "r8", Parents: []graveler.CommitID{"r6", "r7"}} 307 r9 := &graveler.Commit{Message: "r9", Parents: []graveler.CommitID{"r7", "r8"}} 308 right := &graveler.Commit{Message: "right", Parents: []graveler.CommitID{"r8", "r9"}} 309 return newReader(map[graveler.CommitID]*graveler.Commit{ 310 "root": root, "a": a, "b": b, "ab": ab, 311 "l0": l0, "l1": l1, "l2": l2, "l3": l3, "l4": l4, 312 "l5": l5, "l6": l6, "l7": l7, "l8": l8, "l9": l9, 313 "r0": r0, "r1": r1, "r2": r2, "r3": r3, "r4": r4, 314 "r5": r5, "r6": r6, "r7": r7, "r8": r8, "r9": r9, 315 "right": right, "left": left, 316 }) 317 }, 318 Expected: []string{"ab"}, 319 }, 320 } 321 repository := &graveler.RepositoryRecord{RepositoryID: "ref-test-repo"} 322 for _, cas := range cases { 323 t.Run(cas.Name, func(t *testing.T) { 324 getter := cas.Getter() 325 base, err := ref.FindMergeBase(context.Background(), getter, repository, cas.Left, cas.Right) 326 if err != nil { 327 t.Fatalf("unexpected error %v", err) 328 } 329 verifyResult(t, base, cas.Expected, getter.visited) 330 331 // flip right and left and expect the same result, reset visited to keep track of the second round visits 332 getter.visited = map[graveler.CommitID]int{} 333 base, err = ref.FindMergeBase( 334 context.Background(), getter, repository, cas.Right, cas.Left) 335 if err != nil { 336 t.Fatalf("unexpected error %v", err) 337 } 338 verifyResult(t, base, cas.Expected, getter.visited) 339 }) 340 } 341 } 342 343 func TestGrid(t *testing.T) { 344 // Construct the following grid, taken from https://github.com/git/git/blob/master/t/t6600-test-reach.sh 345 // (10,10) 346 // / \ 347 // (10,9) (9,10) 348 // / \ / \ 349 // (10,8) (9,9) (8,10) 350 // / \ / \ / \ 351 // ( continued...) 352 // \ / \ / \ / 353 // (3,1) (2,2) (1,3) 354 // \ / \ / 355 // (2,1) (2,1) 356 // \ / 357 // (1,1) 358 grid := make([][]*graveler.Commit, 10) 359 kv := make(map[graveler.CommitID]*graveler.Commit) 360 for i := 0; i < 10; i++ { 361 grid[i] = make([]*graveler.Commit, 10) 362 for j := 0; j < 10; j++ { 363 parents := make([]graveler.CommitID, 0, 2) 364 if i > 0 { 365 parents = append(parents, graveler.CommitID(fmt.Sprintf("%d-%d", i-1, j))) 366 } 367 if j > 0 { 368 parents = append(parents, graveler.CommitID(fmt.Sprintf("%d-%d", i, j-1))) 369 } 370 grid[i][j] = &graveler.Commit{Message: fmt.Sprintf("%d-%d", i, j), Parents: parents} 371 kv[graveler.CommitID(fmt.Sprintf("%d-%d", i, j))] = grid[i][j] 372 } 373 } 374 repository := &graveler.RepositoryRecord{RepositoryID: "ref-test-repo"} 375 getter := newReader(kv) 376 c, err := ref.FindMergeBase(context.Background(), getter, repository, "7-4", "5-6") 377 testutil.Must(t, err) 378 verifyResult(t, c, []string{"5-4"}, getter.visited) 379 380 getter.visited = map[graveler.CommitID]int{} 381 c, err = ref.FindMergeBase(context.Background(), getter, repository, "1-2", "2-1") 382 testutil.Must(t, err) 383 verifyResult(t, c, []string{"1-1"}, getter.visited) 384 385 getter.visited = map[graveler.CommitID]int{} 386 c, err = ref.FindMergeBase(context.Background(), getter, repository, "0-9", "9-0") 387 testutil.Must(t, err) 388 verifyResult(t, c, []string{"0-0"}, getter.visited) 389 390 getter.visited = map[graveler.CommitID]int{} 391 c, err = ref.FindMergeBase(context.Background(), getter, repository, "6-9", "9-6") 392 testutil.Must(t, err) 393 verifyResult(t, c, []string{"6-6"}, getter.visited) 394 } 395 396 func verifyResult(t *testing.T, base *graveler.Commit, expected []string, visited map[graveler.CommitID]int) { 397 if base == nil { 398 if len(expected) != 0 { 399 t.Fatalf("got nil result, expected %s", expected) 400 } 401 return 402 } 403 for id, numVisits := range visited { 404 if string(id) == base.Message && numVisits > 2 { 405 t.Fatalf("visited base commit %d, expected max 2 visits", numVisits) 406 } else if string(id) != base.Message && numVisits > 1 { 407 t.Fatalf("visited non-base commit %d, expected max 1 visit", numVisits) 408 } 409 } 410 for _, expectedKey := range expected { 411 if base.Message == expectedKey { 412 return 413 } 414 } 415 t.Fatalf("expected one of (%v) got (%v)", expected, base.Message) 416 }