Stricter checking for how hierarchies are merged (#1909)
## Description Add some extra error checking for how the hierarchy can evolve during merging in kopia.Wrapper. Add more tests to solidify this behavior as well. ## Does this PR need a docs update or release note? - [ ] ✅ Yes, it's included - [ ] 🕐 Yes, but in a later PR - [x] ⛔ No ## Type of change - [ ] 🌻 Feature - [x] 🐛 Bugfix - [ ] 🗺️ Documentation - [x] 🤖 Test - [ ] 💻 CI/Deployment - [ ] 🐹 Trivial/Minor ## Issue(s) * closes #1884 ## Test Plan <!-- How will this be tested prior to merging.--> - [ ] 💪 Manual - [x] ⚡ Unit test - [ ] 💚 E2E
This commit is contained in:
parent
741b36da98
commit
5243dddcbf
@ -455,6 +455,29 @@ func newTreeMap() *treeMap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// maybeGetTreeNode walks the tree(s) with roots roots and returns the node
|
||||||
|
// specified by pathElements if all nodes on the path exist. If pathElements is
|
||||||
|
// nil or empty then returns nil.
|
||||||
|
func maybeGetTreeNode(roots map[string]*treeMap, pathElements []string) *treeMap {
|
||||||
|
if len(pathElements) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dir := roots[pathElements[0]]
|
||||||
|
|
||||||
|
for i := 1; i < len(pathElements); i++ {
|
||||||
|
if dir == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
p := pathElements[i]
|
||||||
|
|
||||||
|
dir = dir.childDirs[p]
|
||||||
|
}
|
||||||
|
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
// getTreeNode walks the tree(s) with roots roots and returns the node specified
|
// getTreeNode walks the tree(s) with roots roots and returns the node specified
|
||||||
// by pathElements. If pathElements is nil or empty then returns nil. Tree nodes
|
// by pathElements. If pathElements is nil or empty then returns nil. Tree nodes
|
||||||
// are created for any path elements where a node is not already present.
|
// are created for any path elements where a node is not already present.
|
||||||
@ -500,6 +523,9 @@ func inflateCollectionTree(
|
|||||||
// Allows resolving what the new path should be when walking the base
|
// Allows resolving what the new path should be when walking the base
|
||||||
// snapshot(s)'s hierarchy. Nil represents a collection that was deleted.
|
// snapshot(s)'s hierarchy. Nil represents a collection that was deleted.
|
||||||
updatedPaths := make(map[string]path.Path)
|
updatedPaths := make(map[string]path.Path)
|
||||||
|
// Temporary variable just to track the things that have been marked as
|
||||||
|
// changed while keeping a reference to their path.
|
||||||
|
changedPaths := []path.Path{}
|
||||||
ownerCats := &OwnersCats{
|
ownerCats := &OwnersCats{
|
||||||
ResourceOwners: make(map[string]struct{}),
|
ResourceOwners: make(map[string]struct{}),
|
||||||
ServiceCats: make(map[string]ServiceCat),
|
ServiceCats: make(map[string]ServiceCat),
|
||||||
@ -508,10 +534,29 @@ func inflateCollectionTree(
|
|||||||
for _, s := range collections {
|
for _, s := range collections {
|
||||||
switch s.State() {
|
switch s.State() {
|
||||||
case data.DeletedState:
|
case data.DeletedState:
|
||||||
|
changedPaths = append(changedPaths, s.PreviousPath())
|
||||||
|
|
||||||
|
if _, ok := updatedPaths[s.PreviousPath().String()]; ok {
|
||||||
|
return nil, nil, errors.Errorf(
|
||||||
|
"multiple previous state changes to collection %s",
|
||||||
|
s.PreviousPath(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
updatedPaths[s.PreviousPath().String()] = nil
|
updatedPaths[s.PreviousPath().String()] = nil
|
||||||
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
case data.MovedState:
|
case data.MovedState:
|
||||||
|
changedPaths = append(changedPaths, s.PreviousPath())
|
||||||
|
|
||||||
|
if _, ok := updatedPaths[s.PreviousPath().String()]; ok {
|
||||||
|
return nil, nil, errors.Errorf(
|
||||||
|
"multiple previous state changes to collection %s",
|
||||||
|
s.PreviousPath(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
updatedPaths[s.PreviousPath().String()] = s.FullPath()
|
updatedPaths[s.PreviousPath().String()] = s.FullPath()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -531,9 +576,29 @@ func inflateCollectionTree(
|
|||||||
ownerCats.ServiceCats[serviceCat] = ServiceCat{}
|
ownerCats.ServiceCats[serviceCat] = ServiceCat{}
|
||||||
ownerCats.ResourceOwners[s.FullPath().ResourceOwner()] = struct{}{}
|
ownerCats.ResourceOwners[s.FullPath().ResourceOwner()] = struct{}{}
|
||||||
|
|
||||||
|
// Make sure there's only a single collection adding items for any given
|
||||||
|
// path in the new hierarchy.
|
||||||
|
if node.collection != nil {
|
||||||
|
return nil, nil, errors.Errorf("multiple instances of collection at %s", s.FullPath())
|
||||||
|
}
|
||||||
|
|
||||||
node.collection = s
|
node.collection = s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that each previous path has only one of the states of deleted, moved,
|
||||||
|
// or notmoved. Check at the end to avoid issues like seeing a notmoved state
|
||||||
|
// collection and then a deleted state collection.
|
||||||
|
for _, p := range changedPaths {
|
||||||
|
node := maybeGetTreeNode(roots, p.Elements())
|
||||||
|
if node == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.collection != nil && node.collection.State() == data.NotMovedState {
|
||||||
|
return nil, nil, errors.Errorf("conflicting states for collection %s", p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return roots, updatedPaths, nil
|
return roots, updatedPaths, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -816,11 +816,93 @@ func mockIncrementalBase(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeErrors() {
|
||||||
|
dirPath := makePath(
|
||||||
|
suite.T(),
|
||||||
|
[]string{testTenant, service, testUser, category, testInboxDir},
|
||||||
|
)
|
||||||
|
dirPath2 := makePath(
|
||||||
|
suite.T(),
|
||||||
|
[]string{testTenant, service, testUser, category, testArchiveDir},
|
||||||
|
)
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
states []data.CollectionState
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "DeletedAndNotMoved",
|
||||||
|
states: []data.CollectionState{
|
||||||
|
data.NotMovedState,
|
||||||
|
data.DeletedState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NotMovedAndDeleted",
|
||||||
|
states: []data.CollectionState{
|
||||||
|
data.DeletedState,
|
||||||
|
data.NotMovedState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DeletedAndMoved",
|
||||||
|
states: []data.CollectionState{
|
||||||
|
data.DeletedState,
|
||||||
|
data.MovedState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NotMovedAndMoved",
|
||||||
|
states: []data.CollectionState{
|
||||||
|
data.NotMovedState,
|
||||||
|
data.MovedState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
suite.T().Run(test.name, func(t *testing.T) {
|
||||||
|
tester.LogTimeOfTest(t)
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext()
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
progress := &corsoProgress{pending: map[string]*itemDetails{}}
|
||||||
|
|
||||||
|
cols := []data.Collection{}
|
||||||
|
for _, s := range test.states {
|
||||||
|
prevPath := dirPath
|
||||||
|
nowPath := dirPath
|
||||||
|
|
||||||
|
switch s {
|
||||||
|
case data.DeletedState:
|
||||||
|
nowPath = nil
|
||||||
|
case data.MovedState:
|
||||||
|
nowPath = dirPath2
|
||||||
|
}
|
||||||
|
|
||||||
|
mc := mockconnector.NewMockExchangeCollection(nowPath, 0)
|
||||||
|
mc.ColState = s
|
||||||
|
mc.PrevPath = prevPath
|
||||||
|
|
||||||
|
cols = append(cols, mc)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := inflateDirTree(ctx, nil, nil, cols, progress)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
||||||
dirPath := makePath(
|
dirPath := makePath(
|
||||||
suite.T(),
|
suite.T(),
|
||||||
[]string{testTenant, service, testUser, category, testInboxDir},
|
[]string{testTenant, service, testUser, category, testInboxDir},
|
||||||
)
|
)
|
||||||
|
dirPath2 := makePath(
|
||||||
|
suite.T(),
|
||||||
|
[]string{testTenant, service, testUser, category, testArchiveDir},
|
||||||
|
)
|
||||||
|
|
||||||
// Must be a function that returns a new instance each time as StreamingFile
|
// Must be a function that returns a new instance each time as StreamingFile
|
||||||
// can only return its Reader once.
|
// can only return its Reader once.
|
||||||
@ -942,6 +1024,85 @@ func (suite *HierarchyBuilderUnitSuite) TestBuildDirectoryTreeSingleSubtree() {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "DeleteAndNew",
|
||||||
|
inputCollections: func() []data.Collection {
|
||||||
|
mc1 := mockconnector.NewMockExchangeCollection(dirPath, 0)
|
||||||
|
mc1.ColState = data.DeletedState
|
||||||
|
mc1.PrevPath = dirPath
|
||||||
|
|
||||||
|
mc2 := mockconnector.NewMockExchangeCollection(dirPath, 1)
|
||||||
|
mc2.ColState = data.NewState
|
||||||
|
mc2.Names[0] = testFileName2
|
||||||
|
mc2.Data[0] = testFileData2
|
||||||
|
|
||||||
|
return []data.Collection{mc1, mc2}
|
||||||
|
},
|
||||||
|
expected: expectedTreeWithChildren(
|
||||||
|
[]string{
|
||||||
|
testTenant,
|
||||||
|
service,
|
||||||
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
|
[]*expectedNode{
|
||||||
|
{
|
||||||
|
name: testInboxDir,
|
||||||
|
children: []*expectedNode{
|
||||||
|
{
|
||||||
|
name: testFileName2,
|
||||||
|
children: []*expectedNode{},
|
||||||
|
data: testFileData2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MovedAndNew",
|
||||||
|
inputCollections: func() []data.Collection {
|
||||||
|
mc1 := mockconnector.NewMockExchangeCollection(dirPath2, 0)
|
||||||
|
mc1.ColState = data.MovedState
|
||||||
|
mc1.PrevPath = dirPath
|
||||||
|
|
||||||
|
mc2 := mockconnector.NewMockExchangeCollection(dirPath, 1)
|
||||||
|
mc2.ColState = data.NewState
|
||||||
|
mc2.Names[0] = testFileName2
|
||||||
|
mc2.Data[0] = testFileData2
|
||||||
|
|
||||||
|
return []data.Collection{mc1, mc2}
|
||||||
|
},
|
||||||
|
expected: expectedTreeWithChildren(
|
||||||
|
[]string{
|
||||||
|
testTenant,
|
||||||
|
service,
|
||||||
|
testUser,
|
||||||
|
category,
|
||||||
|
},
|
||||||
|
[]*expectedNode{
|
||||||
|
{
|
||||||
|
name: testInboxDir,
|
||||||
|
children: []*expectedNode{
|
||||||
|
{
|
||||||
|
name: testFileName2,
|
||||||
|
children: []*expectedNode{},
|
||||||
|
data: testFileData2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: testArchiveDir,
|
||||||
|
children: []*expectedNode{
|
||||||
|
{
|
||||||
|
name: testFileName,
|
||||||
|
children: []*expectedNode{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user