Keepers 7419faab23
refactor drive restore & export to use sanitree (#4425)
refactors the common drive sanity checks to use the sanitree data container.  Also expands the sanitree in two ways:
1. adds leaves (individual items) to nodes for granular data comparison
2. adds multi-type support for comparing nodes of different types.

---

#### Does this PR need a docs update or release note?

- [x]  No

#### Type of change

- [x] 🤖 Supportability/Tests

#### Issue(s)

* #3988

#### Test Plan

- [x] 💪 Manual
- [x] 💚 E2E
2023-10-13 22:55:35 +00:00

270 lines
6.0 KiB
Go

package common
import (
"context"
"github.com/alcionai/clues"
"golang.org/x/exp/maps"
"github.com/alcionai/corso/src/pkg/path"
)
type Sanileaf[T, L any] struct {
Parent *Sanitree[T, L]
Self L
ID string
Name string
Size int64
// Expand is an arbitrary k:v map of any data that is
// uniquely scrutinized by a given service.
Expand map[string]any
}
// Sanitree is used to build out a hierarchical tree of items
// for comparison against each other. Primarily so that a restore
// can compare two subtrees easily.
type Sanitree[T, L any] struct {
Parent *Sanitree[T, L]
Self T
ID string
Name string
// CountLeaves is the number of non-container child items.
// Used for services that don't need full item metadata, and
// just want a count of children.
CountLeaves int
// leaves are non-container child items. Used by services
// that need more than just a count of items.
// name (or equivalent) -> leaf
Leaves map[string]*Sanileaf[T, L]
// Children holds all child containers
// name -> node
Children map[string]*Sanitree[T, L]
// Expand is an arbitrary k:v map of any data that is
// uniquely scrutinized by a given service.
Expand map[string]any
}
func (s *Sanitree[T, L]) Path() path.Elements {
if s.Parent == nil {
return path.NewElements(s.Name)
}
fp := s.Parent.Path()
return append(fp, s.Name)
}
func (s *Sanitree[T, L]) NodeAt(
ctx context.Context,
elems []string,
) *Sanitree[T, L] {
node := s
for _, e := range elems {
child, ok := node.Children[e]
Assert(
ctx,
func() bool { return ok },
"tree node should contain next child",
s.Path(),
maps.Keys(s.Children))
node = child
}
return node
}
// ---------------------------------------------------------------------------
// Comparing trees
// ---------------------------------------------------------------------------
type (
ContainerComparatorFn[ET, EL, RT, RL any] func(
ctx context.Context,
expect *Sanitree[ET, EL],
result *Sanitree[RT, RL])
LeafComparatorFn[ET, EL, RT, RL any] func(
ctx context.Context,
expect *Sanileaf[ET, EL],
result *Sanileaf[RT, RL])
)
func AssertEqualTrees[ET, EL, RT, RL any](
ctx context.Context,
expect *Sanitree[ET, EL],
result *Sanitree[RT, RL],
customContainerCheck ContainerComparatorFn[ET, EL, RT, RL],
customLeafCheck LeafComparatorFn[ET, EL, RT, RL],
) {
if expect == nil && result == nil {
return
}
Debugf(ctx, "comparing trees at path: %+v", expect.Path())
checkChildrenAndLeaves(ctx, expect, result)
ctx = clues.Add(ctx, "container_name", expect.Name)
if customContainerCheck != nil {
customContainerCheck(ctx, expect, result)
}
CompareLeaves[ET, EL, RT, RL](
ctx,
expect.Leaves,
result.Leaves,
customLeafCheck)
// recurse
for name, s := range expect.Children {
r, ok := result.Children[name]
Assert(
ctx,
func() bool { return ok },
"found matching child container",
name,
maps.Keys(result.Children))
AssertEqualTrees(ctx, s, r, customContainerCheck, customLeafCheck)
}
}
// ---------------------------------------------------------------------------
// Comparing differently typed trees.
// ---------------------------------------------------------------------------
type NodeComparator[ET, EL, RT, RL any] func(
ctx context.Context,
expect *Sanitree[ET, EL],
result *Sanitree[RT, RL],
)
// CompareDiffTrees recursively compares two sanitrees that have
// different data types. The two trees are expected to represent
// a common hierarchy.
//
// Additional comparisons besides the tre hierarchy are optionally
// left to the caller by population of the NodeComparator func.
func CompareDiffTrees[ET, EL, RT, RL any](
ctx context.Context,
expect *Sanitree[ET, EL],
result *Sanitree[RT, RL],
comparator NodeComparator[ET, EL, RT, RL],
) {
if expect == nil && result == nil {
return
}
Debugf(ctx, "comparing tree at path: %+v", expect.Path())
checkChildrenAndLeaves(ctx, expect, result)
ctx = clues.Add(ctx, "container_name", expect.Name)
if comparator != nil {
comparator(ctx, expect, result)
}
// recurse
for name, s := range expect.Children {
r, ok := result.Children[name]
Assert(
ctx,
func() bool { return ok },
"found matching child container",
name,
maps.Keys(result.Children))
CompareDiffTrees(ctx, s, r, comparator)
}
}
// ---------------------------------------------------------------------------
// Checking hierarchy likeness
// ---------------------------------------------------------------------------
func checkChildrenAndLeaves[ET, EL, RT, RL any](
ctx context.Context,
expect *Sanitree[ET, EL],
result *Sanitree[RT, RL],
) {
Assert(
ctx,
func() bool { return expect != nil },
"expected stree is nil",
"not nil",
expect)
Assert(
ctx,
func() bool { return result != nil },
"result stree is nil",
"not nil",
result)
ctx = clues.Add(ctx, "container_name", expect.Name)
Assert(
ctx,
func() bool { return expect.Name == result.Name },
"container names match",
expect.Name,
result.Name)
Assert(
ctx,
func() bool { return expect.CountLeaves == result.CountLeaves },
"count of leaves in container matches",
expect.CountLeaves,
result.CountLeaves)
Assert(
ctx,
func() bool { return len(expect.Leaves) == len(result.Leaves) },
"len of leaves in container matches",
len(expect.Leaves),
len(result.Leaves))
Assert(
ctx,
func() bool { return len(expect.Children) == len(result.Children) },
"count of child containers matches",
len(expect.Children),
len(result.Children))
}
func CompareLeaves[ET, EL, RT, RL any](
ctx context.Context,
expect map[string]*Sanileaf[ET, EL],
result map[string]*Sanileaf[RT, RL],
customLeafCheck LeafComparatorFn[ET, EL, RT, RL],
) {
for name, l := range expect {
ictx := clues.Add(ctx, "leaf_name", l.Name)
r, ok := result[name]
Assert(
ictx,
func() bool { return ok },
"found matching leaf item",
name,
maps.Keys(result))
Assert(
ictx,
func() bool { return l.Size == r.Size },
"leaf sizes match",
l.Size,
r.Size)
if customLeafCheck != nil {
customLeafCheck(ictx, l, r)
}
}
}