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  }