add basic folder operations to tree (#4696)
adds SetFolder and AddTombstone operations to the drive delta tree. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🌻 Feature #### Issue(s) * #4689 #### Test Plan - [x] ⚡ Unit test
This commit is contained in:
parent
d36636285a
commit
bbf5350f6e
@ -1,8 +1,13 @@
|
||||
package drive
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/alcionai/clues"
|
||||
|
||||
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
|
||||
"github.com/alcionai/corso/src/pkg/logger"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
)
|
||||
|
||||
@ -15,8 +20,8 @@ type folderyMcFolderFace struct {
|
||||
prefix path.Path
|
||||
|
||||
// the root of the tree;
|
||||
// new, moved, and notMoved collections
|
||||
collections *nodeyMcNodeFace
|
||||
// new, moved, and notMoved root
|
||||
root *nodeyMcNodeFace
|
||||
|
||||
// the majority of operations we perform can be handled with
|
||||
// a folder ID lookup instead of re-walking the entire tree.
|
||||
@ -24,8 +29,9 @@ type folderyMcFolderFace struct {
|
||||
folderIDToNode map[string]*nodeyMcNodeFace
|
||||
|
||||
// tombstones don't need to form a tree.
|
||||
// we only need the folder ID and their previous path.
|
||||
tombstones map[string]path.Path
|
||||
// We maintain the node data in case we swap back
|
||||
// and forth between live and tombstoned states.
|
||||
tombstones map[string]*nodeyMcNodeFace
|
||||
|
||||
// it's just a sensible place to store the data, since we're
|
||||
// already pushing file additions through the api.
|
||||
@ -38,7 +44,7 @@ func newFolderyMcFolderFace(
|
||||
return &folderyMcFolderFace{
|
||||
prefix: prefix,
|
||||
folderIDToNode: map[string]*nodeyMcNodeFace{},
|
||||
tombstones: map[string]path.Path{},
|
||||
tombstones: map[string]*nodeyMcNodeFace{},
|
||||
excludeFileIDs: map[string]struct{}{},
|
||||
}
|
||||
}
|
||||
@ -53,9 +59,9 @@ type nodeyMcNodeFace struct {
|
||||
// single directory name, not a path
|
||||
name string
|
||||
// only contains the folders starting at and including '/root:'
|
||||
prev path.Path
|
||||
prev path.Elements
|
||||
// map folderID -> node
|
||||
childDirs map[string]*nodeyMcNodeFace
|
||||
children map[string]*nodeyMcNodeFace
|
||||
// items are keyed by item ID
|
||||
items map[string]time.Time
|
||||
// for special handling protocols around packages
|
||||
@ -65,7 +71,7 @@ type nodeyMcNodeFace struct {
|
||||
func newNodeyMcNodeFace(
|
||||
parent *nodeyMcNodeFace,
|
||||
id, name string,
|
||||
prev path.Path,
|
||||
prev path.Elements,
|
||||
isPackage bool,
|
||||
) *nodeyMcNodeFace {
|
||||
return &nodeyMcNodeFace{
|
||||
@ -73,8 +79,176 @@ func newNodeyMcNodeFace(
|
||||
id: id,
|
||||
name: name,
|
||||
prev: prev,
|
||||
childDirs: map[string]*nodeyMcNodeFace{},
|
||||
children: map[string]*nodeyMcNodeFace{},
|
||||
items: map[string]time.Time{},
|
||||
isPackage: isPackage,
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// folder handling
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// SetFolder adds a node with the following details to the tree.
|
||||
// If the node already exists with the given ID, the name and parent
|
||||
// values are updated to match (isPackage is assumed not to change).
|
||||
func (face *folderyMcFolderFace) SetFolder(
|
||||
ctx context.Context,
|
||||
parentID, id, name string,
|
||||
isPackage bool,
|
||||
) error {
|
||||
// need to ensure we have the minimum requirements met for adding a node.
|
||||
if len(id) == 0 {
|
||||
return clues.NewWC(ctx, "missing folder ID")
|
||||
}
|
||||
|
||||
if len(name) == 0 {
|
||||
return clues.NewWC(ctx, "missing folder name")
|
||||
}
|
||||
|
||||
// drive doesn't normally allow the `:` character in folder names.
|
||||
// so `root:` is, by default, the only folder that can match this
|
||||
// name. That makes this check a little bit brittle, but generally
|
||||
// reliable, since we should always see the root first and can rely
|
||||
// on the naming structure.
|
||||
if len(parentID) == 0 && name != odConsts.RootPathDir {
|
||||
return clues.NewWC(ctx, "non-root folder missing parent id")
|
||||
}
|
||||
|
||||
// only set the root node once.
|
||||
if name == odConsts.RootPathDir {
|
||||
if face.root == nil {
|
||||
root := newNodeyMcNodeFace(nil, id, name, nil, isPackage)
|
||||
face.root = root
|
||||
face.folderIDToNode[id] = root
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// There are four possible changes that can happen at this point.
|
||||
// 1. new folder addition.
|
||||
// 2. duplicate folder addition.
|
||||
// 3. existing folder migrated to new location.
|
||||
// 4. tombstoned folder restored.
|
||||
|
||||
parent, ok := face.folderIDToNode[parentID]
|
||||
if !ok {
|
||||
return clues.NewWC(ctx, "folder added before parent")
|
||||
}
|
||||
|
||||
// Handling case 4 is exclusive to 1-3. IE: we ensure tree state such
|
||||
// that a node's previous appearance can be either a tombstone or
|
||||
// a live node, but not both. So if we find a tombstone, we assume
|
||||
// there is not also a node in the live tree for this id.
|
||||
|
||||
// if a folder is deleted and restored, we'll get both the deletion marker
|
||||
// (which should be first in enumeration, since all deletion markers are first,
|
||||
// or it would have happened in one of the prior enumerations), followed by
|
||||
// the restoration of the folder.
|
||||
if zombey, tombstoned := face.tombstones[id]; tombstoned {
|
||||
delete(face.tombstones, id)
|
||||
|
||||
zombey.parent = parent
|
||||
zombey.name = name
|
||||
parent.children[id] = zombey
|
||||
face.folderIDToNode[id] = zombey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// if not previously a tombstone, handle change cases 1-3
|
||||
var (
|
||||
nodey *nodeyMcNodeFace
|
||||
visited bool
|
||||
)
|
||||
|
||||
// change type 2 & 3. Update the existing node details to match current data.
|
||||
if nodey, visited = face.folderIDToNode[id]; visited {
|
||||
if nodey.parent == nil {
|
||||
// technically shouldn't be possible but better to keep the problem tracked
|
||||
// just in case.
|
||||
logger.Ctx(ctx).Info("non-root folder already exists with no parent ref")
|
||||
} else if nodey.parent != parent {
|
||||
// change type 3. we need to ensure the old parent stops pointing to this node.
|
||||
delete(nodey.parent.children, id)
|
||||
}
|
||||
|
||||
nodey.name = name
|
||||
nodey.parent = parent
|
||||
} else {
|
||||
// change type 1: new addition
|
||||
// the previous location is always nil, since previous path additions get their
|
||||
// own setter func.
|
||||
nodey = newNodeyMcNodeFace(parent, id, name, nil, isPackage)
|
||||
}
|
||||
|
||||
// ensure the parent points to this node, and that the node is registered
|
||||
// in the map of all nodes in the tree.
|
||||
parent.children[id] = nodey
|
||||
face.folderIDToNode[id] = nodey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (face *folderyMcFolderFace) SetTombstone(
|
||||
ctx context.Context,
|
||||
id string,
|
||||
loc path.Elements,
|
||||
) error {
|
||||
if len(id) == 0 {
|
||||
return clues.NewWC(ctx, "missing tombstone folder ID")
|
||||
}
|
||||
|
||||
if len(loc) == 0 {
|
||||
return clues.NewWC(ctx, "missing tombstone location")
|
||||
}
|
||||
|
||||
// since we run mutiple enumerations, it's possible to see a folder added on the
|
||||
// first enumeration that then gets deleted on the next. This means that the folder
|
||||
// was deleted while the first enumeration was in flight, and will show up even if
|
||||
// the folder gets restored after deletion.
|
||||
// When this happens, we have to adjust the original tree accordingly.
|
||||
if zombey, stillKicking := face.folderIDToNode[id]; stillKicking {
|
||||
if zombey.parent != nil {
|
||||
delete(zombey.parent.children, id)
|
||||
}
|
||||
|
||||
delete(face.folderIDToNode, id)
|
||||
|
||||
zombey.parent = nil
|
||||
face.tombstones[id] = zombey
|
||||
|
||||
// this handling is exclusive to updating an already-existing tombstone.
|
||||
// ie: if we find a living node in the tree, we assume there is no tombstone
|
||||
// entry with the same ID.
|
||||
return nil
|
||||
}
|
||||
|
||||
zombey, alreadyBuried := face.tombstones[id]
|
||||
if alreadyBuried {
|
||||
if zombey.prev.String() != loc.String() {
|
||||
// logging for sanity
|
||||
logger.Ctx(ctx).Infow(
|
||||
"attempted to tombstone two paths with the same ID",
|
||||
"first_tombstone_path", zombey.prev,
|
||||
"second_tombstone_path", loc)
|
||||
}
|
||||
|
||||
// since we're storing drive data by folder name in kopia, not id, we need
|
||||
// to make sure to preserve the original tombstone location. If we get a
|
||||
// conflicting set of locations in the same delta enumeration, we can always
|
||||
// treat the original one as the canonical one. IE: what we're deleting is
|
||||
// the original location as it exists in kopia. So even if we get a newer
|
||||
// location in the drive enumeration, the original location is the one that
|
||||
// kopia uses, and the one we need to tombstone.
|
||||
//
|
||||
// this should also be asserted in the second step, where we compare the delta
|
||||
// changes to the backup previous paths metadata.
|
||||
face.tombstones[id] = zombey
|
||||
} else {
|
||||
face.tombstones[id] = newNodeyMcNodeFace(nil, id, "", loc, false)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
|
||||
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
|
||||
"github.com/alcionai/corso/src/internal/tester"
|
||||
"github.com/alcionai/corso/src/pkg/path"
|
||||
)
|
||||
@ -30,7 +31,7 @@ func (suite *DeltaTreeUnitSuite) TestNewFolderyMcFolderFace() {
|
||||
|
||||
folderFace := newFolderyMcFolderFace(p)
|
||||
assert.Equal(t, p, folderFace.prefix)
|
||||
assert.Nil(t, folderFace.collections)
|
||||
assert.Nil(t, folderFace.root)
|
||||
assert.NotNil(t, folderFace.folderIDToNode)
|
||||
assert.NotNil(t, folderFace.tombstones)
|
||||
assert.NotNil(t, folderFace.excludeFileIDs)
|
||||
@ -40,17 +41,657 @@ func (suite *DeltaTreeUnitSuite) TestNewNodeyMcNodeFace() {
|
||||
var (
|
||||
t = suite.T()
|
||||
parent = &nodeyMcNodeFace{}
|
||||
p, err = path.Build("t", "r", path.SharePointService, path.LibrariesCategory, false, "drive-id", "root:")
|
||||
loc = path.NewElements("root:/foo/bar/baz/qux/fnords/smarf/voi/zumba/bangles/howdyhowdyhowdy")
|
||||
)
|
||||
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
|
||||
nodeFace := newNodeyMcNodeFace(parent, "id", "name", p, true)
|
||||
nodeFace := newNodeyMcNodeFace(parent, "id", "name", loc, true)
|
||||
assert.Equal(t, parent, nodeFace.parent)
|
||||
assert.Equal(t, "id", nodeFace.id)
|
||||
assert.Equal(t, "name", nodeFace.name)
|
||||
assert.Equal(t, p, nodeFace.prev)
|
||||
assert.Equal(t, loc, nodeFace.prev)
|
||||
assert.True(t, nodeFace.isPackage)
|
||||
assert.NotNil(t, nodeFace.childDirs)
|
||||
assert.NotNil(t, nodeFace.children)
|
||||
assert.NotNil(t, nodeFace.items)
|
||||
}
|
||||
|
||||
// note that this test is focused on the SetFolder function,
|
||||
// and intentionally does not verify the resulting node tree
|
||||
func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder() {
|
||||
treeWithRoot := func() *folderyMcFolderFace {
|
||||
rootey := newNodeyMcNodeFace(nil, odConsts.RootID, odConsts.RootPathDir, nil, false)
|
||||
tree := newFolderyMcFolderFace(nil)
|
||||
tree.root = rootey
|
||||
tree.folderIDToNode[odConsts.RootID] = rootey
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
treeWithTombstone := func() *folderyMcFolderFace {
|
||||
tree := treeWithRoot()
|
||||
tree.tombstones["folder"] = newNodeyMcNodeFace(nil, "folder", "", path.NewElements(""), false)
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
treeWithFolders := func() *folderyMcFolderFace {
|
||||
tree := treeWithRoot()
|
||||
|
||||
o := newNodeyMcNodeFace(tree.root, "other", "o", nil, true)
|
||||
tree.folderIDToNode[o.id] = o
|
||||
|
||||
f := newNodeyMcNodeFace(o, "folder", "f", nil, false)
|
||||
tree.folderIDToNode[f.id] = f
|
||||
o.children[f.id] = f
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
table := []struct {
|
||||
tname string
|
||||
tree *folderyMcFolderFace
|
||||
parentID string
|
||||
id string
|
||||
name string
|
||||
isPackage bool
|
||||
expectErr assert.ErrorAssertionFunc
|
||||
expectPrev assert.ValueAssertionFunc
|
||||
}{
|
||||
{
|
||||
tname: "add root",
|
||||
tree: &folderyMcFolderFace{
|
||||
folderIDToNode: map[string]*nodeyMcNodeFace{},
|
||||
},
|
||||
id: odConsts.RootID,
|
||||
name: odConsts.RootPathDir,
|
||||
isPackage: true,
|
||||
expectErr: assert.NoError,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
{
|
||||
tname: "root already exists",
|
||||
tree: treeWithRoot(),
|
||||
id: odConsts.RootID,
|
||||
name: odConsts.RootPathDir,
|
||||
expectErr: assert.NoError,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
{
|
||||
tname: "add folder",
|
||||
tree: treeWithRoot(),
|
||||
parentID: odConsts.RootID,
|
||||
id: "folder",
|
||||
name: "nameyMcNameFace",
|
||||
expectErr: assert.NoError,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
{
|
||||
tname: "add package",
|
||||
tree: treeWithRoot(),
|
||||
parentID: odConsts.RootID,
|
||||
id: "folder",
|
||||
name: "nameyMcNameFace",
|
||||
isPackage: true,
|
||||
expectErr: assert.NoError,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
{
|
||||
tname: "missing ID",
|
||||
tree: treeWithRoot(),
|
||||
parentID: odConsts.RootID,
|
||||
name: "nameyMcNameFace",
|
||||
isPackage: true,
|
||||
expectErr: assert.Error,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
{
|
||||
tname: "missing name",
|
||||
tree: treeWithRoot(),
|
||||
parentID: odConsts.RootID,
|
||||
id: "folder",
|
||||
isPackage: true,
|
||||
expectErr: assert.Error,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
{
|
||||
tname: "missing parentID",
|
||||
tree: treeWithRoot(),
|
||||
id: "folder",
|
||||
name: "nameyMcNameFace",
|
||||
isPackage: true,
|
||||
expectErr: assert.Error,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
{
|
||||
tname: "already tombstoned",
|
||||
tree: treeWithTombstone(),
|
||||
parentID: odConsts.RootID,
|
||||
id: "folder",
|
||||
name: "nameyMcNameFace",
|
||||
expectErr: assert.NoError,
|
||||
expectPrev: assert.NotNil,
|
||||
},
|
||||
{
|
||||
tname: "add folder before parent",
|
||||
tree: &folderyMcFolderFace{
|
||||
folderIDToNode: map[string]*nodeyMcNodeFace{},
|
||||
},
|
||||
parentID: odConsts.RootID,
|
||||
id: "folder",
|
||||
name: "nameyMcNameFace",
|
||||
isPackage: true,
|
||||
expectErr: assert.Error,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
{
|
||||
tname: "folder already exists",
|
||||
tree: treeWithFolders(),
|
||||
parentID: "other",
|
||||
id: "folder",
|
||||
name: "nameyMcNameFace",
|
||||
expectErr: assert.NoError,
|
||||
expectPrev: assert.Nil,
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.tname, func() {
|
||||
t := suite.T()
|
||||
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
err := test.tree.SetFolder(
|
||||
ctx,
|
||||
test.parentID,
|
||||
test.id,
|
||||
test.name,
|
||||
test.isPackage)
|
||||
test.expectErr(t, err, clues.ToCore(err))
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result := test.tree.folderIDToNode[test.id]
|
||||
require.NotNil(t, result)
|
||||
test.expectPrev(t, result.prev)
|
||||
assert.Equal(t, test.id, result.id)
|
||||
assert.Equal(t, test.name, result.name)
|
||||
assert.Equal(t, test.isPackage, result.isPackage)
|
||||
|
||||
_, ded := test.tree.tombstones[test.id]
|
||||
assert.False(t, ded)
|
||||
|
||||
if len(test.parentID) > 0 {
|
||||
parent := test.tree.folderIDToNode[test.parentID]
|
||||
assert.Equal(t, parent, result.parent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_AddTombstone() {
|
||||
loc := path.NewElements("root:/foo/bar/baz/qux/fnords/smarf/voi/zumba/bangles/howdyhowdyhowdy")
|
||||
treeWithTombstone := func() *folderyMcFolderFace {
|
||||
tree := newFolderyMcFolderFace(nil)
|
||||
tree.tombstones["id"] = newNodeyMcNodeFace(nil, "id", "", loc, false)
|
||||
|
||||
return tree
|
||||
}
|
||||
|
||||
table := []struct {
|
||||
name string
|
||||
id string
|
||||
loc path.Elements
|
||||
tree *folderyMcFolderFace
|
||||
expectErr assert.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
name: "add tombstone",
|
||||
id: "id",
|
||||
loc: loc,
|
||||
tree: newFolderyMcFolderFace(nil),
|
||||
expectErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "duplicate tombstone",
|
||||
id: "id",
|
||||
loc: loc,
|
||||
tree: treeWithTombstone(),
|
||||
expectErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "missing ID",
|
||||
loc: loc,
|
||||
tree: newFolderyMcFolderFace(nil),
|
||||
expectErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "missing loc",
|
||||
id: "id",
|
||||
tree: newFolderyMcFolderFace(nil),
|
||||
expectErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "empty loc",
|
||||
id: "id",
|
||||
loc: path.Elements{},
|
||||
tree: newFolderyMcFolderFace(nil),
|
||||
expectErr: assert.Error,
|
||||
},
|
||||
{
|
||||
name: "conflict: folder alive",
|
||||
id: "id",
|
||||
loc: loc,
|
||||
tree: treeWithTombstone(),
|
||||
expectErr: assert.NoError,
|
||||
},
|
||||
{
|
||||
name: "already tombstoned with different path",
|
||||
id: "id",
|
||||
loc: append(path.Elements{"foo"}, loc...),
|
||||
tree: treeWithTombstone(),
|
||||
expectErr: assert.NoError,
|
||||
},
|
||||
}
|
||||
for _, test := range table {
|
||||
suite.Run(test.name, func() {
|
||||
t := suite.T()
|
||||
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
err := test.tree.SetTombstone(ctx, test.id, test.loc)
|
||||
test.expectErr(t, err, clues.ToCore(err))
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
result := test.tree.tombstones[test.id]
|
||||
require.NotNil(t, result)
|
||||
require.NotEmpty(t, result.prev)
|
||||
assert.Equal(t, loc, result.prev)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// tree structure assertions tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
type assertNode struct {
|
||||
self *nodeyMcNodeFace
|
||||
children []assertNode
|
||||
}
|
||||
|
||||
func an(
|
||||
self *nodeyMcNodeFace,
|
||||
children ...assertNode,
|
||||
) assertNode {
|
||||
return assertNode{
|
||||
self: self,
|
||||
children: children,
|
||||
}
|
||||
}
|
||||
|
||||
func (an assertNode) compare(
|
||||
t *testing.T,
|
||||
tree *folderyMcFolderFace,
|
||||
checkLiveNodeCount bool,
|
||||
) {
|
||||
var nodeCount int
|
||||
|
||||
t.Run("assert_tree_shape/root", func(_t *testing.T) {
|
||||
nodeCount = compareNodes(_t, tree.root, an)
|
||||
})
|
||||
|
||||
if checkLiveNodeCount {
|
||||
require.Len(t, tree.folderIDToNode, nodeCount, "total count of live nodes")
|
||||
}
|
||||
}
|
||||
|
||||
func compareNodes(
|
||||
t *testing.T,
|
||||
node *nodeyMcNodeFace,
|
||||
expect assertNode,
|
||||
) int {
|
||||
// ensure the nodes match
|
||||
require.NotNil(t, node, "node does not exist in tree")
|
||||
require.Equal(
|
||||
t,
|
||||
expect.self,
|
||||
node,
|
||||
"non-matching node")
|
||||
|
||||
// ensure the node has the expected number of children
|
||||
assert.Len(
|
||||
t,
|
||||
node.children,
|
||||
len(expect.children),
|
||||
"node has expected number of children")
|
||||
|
||||
var nodeCount int
|
||||
|
||||
for _, expectChild := range expect.children {
|
||||
t.Run(expectChild.self.id, func(_t *testing.T) {
|
||||
nodeChild := node.children[expectChild.self.id]
|
||||
require.NotNilf(
|
||||
_t,
|
||||
nodeChild,
|
||||
"child must exist with id %q",
|
||||
expectChild.self.id)
|
||||
|
||||
// ensure each child points to the current node as its parent
|
||||
assert.Equal(
|
||||
_t,
|
||||
node,
|
||||
nodeChild.parent,
|
||||
"should point to correct parent")
|
||||
|
||||
// recurse
|
||||
nodeCount += compareNodes(_t, nodeChild, expectChild)
|
||||
})
|
||||
}
|
||||
|
||||
return nodeCount + 1
|
||||
}
|
||||
|
||||
type tombs []assertNode
|
||||
|
||||
func entomb(nodes ...assertNode) tombs {
|
||||
if len(nodes) == 0 {
|
||||
return tombs{}
|
||||
}
|
||||
|
||||
return append(tombs{}, nodes...)
|
||||
}
|
||||
|
||||
func (ts tombs) compare(
|
||||
t *testing.T,
|
||||
tombstones map[string]*nodeyMcNodeFace,
|
||||
) {
|
||||
require.Len(t, tombstones, len(ts), "count of tombstoned nodes")
|
||||
|
||||
for _, entombed := range ts {
|
||||
zombey := tombstones[entombed.self.id]
|
||||
require.NotNil(t, zombey, "tombstone must exist")
|
||||
assert.Nil(t, zombey.parent, "tombstoned node should not have a parent reference")
|
||||
|
||||
t.Run("assert_tombstones/"+zombey.id, func(_t *testing.T) {
|
||||
compareNodes(_t, zombey, entombed)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// unlike the prior test, this focuses entirely on whether or not the
|
||||
// tree produced by folder additions is correct.
|
||||
func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder_correctTree() {
|
||||
t := suite.T()
|
||||
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
tree := newFolderyMcFolderFace(nil)
|
||||
rootID := odConsts.RootID
|
||||
|
||||
set := func(
|
||||
parentID, id, name string,
|
||||
isPackage bool,
|
||||
) {
|
||||
err := tree.SetFolder(ctx, parentID, id, name, isPackage)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
}
|
||||
|
||||
assert.Nil(t, tree.root)
|
||||
assert.Empty(t, tree.folderIDToNode)
|
||||
|
||||
// add the root
|
||||
set("", rootID, odConsts.RootPathDir, false)
|
||||
|
||||
assert.NotNil(t, tree.root)
|
||||
assert.Equal(t, rootID, tree.root.id)
|
||||
assert.Equal(t, rootID, tree.folderIDToNode[rootID].id)
|
||||
|
||||
an(tree.root).compare(t, tree, true)
|
||||
|
||||
// add a child at the root
|
||||
set(tree.root.id, "lefty", "l", false)
|
||||
|
||||
lefty := tree.folderIDToNode["lefty"]
|
||||
an(
|
||||
tree.root,
|
||||
an(lefty)).
|
||||
compare(t, tree, true)
|
||||
|
||||
// add another child at the root
|
||||
set(tree.root.id, "righty", "r", false)
|
||||
|
||||
righty := tree.folderIDToNode["righty"]
|
||||
an(
|
||||
tree.root,
|
||||
an(lefty),
|
||||
an(righty)).
|
||||
compare(t, tree, true)
|
||||
|
||||
// add a child to lefty
|
||||
set(lefty.id, "bloaty", "bl", false)
|
||||
|
||||
bloaty := tree.folderIDToNode["bloaty"]
|
||||
an(
|
||||
tree.root,
|
||||
an(lefty, an(bloaty)),
|
||||
an(righty)).
|
||||
compare(t, tree, true)
|
||||
|
||||
// add another child to lefty
|
||||
set(lefty.id, "brightly", "br", false)
|
||||
|
||||
brightly := tree.folderIDToNode["brightly"]
|
||||
an(
|
||||
tree.root,
|
||||
an(lefty, an(bloaty), an(brightly)),
|
||||
an(righty)).
|
||||
compare(t, tree, true)
|
||||
|
||||
// relocate brightly underneath righty
|
||||
set(righty.id, brightly.id, brightly.name, false)
|
||||
|
||||
an(
|
||||
tree.root,
|
||||
an(lefty, an(bloaty)),
|
||||
an(righty, an(brightly))).
|
||||
compare(t, tree, true)
|
||||
|
||||
// relocate righty and subtree beneath lefty
|
||||
set(lefty.id, righty.id, righty.name, false)
|
||||
|
||||
an(
|
||||
tree.root,
|
||||
an(
|
||||
lefty,
|
||||
an(bloaty),
|
||||
an(righty, an(brightly)))).
|
||||
compare(t, tree, true)
|
||||
}
|
||||
|
||||
// this test focuses on whether the tree is correct when bouncing back and forth
|
||||
// between live and tombstoned states on the same folder
|
||||
func (suite *DeltaTreeUnitSuite) TestFolderyMcFolderFace_SetFolder_correctTombstones() {
|
||||
t := suite.T()
|
||||
|
||||
ctx, flush := tester.NewContext(t)
|
||||
defer flush()
|
||||
|
||||
tree := newFolderyMcFolderFace(nil)
|
||||
rootID := odConsts.RootID
|
||||
|
||||
set := func(
|
||||
parentID, id, name string,
|
||||
isPackage bool,
|
||||
) {
|
||||
err := tree.SetFolder(ctx, parentID, id, name, isPackage)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
}
|
||||
|
||||
tomb := func(
|
||||
id string,
|
||||
loc path.Elements,
|
||||
) {
|
||||
err := tree.SetTombstone(ctx, id, loc)
|
||||
require.NoError(t, err, clues.ToCore(err))
|
||||
}
|
||||
|
||||
assert.Nil(t, tree.root)
|
||||
assert.Empty(t, tree.folderIDToNode)
|
||||
|
||||
// create a simple tree
|
||||
// root > branchy > [leafy, bob]
|
||||
set("", rootID, odConsts.RootPathDir, false)
|
||||
|
||||
set(tree.root.id, "branchy", "br", false)
|
||||
branchy := tree.folderIDToNode["branchy"]
|
||||
|
||||
set(branchy.id, "leafy", "l", false)
|
||||
set(branchy.id, "bob", "bobbers", false)
|
||||
|
||||
leafy := tree.folderIDToNode["leafy"]
|
||||
bob := tree.folderIDToNode["bob"]
|
||||
|
||||
an(
|
||||
tree.root,
|
||||
an(
|
||||
branchy,
|
||||
an(leafy),
|
||||
an(bob))).
|
||||
compare(t, tree, true)
|
||||
|
||||
entomb().compare(t, tree.tombstones)
|
||||
|
||||
var (
|
||||
branchyLoc = path.NewElements("root/branchy")
|
||||
leafyLoc = path.NewElements("root/branchy/leafy")
|
||||
bobLoc = path.NewElements("root/branchy/bob")
|
||||
)
|
||||
|
||||
// tombstone bob
|
||||
tomb(bob.id, bobLoc)
|
||||
an(
|
||||
tree.root,
|
||||
an(branchy, an(leafy))).
|
||||
compare(t, tree, true)
|
||||
|
||||
entomb(an(bob)).compare(t, tree.tombstones)
|
||||
|
||||
// tombstone leafy
|
||||
tomb(leafy.id, leafyLoc)
|
||||
an(
|
||||
tree.root,
|
||||
an(branchy)).
|
||||
compare(t, tree, true)
|
||||
|
||||
entomb(an(bob), an(leafy)).compare(t, tree.tombstones)
|
||||
|
||||
// resurrect leafy
|
||||
set(branchy.id, leafy.id, leafy.name, false)
|
||||
|
||||
an(
|
||||
tree.root,
|
||||
an(branchy, an(leafy))).
|
||||
compare(t, tree, true)
|
||||
|
||||
entomb(an(bob)).compare(t, tree.tombstones)
|
||||
|
||||
// resurrect bob
|
||||
set(branchy.id, bob.id, bob.name, false)
|
||||
|
||||
an(
|
||||
tree.root,
|
||||
an(
|
||||
branchy,
|
||||
an(leafy),
|
||||
an(bob))).
|
||||
compare(t, tree, true)
|
||||
|
||||
entomb().compare(t, tree.tombstones)
|
||||
|
||||
// tombstone branchy
|
||||
tomb(branchy.id, branchyLoc)
|
||||
|
||||
an(tree.root).compare(t, tree, false)
|
||||
// note: the folder count here *will be wrong*.
|
||||
// since we've only tombstoned branchy, both leafy
|
||||
// and bob will remain in the folderIDToNode map.
|
||||
// If this were real graph behavior, the delete would
|
||||
// necessarily cascade and those children would get
|
||||
// tombstoned next.
|
||||
// So we skip the check here, just to minimize code.
|
||||
// It's safe to do so, for the scope of this test.
|
||||
// This should be part of the consideration for prev-
|
||||
// path iteration that could create improper state in
|
||||
// the post-processing stage if we're nott careful.
|
||||
|
||||
entomb(
|
||||
an(
|
||||
branchy,
|
||||
an(leafy),
|
||||
an(bob))).
|
||||
compare(t, tree.tombstones)
|
||||
|
||||
// resurrect branchy
|
||||
set(tree.root.id, branchy.id, branchy.name, false)
|
||||
|
||||
an(
|
||||
tree.root,
|
||||
an(
|
||||
branchy,
|
||||
an(leafy),
|
||||
an(bob))).
|
||||
compare(t, tree, true)
|
||||
|
||||
entomb().compare(t, tree.tombstones)
|
||||
|
||||
// tombstone branchy
|
||||
tomb(branchy.id, branchyLoc)
|
||||
|
||||
an(tree.root).compare(t, tree, false)
|
||||
|
||||
entomb(
|
||||
an(
|
||||
branchy,
|
||||
an(leafy),
|
||||
an(bob))).
|
||||
compare(t, tree.tombstones)
|
||||
|
||||
// tombstone bob
|
||||
tomb(bob.id, bobLoc)
|
||||
|
||||
an(tree.root).compare(t, tree, false)
|
||||
|
||||
entomb(
|
||||
an(branchy, an(leafy)),
|
||||
an(bob)).
|
||||
compare(t, tree.tombstones)
|
||||
|
||||
// resurrect branchy
|
||||
set(tree.root.id, branchy.id, branchy.name, false)
|
||||
|
||||
an(
|
||||
tree.root,
|
||||
an(branchy, an(leafy))).
|
||||
compare(t, tree, false)
|
||||
|
||||
entomb(an(bob)).compare(t, tree.tombstones)
|
||||
|
||||
// resurrect bob
|
||||
set(branchy.id, bob.id, bob.name, false)
|
||||
|
||||
an(
|
||||
tree.root,
|
||||
an(
|
||||
branchy,
|
||||
an(leafy),
|
||||
an(bob))).
|
||||
compare(t, tree, true)
|
||||
|
||||
entomb().compare(t, tree.tombstones)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user