diff --git a/src/pkg/selectors/exchange.go b/src/pkg/selectors/exchange.go index d123cca3a..8e5a37fc0 100644 --- a/src/pkg/selectors/exchange.go +++ b/src/pkg/selectors/exchange.go @@ -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 -} diff --git a/src/pkg/selectors/exchange_test.go b/src/pkg/selectors/exchange_test.go index b3b8875f9..f9076a2bd 100644 --- a/src/pkg/selectors/exchange_test.go +++ b/src/pkg/selectors/exchange_test.go @@ -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}, diff --git a/src/pkg/selectors/helpers_test.go b/src/pkg/selectors/helpers_test.go index 095581cde..8a4074fad 100644 --- a/src/pkg/selectors/helpers_test.go +++ b/src/pkg/selectors/helpers_test.go @@ -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 diff --git a/src/pkg/selectors/scopes.go b/src/pkg/selectors/scopes.go index bace31879..5862a8f04 100644 --- a/src/pkg/selectors/scopes.go +++ b/src/pkg/selectors/scopes.go @@ -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 +} diff --git a/src/pkg/selectors/scopes_test.go b/src/pkg/selectors/scopes_test.go index fd3662b10..816e28a7f 100644 --- a/src/pkg/selectors/scopes_test.go +++ b/src/pkg/selectors/scopes_test.go @@ -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") +} diff --git a/src/pkg/selectors/selectors_test.go b/src/pkg/selectors/selectors_test.go index 5bb3ac73d..ae8016617 100644 --- a/src/pkg/selectors/selectors_test.go +++ b/src/pkg/selectors/selectors_test.go @@ -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()