diff --git a/pkg/acl/tree/tree_test.go b/pkg/acl/tree/tree_test.go index 563280bf..6fe31994 100644 --- a/pkg/acl/tree/tree_test.go +++ b/pkg/acl/tree/tree_test.go @@ -150,6 +150,97 @@ func TestTree_AddFuzzy(t *testing.T) { } } +func TestTree_CheckRootReduce(t *testing.T) { + t.Run("check root once", func(t *testing.T) { + tr := new(Tree) + tr.Add( + newSnapshot("0", ""), + newChange("1", "0", "0"), + newChange("1.1", "0", "1"), + newChange("1.2", "0", "1"), + newChange("1.4", "0", "1.2"), + newChange("1.3", "0", "1"), + newChange("1.3.1", "0", "1.3"), + newChange("1.2+3", "0", "1.4", "1.3.1"), + newChange("1.2+3.1", "0", "1.2+3"), + newSnapshot("10", "0", "1.2+3.1", "1.1"), + newChange("last", "10", "10"), + ) + t.Run("check root", func(t *testing.T) { + total := tr.checkRoot(tr.attached["10"]) + assert.Equal(t, 1, total) + }) + t.Run("reduce", func(t *testing.T) { + tr.reduceTree() + assert.Equal(t, "10", tr.RootId()) + var res []string + tr.Iterate(tr.RootId(), func(c *Change) (isContinue bool) { + res = append(res, c.Id) + return true + }) + assert.Equal(t, []string{"10", "last"}, res) + }) + }) + t.Run("check root many", func(t *testing.T) { + tr := new(Tree) + tr.Add( + newSnapshot("0", ""), + newSnapshot("1", "0", "0"), + newChange("1.1", "0", "1"), + newChange("1.2", "0", "1"), + newChange("1.4", "0", "1.2"), + newChange("1.3", "0", "1"), + newChange("1.3.1", "0", "1.3"), + newChange("1.2+3", "0", "1.4", "1.3.1"), + newChange("1.2+3.1", "0", "1.2+3"), + newSnapshot("10", "0", "1.2+3.1", "1.1"), + newChange("last", "10", "10"), + ) + t.Run("check root", func(t *testing.T) { + total := tr.checkRoot(tr.attached["10"]) + assert.Equal(t, 1, total) + + total = tr.checkRoot(tr.attached["1"]) + assert.Equal(t, 9, total) + }) + t.Run("reduce", func(t *testing.T) { + tr.reduceTree() + assert.Equal(t, "10", tr.RootId()) + var res []string + tr.Iterate(tr.RootId(), func(c *Change) (isContinue bool) { + res = append(res, c.Id) + return true + }) + assert.Equal(t, []string{"10", "last"}, res) + }) + }) + t.Run("check root incorrect", func(t *testing.T) { + tr := new(Tree) + tr.Add( + newSnapshot("0", ""), + newChange("1", "0", "0"), + newChange("1.1", "0", "1"), + newChange("1.2", "0", "1"), + newChange("1.4", "0", "1.2"), + newChange("1.3", "0", "1"), + newSnapshot("1.3.1", "0", "1.3"), + newChange("1.2+3", "0", "1.4", "1.3.1"), + newChange("1.2+3.1", "0", "1.2+3"), + newChange("10", "0", "1.2+3.1", "1.1"), + newChange("last", "10", "10"), + ) + t.Run("check root", func(t *testing.T) { + total := tr.checkRoot(tr.attached["1.3.1"]) + assert.Equal(t, -1, total) + }) + t.Run("reduce", func(t *testing.T) { + tr.reduceTree() + assert.Equal(t, "0", tr.RootId()) + assert.Equal(t, 0, len(tr.possibleRoots)) + }) + }) +} + func TestTree_Iterate(t *testing.T) { t.Run("complex tree", func(t *testing.T) { tr := new(Tree) diff --git a/pkg/acl/tree/treereduce.go b/pkg/acl/tree/treereduce.go new file mode 100644 index 00000000..d1a788ac --- /dev/null +++ b/pkg/acl/tree/treereduce.go @@ -0,0 +1,89 @@ +package tree + +import "math" + +// clearPossibleRoots force removes any snapshots which can further be deemed as roots +func (t *Tree) clearPossibleRoots() { + t.possibleRoots = t.possibleRoots[:0] +} + +// checkRoot checks if a change can be a new root for the tree +// it returns total changes which were discovered during dfsPrev from heads +func (t *Tree) checkRoot(change *Change) (total int) { + t.stackBuf = t.stackBuf[:0] + stack := t.stackBuf + + // starting with heads + for _, h := range t.headIds { + stack = append(stack, t.attached[h]) + } + + change.visited = true + t.dfsPrev( + stack, + func(ch *Change) { + total += 1 + }, + func(changes []*Change) { + if t.root.visited { + total = -1 + } + }, + ) + change.visited = false + + return +} + +// makeRootAndRemove removes all changes before start and makes start the root +func (t *Tree) makeRootAndRemove(start *Change) { + t.stackBuf = t.stackBuf[:0] + stack := t.stackBuf + for _, prev := range start.PreviousIds { + stack = append(stack, t.attached[prev]) + } + + t.dfsPrev( + stack, + func(ch *Change) {}, + func(changes []*Change) { + for _, ch := range changes { + delete(t.unAttached, ch.Id) + } + }, + ) + + // removing unattached because they may refer to previous root + t.unAttached = make(map[string]*Change) + t.root = start +} + +// reduceTree tries to reduce the tree to one of possible tree roots +func (t *Tree) reduceTree() (res bool) { + if len(t.possibleRoots) == 0 { + return + } + var ( + minRoot *Change + minTotal = math.MaxInt + ) + + // checking if we can reduce tree to other root + for _, root := range t.possibleRoots { + totalChanges := t.checkRoot(root) + // we prefer new root with min amount of total changes + if totalChanges != -1 && totalChanges < minTotal { + minRoot = root + minTotal = totalChanges + } + } + + t.clearPossibleRoots() + if minRoot == nil { + return + } + + t.makeRootAndRemove(minRoot) + res = true + return +}