refactor scope filtering (#555)

Scope filtering is currently hardcoded to the exchange
use case.  In order for future work to rely on boilerplate
rather than re-writing the full filtering logic on each new
type, as much of that code as is possible has been moved
into a generic toolset.
This commit is contained in:
Keepers 2022-08-22 13:08:14 -06:00 committed by GitHub
parent 7d057dd2ac
commit 12dbfce6d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 460 additions and 208 deletions

View File

@ -480,7 +480,7 @@ func (ec exchangeCategory) includesType(cat categorizer) bool {
// => {exchUser: userID, exchMailFolder: mailFolder, exchMail: mailID}
func (ec exchangeCategory) pathValues(path []string) map[categorizer]string {
m := map[categorizer]string{}
if len(path) < 4 {
if len(path) < 2 {
return m
}
m[ExchangeUser] = path[1]
@ -528,7 +528,7 @@ func (ec exchangeCategory) pathKeys() []categorizer {
type ExchangeScope scope
// interface compliance checks
// var _ scoper = &ExchangeScope{}
var _ scoper = &ExchangeScope{}
// Category describes the type of the data in scope.
func (s ExchangeScope) Category() exchangeCategory {
@ -541,8 +541,15 @@ func (s ExchangeScope) categorizer() categorizer {
return s.Category()
}
// Filer describes the specific filter, and its target values.
func (s ExchangeScope) Filter() exchangeCategory {
// Contains returns true if the category is included in the scope's
// data type, and the target string is included in the scope.
func (s ExchangeScope) Contains(cat exchangeCategory, target string) bool {
return contains(s, cat, target)
}
// FilterCategory returns the category enum of the scope filter.
// If the scope is not a filter type, returns ExchangeUnknownCategory.
func (s ExchangeScope) FilterCategory() exchangeCategory {
return exchangeCatAtoI(s[scopeKeyInfoFilter])
}
@ -552,20 +559,13 @@ func (s ExchangeScope) Granularity() string {
return s[scopeKeyGranularity]
}
// IncludeCategory checks whether the scope includes a
// certain category of data.
// IncludeCategory checks whether the scope includes a certain category of data.
// Ex: to check if the scope includes mail data:
// s.IncludesCategory(selector.ExchangeMail)
func (s ExchangeScope) IncludesCategory(cat exchangeCategory) bool {
return s.Category().isType(cat)
}
// Contains returns true if the category is included in the scope's
// data type, and the target string is included in the scope.
func (s ExchangeScope) Contains(cat exchangeCategory, target string) bool {
return contains(s, cat, target)
}
// returns true if the category is included in the scope's data type,
// and the value is set to Any().
func (s ExchangeScope) IsAny(cat exchangeCategory) bool {
@ -606,31 +606,39 @@ func (s ExchangeScope) setDefaults() {
// Backup Details Filtering
// ---------------------------------------------------------------------------
// Reduce filters the entries in a details struct to only those that match the
// inclusions, filters, and exclusions in the selector.
func (s exchange) Reduce(deets *details.Details) *details.Details {
return reduce[ExchangeScope](
deets,
s.Selector,
map[pathType]exchangeCategory{
exchangeContactPath: ExchangeContact,
exchangeEventPath: ExchangeEvent,
exchangeMailPath: ExchangeMail,
},
)
}
// matchesEntry returns true if either the path or the info in the exchangeEntry matches the scope details.
func (s ExchangeScope) matchesEntry(
cat categorizer,
pathValues map[categorizer]string,
entry details.DetailsEntry,
) bool {
return false
// TODO: uncomment when reducer is added.
// return matchesPathValues(s, cat, pathValues) || s.matchesInfo(entry.Exchange)
}
// matches returns true if either the path or the info matches the scope details.
func (s ExchangeScope) matches(cat exchangeCategory, path []string, info *details.ExchangeInfo) bool {
return s.matchesPath(cat, path) || s.matchesInfo(cat, info)
// matchesPathValues can be handled generically, thanks to SCIENCE.
return matchesPathValues(s, cat, pathValues) || s.matchesInfo(entry.Exchange)
}
// matchesInfo handles the standard behavior when comparing a scope and an exchangeInfo
// returns true if the scope and info match for the provided category.
func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeInfo) bool {
func (s ExchangeScope) matchesInfo(info *details.ExchangeInfo) bool {
// we need values to match against
if info == nil {
return false
}
// the scope must define targets to match on
filterCat := s.Filter()
filterCat := s.FilterCategory()
targets := s.Get(filterCat)
if len(targets) == 0 {
return false
@ -664,152 +672,3 @@ func (s ExchangeScope) matchesInfo(cat exchangeCategory, info *details.ExchangeI
}
return false
}
// matchesPath handles the standard behavior when comparing a scope and a path
// returns true if the scope and path match for the provided category.
func (s ExchangeScope) matchesPath(cat exchangeCategory, path []string) bool {
pathIDs := cat.pathValues(path)
for _, c := range categoryPathSet[cat] {
target := s.Get(c.(exchangeCategory))
// the scope must define the targets to match on
if len(target) == 0 {
return false
}
// None() fails all matches
if target[0] == NoneTgt {
return false
}
// the path must contain a value to match against
id, ok := pathIDs[c]
if !ok {
return false
}
// all parts of the scope must match
isAny := target[0] == AnyTgt
if !isAny {
if !common.ContainsString(target, id) {
return false
}
}
}
return true
}
// ---------------------------------------------------------------------------
// Restore Point Filtering
// ---------------------------------------------------------------------------
// Reduce reduces the entries in a backupDetails struct to only
// those that match the inclusions, filters, and exclusions in the selector.
func (sr *ExchangeRestore) Reduce(deets *details.Details) *details.Details {
if deets == nil {
return nil
}
entExcs := exchangeScopesByCategory(sr.Excludes)
entFilt := exchangeScopesByCategory(sr.Filters)
entIncs := exchangeScopesByCategory(sr.Includes)
ents := []details.DetailsEntry{}
for _, ent := range deets.Entries {
// todo: use Path pkg for this
path := strings.Split(ent.RepoRef, "/")
// not all paths will be len=3. Most should be longer.
// This just protects us from panicing four lines later.
if len(path) < 3 {
continue
}
var cat exchangeCategory
switch path[2] {
case "contact":
cat = ExchangeContact
case "event":
cat = ExchangeEvent
case "mail":
cat = ExchangeMail
}
matched := matchExchangeEntry(
cat,
path,
ent.Exchange,
entExcs[cat.String()],
entFilt[cat.String()],
entIncs[cat.String()])
if matched {
ents = append(ents, ent)
}
}
deets.Entries = ents
return deets
}
// groups each scope by its category of data (contact, event, or mail).
// user-level scopes will duplicate to all three categories.
func exchangeScopesByCategory(scopes []scope) map[string][]ExchangeScope {
m := map[string][]ExchangeScope{
ExchangeContact.String(): {},
ExchangeEvent.String(): {},
ExchangeMail.String(): {},
}
for _, msc := range scopes {
sc := ExchangeScope(msc)
if sc.IncludesCategory(ExchangeContact) {
m[ExchangeContact.String()] = append(m[ExchangeContact.String()], sc)
}
if sc.IncludesCategory(ExchangeEvent) {
m[ExchangeEvent.String()] = append(m[ExchangeEvent.String()], sc)
}
if sc.IncludesCategory(ExchangeMail) {
m[ExchangeMail.String()] = append(m[ExchangeMail.String()], sc)
}
}
return m
}
// compare each path to the included and excluded exchange scopes. Returns true
// if the path is included, passes filters, and not excluded.
func matchExchangeEntry(
cat exchangeCategory,
path []string,
info *details.ExchangeInfo,
excs, filts, incs []ExchangeScope,
) bool {
// a passing match requires either a filter or an inclusion
if len(incs)+len(filts) == 0 {
return false
}
// skip this check if 0 inclusions were populated
// since filters act as the inclusion check in that case
if len(incs) > 0 {
// at least one inclusion must apply.
var included bool
for _, inc := range incs {
if inc.matches(cat, path, info) {
included = true
break
}
}
if !included {
return false
}
}
// all filters must pass
for _, filt := range filts {
if !filt.matches(cat, path, info) {
return false
}
}
// any matching exclusion means failure
for _, exc := range excs {
if exc.matches(cat, path, info) {
return false
}
}
return true
}

View File

@ -67,7 +67,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Contacts() {
sel.Exclude(sel.Contacts([]string{user}, []string{folder}, []string{c1, c2}))
scopes := sel.Excludes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -88,7 +88,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_Contacts() {
sel.Include(sel.Contacts([]string{user}, []string{folder}, []string{c1, c2}))
scopes := sel.Includes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -110,7 +110,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_ContactFolders()
sel.Exclude(sel.ContactFolders([]string{user}, []string{f1, f2}))
scopes := sel.Excludes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -130,7 +130,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_ContactFolders()
sel.Include(sel.ContactFolders([]string{user}, []string{f1, f2}))
scopes := sel.Includes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -152,7 +152,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Events() {
sel.Exclude(sel.Events([]string{user}, []string{e1, e2}))
scopes := sel.Excludes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -171,7 +171,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_Events() {
sel.Include(sel.Events([]string{user}, []string{e1, e2}))
scopes := sel.Includes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -193,7 +193,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Mails() {
sel.Exclude(sel.Mails([]string{user}, []string{folder}, []string{m1, m2}))
scopes := sel.Excludes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -214,7 +214,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_Mails() {
sel.Include(sel.Mails([]string{user}, []string{folder}, []string{m1, m2}))
scopes := sel.Includes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -236,7 +236,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_MailFolders() {
sel.Exclude(sel.MailFolders([]string{user}, []string{f1, f2}))
scopes := sel.Excludes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -256,7 +256,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_MailFolders() {
sel.Include(sel.MailFolders([]string{user}, []string{f1, f2}))
scopes := sel.Includes
require.Equal(t, 1, len(scopes))
require.Len(t, scopes, 1)
scope := scopes[0]
assert.Equal(t, scope[ExchangeUser.String()], user)
@ -277,7 +277,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Exclude_Users() {
sel.Exclude(sel.Users([]string{u1, u2}))
scopes := sel.Excludes
require.Equal(t, 6, len(scopes))
require.Len(t, scopes, 6)
for _, scope := range scopes {
assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()])
@ -306,7 +306,7 @@ func (suite *ExchangeSourceSuite) TestExchangeSelector_Include_Users() {
sel.Include(sel.Users([]string{u1, u2}))
scopes := sel.Includes
require.Equal(t, 6, len(scopes))
require.Len(t, scopes, 6)
for _, scope := range scopes {
assert.Contains(t, join(u1, u2), scope[ExchangeUser.String()])
@ -531,7 +531,7 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_MatchesInfo() {
suite.T().Run(test.name, func(t *testing.T) {
scopes := setScopesToDefault(test.scope)
for _, scope := range scopes {
test.expect(t, scope.matchesInfo(scope.Category(), info))
test.expect(t, scope.matchesInfo(info))
}
})
}
@ -574,7 +574,8 @@ func (suite *ExchangeSourceSuite) TestExchangeScope_MatchesPath() {
scopes := setScopesToDefault(test.scope)
var aMatch bool
for _, scope := range scopes {
if scope.matchesPath(ExchangeMail, path) {
pv := ExchangeMail.pathValues(path)
if matchesPathValues(scope, ExchangeMail, pv) {
aMatch = true
break
}
@ -781,7 +782,7 @@ func (suite *ExchangeSourceSuite) TestExchangeRestore_Reduce() {
}
}
func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
func (suite *ExchangeSourceSuite) TestScopesByCategory() {
var (
es = NewExchangeRestore()
users = es.Users(Any())
@ -804,6 +805,11 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
}
return mss
}
cats := map[pathType]exchangeCategory{
exchangeContactPath: ExchangeContact,
exchangeEventPath: ExchangeEvent,
exchangeMailPath: ExchangeMail,
}
table := []struct {
name string
scopes input
@ -817,16 +823,16 @@ func (suite *ExchangeSourceSuite) TestExchangeScopesByCategory() {
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
result := exchangeScopesByCategory(test.scopes)
assert.Equal(t, test.expect.contact, len(result[ExchangeContact.String()]))
assert.Equal(t, test.expect.event, len(result[ExchangeEvent.String()]))
assert.Equal(t, test.expect.mail, len(result[ExchangeMail.String()]))
result := scopesByCategory[ExchangeScope](test.scopes, cats)
assert.Len(t, result[ExchangeContact], test.expect.contact)
assert.Len(t, result[ExchangeEvent], test.expect.event)
assert.Len(t, result[ExchangeMail], test.expect.mail)
})
}
}
func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
var exchangeInfo *details.ExchangeInfo
func (suite *ExchangeSourceSuite) TestPasses() {
deets := details.DetailsEntry{}
const (
mid = "mailID"
cat = ExchangeMail
@ -867,7 +873,14 @@ func (suite *ExchangeSourceSuite) TestMatchExchangeEntry() {
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
test.expect(t, matchExchangeEntry(cat, path, exchangeInfo, test.excludes, test.filters, test.includes))
result := passes(
cat,
cat.pathValues(path),
deets,
test.excludes,
test.filters,
test.includes)
test.expect(t, result)
})
}
}
@ -1049,7 +1062,7 @@ func (suite *ExchangeSourceSuite) TestExchangeCategory_PathValues() {
expect map[categorizer]string
}{
{ExchangeCategoryUnknown, nil, map[categorizer]string{}},
{ExchangeCategoryUnknown, []string{"a", "b", "c"}, map[categorizer]string{}},
{ExchangeCategoryUnknown, []string{"a"}, map[categorizer]string{}},
{ExchangeContact, contactPath, contactMap},
{ExchangeEvent, eventPath, eventMap},
{ExchangeMail, mailPath, mailMap},

View File

@ -45,13 +45,12 @@ func (sc mockCategorizer) pathKeys() []categorizer {
return []categorizer{rootCatStub, leafCatStub}
}
// TODO: Uncomment when reducer func is added
// func stubPathValues() map[categorizer]string {
// return map[categorizer]string{
// rootCatStub: rootCatStub.String(),
// leafCatStub: leafCatStub.String(),
// }
// }
func stubPathValues() map[categorizer]string {
return map[categorizer]string{
rootCatStub: rootCatStub.String(),
leafCatStub: leafCatStub.String(),
}
}
// ---------------------------------------------------------------------------
// scopers

View File

@ -3,6 +3,7 @@ package selectors
import (
"strings"
"github.com/alcionai/corso/internal/common"
"github.com/alcionai/corso/pkg/backup/details"
)
@ -40,12 +41,11 @@ type (
// so that the two can be compared.
pathKeys() []categorizer
}
// TODO: Uncomment when reducer func is added
// categoryT is the generic type interface of a categorizer
// categoryT interface {
// ~int
// categorizer
// }
categoryT interface {
~int
categorizer
}
)
type (
@ -157,3 +157,188 @@ func isAnyTarget[T scopeT](s T, cat categorizer) bool {
}
return s[cat.String()] == AnyTgt
}
// reduce filters the entries in the details to only those that match the
// inclusions, filters, and exclusions in the selector.
//
func reduce[T scopeT, C categoryT](
deets *details.Details,
s Selector,
dataCategories map[pathType]C,
) *details.Details {
if deets == nil {
return nil
}
// aggregate each scope type by category for easier isolation in future processing.
excludes := scopesByCategory[T](s.Excludes, dataCategories)
filters := scopesByCategory[T](s.Filters, dataCategories)
includes := scopesByCategory[T](s.Includes, dataCategories)
ents := []details.DetailsEntry{}
// for each entry, compare that entry against the scopes of the same data type
for _, ent := range deets.Entries {
// todo: use Path pkg for this
path := strings.Split(ent.RepoRef, "/")
dc, ok := dataCategories[pathTypeIn(path)]
if !ok {
continue
}
passed := passes(
dc,
dc.pathValues(path),
ent,
excludes[dc],
filters[dc],
includes[dc],
)
if passed {
ents = append(ents, ent)
}
}
reduced := &details.Details{DetailsModel: deets.DetailsModel}
reduced.Entries = ents
return reduced
}
// TODO: this is a hack. We don't want these values declared here- it will get
// unwieldy to have all of them for all services. They should be declared in
// paths, since that's where service- and data-type-specific assertions are owned.
type pathType int
const (
unknownPathType pathType = iota
exchangeEventPath
exchangeContactPath
exchangeMailPath
)
// return the service data type of the path.
// TODO: this is a hack. We don't want this identification to occur in this
// package. It should get handled in paths, since that's where service- and
// data-type-specific assertions are owned.
// Ideally, we'd use something like path.DataType() instead of this func.
func pathTypeIn(path []string) pathType {
// not all paths will be len=3. Most should be longer.
// This just protects us from panicing below.
if len(path) < 3 {
return unknownPathType
}
switch path[2] {
case "mail":
return exchangeMailPath
case "contact":
return exchangeContactPath
case "event":
return exchangeEventPath
}
return unknownPathType
}
// groups each scope by its category of data (specified by the service-selector).
// ex: a slice containing the scopes [mail1, mail2, event1]
// would produce a map like { mail: [1, 2], event: [1] }
// so long as "mail" and "event" are contained in cats.
func scopesByCategory[T scopeT, C categoryT](
scopes []scope,
cats map[pathType]C,
) map[C][]T {
m := map[C][]T{}
for _, cat := range cats {
m[cat] = []T{}
}
for _, sc := range scopes {
for _, cat := range cats {
t := T(sc)
if t.categorizer().includesType(cat) {
m[cat] = append(m[cat], t)
}
}
}
return m
}
// passes compares each path to the included and excluded exchange scopes. Returns true
// if the path is included, passes filters, and not excluded.
func passes[T scopeT](
cat categorizer,
pathValues map[categorizer]string,
entry details.DetailsEntry,
excs, filts, incs []T,
) bool {
// a passing match requires either a filter or an inclusion
if len(incs)+len(filts) == 0 {
return false
}
// skip this check if 0 inclusions were populated
// since filters act as the inclusion check in that case
if len(incs) > 0 {
// at least one inclusion must apply.
var included bool
for _, inc := range incs {
if inc.matchesEntry(cat, pathValues, entry) {
included = true
break
}
}
if !included {
return false
}
}
// all filters must pass
for _, filt := range filts {
if !filt.matchesEntry(cat, pathValues, entry) {
return false
}
}
// any matching exclusion means failure
for _, exc := range excs {
if exc.matchesEntry(cat, pathValues, entry) {
return false
}
}
return true
}
// matchesPathValues will check whether the pathValues have matching entries
// in the scope. The keys of the values to match against are identified by
// the categorizer.
// Standard expectations apply: None() or missing values always fail, Any()
// always succeeds.
func matchesPathValues[T scopeT](
sc T,
cat categorizer,
pathValues map[categorizer]string,
) bool {
for _, c := range cat.pathKeys() {
target := getCatValue(sc, c)
// the scope must define the targets to match on
if len(target) == 0 {
return false
}
// None() fails all matches
if target[0] == NoneTgt {
return false
}
// the path must contain a value to match against
pv, ok := pathValues[c]
if !ok {
return false
}
// all parts of the scope must match
if !isAnyTarget(sc, c) {
if !common.ContainsString(target, pv) {
return false
}
}
}
return true
}

View File

@ -4,7 +4,10 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/pkg/backup/details"
)
// ---------------------------------------------------------------------------
@ -115,3 +118,196 @@ func (suite *SelectorScopesSuite) TestIsAnyTarget() {
assert.True(t, isAnyTarget(stub, rootCatStub))
assert.False(t, isAnyTarget(stub, leafCatStub))
}
var reduceTestTable = []struct {
name string
sel func() Selector
expectLen int
expectPasses assert.BoolAssertionFunc
}{
{
name: "include all",
sel: func() Selector {
sel := stubSelector()
sel.Filters = nil
sel.Excludes = nil
return sel
},
expectLen: 1,
expectPasses: assert.True,
},
{
name: "include none",
sel: func() Selector {
sel := stubSelector()
sel.Includes[0] = scope(stubScope("none"))
sel.Filters = nil
sel.Excludes = nil
return sel
},
expectLen: 0,
expectPasses: assert.False,
},
{
name: "filter and include all",
sel: func() Selector {
sel := stubSelector()
sel.Excludes = nil
return sel
},
expectLen: 1,
expectPasses: assert.True,
},
{
name: "include all filter none",
sel: func() Selector {
sel := stubSelector()
sel.Filters[0] = scope(stubScope("none"))
sel.Excludes = nil
return sel
},
expectLen: 0,
expectPasses: assert.False,
},
{
name: "include all exclude all",
sel: func() Selector {
sel := stubSelector()
sel.Filters = nil
return sel
},
expectLen: 0,
expectPasses: assert.False,
},
{
name: "include all exclude none",
sel: func() Selector {
sel := stubSelector()
sel.Filters = nil
sel.Excludes[0] = scope(stubScope("none"))
return sel
},
expectLen: 1,
expectPasses: assert.True,
},
{
name: "filter all exclude all",
sel: func() Selector {
sel := stubSelector()
sel.Includes = nil
return sel
},
expectLen: 0,
expectPasses: assert.False,
},
{
name: "filter all exclude none",
sel: func() Selector {
sel := stubSelector()
sel.Includes = nil
sel.Excludes[0] = scope(stubScope("none"))
return sel
},
expectLen: 1,
expectPasses: assert.True,
},
}
func (suite *SelectorScopesSuite) TestReduce() {
deets := func() details.Details {
return details.Details{
DetailsModel: details.DetailsModel{
Entries: []details.DetailsEntry{
{RepoRef: rootCatStub.String() + "/stub/" + leafCatStub.String()},
},
},
}
}
dataCats := map[pathType]mockCategorizer{
unknownPathType: rootCatStub,
}
for _, test := range reduceTestTable {
suite.T().Run(test.name, func(t *testing.T) {
ds := deets()
result := reduce[mockScope](&ds, test.sel(), dataCats)
require.NotNil(t, result)
assert.Len(t, result.Entries, test.expectLen)
})
}
}
func (suite *SelectorScopesSuite) TestPathTypeIn() {
t := suite.T()
assert.Equal(t, unknownPathType, pathTypeIn([]string{}), "empty")
assert.Equal(t, exchangeMailPath, pathTypeIn([]string{"", "", "mail"}), "mail")
assert.Equal(t, exchangeContactPath, pathTypeIn([]string{"", "", "contact"}), "contact")
assert.Equal(t, exchangeEventPath, pathTypeIn([]string{"", "", "event"}), "event")
assert.Equal(t, unknownPathType, pathTypeIn([]string{"", "", "fnords"}), "bogus")
}
func (suite *SelectorScopesSuite) TestScopesByCategory() {
t := suite.T()
s1 := stubScope("")
s2 := stubScope("")
s2[scopeKeyCategory] = unknownCatStub.String()
result := scopesByCategory[mockScope](
[]scope{scope(s1), scope(s2)},
map[pathType]mockCategorizer{
unknownPathType: rootCatStub,
})
assert.Len(t, result, 1)
assert.Len(t, result[rootCatStub], 1)
assert.Empty(t, result[leafCatStub])
}
func (suite *SelectorScopesSuite) TestPasses() {
cat := rootCatStub
pathVals := map[categorizer]string{}
entry := details.DetailsEntry{}
for _, test := range reduceTestTable {
suite.T().Run(test.name, func(t *testing.T) {
sel := test.sel()
excl := toMockScope(sel.Excludes)
filt := toMockScope(sel.Filters)
incl := toMockScope(sel.Includes)
result := passes(
cat,
pathVals,
entry,
excl, filt, incl)
test.expectPasses(t, result)
})
}
}
func toMockScope(sc []scope) []mockScope {
if len(sc) == 0 {
return nil
}
ms := []mockScope{}
for _, s := range sc {
ms = append(ms, mockScope(s))
}
return ms
}
func (suite *SelectorScopesSuite) TestMatchesPathValues() {
t := suite.T()
cat := rootCatStub
sc := stubScope("")
sc[rootCatStub.String()] = rootCatStub.String()
sc[leafCatStub.String()] = leafCatStub.String()
pvs := stubPathValues()
assert.True(t, matchesPathValues(sc, cat, pvs), "matching values")
// "any" seems like it should pass, but this is the path value,
// not the scope value, so unless the scope is also "any", it fails.
pvs[rootCatStub] = AnyTgt
pvs[leafCatStub] = AnyTgt
assert.False(t, matchesPathValues(sc, cat, pvs), "any")
pvs[rootCatStub] = NoneTgt
pvs[leafCatStub] = NoneTgt
assert.False(t, matchesPathValues(sc, cat, pvs), "none")
pvs[rootCatStub] = "foo"
pvs[leafCatStub] = "bar"
assert.False(t, matchesPathValues(sc, cat, pvs), "mismatched values")
}

View File

@ -53,7 +53,7 @@ func (suite *SelectorSuite) TestPrintable_IncludedResources() {
p := sel.Printable()
res := p.Resources()
assert.Equal(t, stubResource, res, "resource should state only the user")
assert.Equal(t, stubResource, res, "resource should state only the stub")
sel.Includes = []scope{
scope(stubScope("")),
@ -68,7 +68,7 @@ func (suite *SelectorSuite) TestPrintable_IncludedResources() {
p.Includes = nil
res = p.Resources()
assert.Equal(t, stubResource, res, "resource on filters should state only the user")
assert.Equal(t, stubResource, res, "resource on filters should state only the stub")
p.Filters = nil
res = p.Resources()