Skip to content

Commit

Permalink
Make debugging AssertResourceFilteredCount issues easier (#199)
Browse files Browse the repository at this point in the history
- If the assert fails, it now logs which part of the filter matched or didn't.
- Refactors ResourceFilter to group the elements of each filter (test and stringer) together. This should make it easier to add new filter options in the future.
  • Loading branch information
rabbitfang authored Jul 22, 2022
1 parent 686b140 commit 12d344a
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 105 deletions.
5 changes: 2 additions & 3 deletions pkg/testingutil/chan.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package testingutil

import (
"context"
"testing"
)

// FetchAll pulls all resources from `f` over the passed channel, returning the resources as a slice
func FetchAll[T any](ctx context.Context, t testing.TB, f func(context.Context, chan<- T) error) ([]T, error) {
func FetchAll[T any](ctx context.Context, t TestingTB, f func(context.Context, chan<- T) error) ([]T, error) {
t.Helper()

var resources []T
Expand All @@ -27,7 +26,7 @@ func FetchAll[T any](ctx context.Context, t testing.TB, f func(context.Context,
}

// MustFetchAll is like FetchAll, but fatals the running test if there is an error during fetching
func MustFetchAll[T any](ctx context.Context, t testing.TB, f func(context.Context, chan<- T) error) []T {
func MustFetchAll[T any](ctx context.Context, t TestingTB, f func(context.Context, chan<- T) error) []T {
t.Helper()

resources, err := FetchAll(ctx, t, f)
Expand Down
7 changes: 4 additions & 3 deletions pkg/testingutil/chan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ func TestMustFetchAllCanceled(t *testing.T) {
return util.SendAllFromSlice(ctx, out, in)
}

assert.PanicsWithError(t, "error with testingutil.FetchAll: context canceled", func() {
MustFetchAll(ctx, Fake(t), fetchFunc)
})
fake := Fake(t)
MustFetchAll(ctx, fake, fetchFunc)
assert.Contains(t, fake.Logs, "error with testingutil.FetchAll: context canceled")
assert.True(t, fake.IsFail)
}
240 changes: 153 additions & 87 deletions pkg/testingutil/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,124 +21,190 @@ type ResourceFilter struct {
func (f ResourceFilter) String() string {
var parts []string

if f.AccountId != "" {
parts = append(parts, fmt.Sprintf("AccountId=%s", f.AccountId))
}

if f.Type != "" {
parts = append(parts, fmt.Sprintf("Type=%s", f.Type))
}

if f.Region != "" {
parts = append(parts, fmt.Sprintf("Region=%s", f.Region))
}

if f.Tags != nil && len(f.Tags) == 0 {
parts = append(parts, "Tags=[]")
} else {
for _, tag := range f.Tags {
if tag.Value == "" {
parts = append(parts, fmt.Sprintf("Tags[%s]", tag.Key))
} else {
parts = append(parts, fmt.Sprintf("Tags[%s]=%s", tag.Key, tag.Value))
}
for _, matcher := range f.matchers() {
if !matcher.present() {
continue
}
}

if len(f.RawData) > 0 {
rawParts := make([]string, 0, len(f.RawData))

for key, val := range f.RawData {
rawParts = append(rawParts, fmt.Sprintf("%s=%v", key, val))
}
//sorting ensures consistent output for testing
sort.Strings(rawParts)
parts = append(parts, fmt.Sprintf("RawData={%s}", strings.Join(rawParts, ", ")))
parts = append(parts, matcher.stringer())
}

fields := strings.Join(parts, ", ")
return fmt.Sprintf("ResourceFilter{%s}", fields)
}

func (f ResourceFilter) Matches(resource model.Resource) bool {
if f.AccountId != "" {
if resource.AccountId != f.AccountId {
return false
}
}

if f.Region != "" {
if resource.Region != f.Region {
return false
for _, matcher := range f.matchers() {
if !matcher.present() {
continue
}
}

if f.Type != "" {
if resource.Type != f.Type {
if !matcher.match(resource) {
return false
}
}

// Treat empty slice different from nil
if f.Tags != nil {
// Treat empty slice as special "no tags" filter
if len(f.Tags) == 0 {
if len(resource.Tags) != 0 {
return false
}
} else {
tagMap := make(map[string]string)
for _, tag := range resource.Tags {
tagMap[tag.Key] = tag.Value
}

for _, tag := range f.Tags {
val, has := tagMap[tag.Key]
if !has {
return false
}
return true
}

if tag.Value == "" {
continue
}
func (f ResourceFilter) Filter(in []model.Resource) []model.Resource {
out := make([]model.Resource, 0, len(in))

if strings.TrimSpace(val) != strings.TrimSpace(tag.Value) {
return false
}
}
for _, resource := range in {
if f.Matches(resource) {
out = append(out, resource)
}
}

if len(f.RawData) > 0 {
var raw map[string]any
err := json.Unmarshal(resource.RawData, &raw)
if err != nil {
panic(fmt.Errorf("cannot pase model.Resource.RawData: %s", resource.Id))
return out
}

// PartialFilter returns a more detailed filtering of the resources, with filter field processed separately.
// This can aid in debugging to determine why a resource isn't matching a given filter.
func (f ResourceFilter) PartialFilter(in []model.Resource) map[string][]model.Resource {
output := make(map[string][]model.Resource)

for _, matcher := range f.matchers() {
if !matcher.present() {
continue
}

for key, val := range f.RawData {
rawVal, has := raw[key]
if !has {
return false
var resources []model.Resource
for _, res := range in {
if !matcher.match(res) {
continue
}

if !reflect.DeepEqual(val, rawVal) {
return false
}
resources = append(resources, res)
}

output[matcher.name] = resources
}

return true
return output
}

func (f ResourceFilter) Filter(in []model.Resource) []model.Resource {
out := make([]model.Resource, 0, len(in))
type resourceFilterMatcher struct {
name string
present func() bool
stringer func() string
match func(model.Resource) bool
}

for _, resource := range in {
if f.Matches(resource) {
out = append(out, resource)
func (f ResourceFilter) matchers() []resourceFilterMatcher {
p := func(present bool) func() bool {
return func() bool {
return present
}
}
s := func(format string, val any) func() string {
return func() string {
return fmt.Sprintf(format, val)
}
}
return []resourceFilterMatcher{
{
name: "AccountId",
present: p(f.AccountId != ""),
stringer: s("AccountId=%s", f.AccountId),
match: func(r model.Resource) bool { return f.AccountId == r.AccountId },
},
{
name: "Type",
present: p(f.Type != ""),
stringer: s("Type=%s", f.Type),
match: func(r model.Resource) bool { return f.Type == r.Type },
},
{
name: "Region",
present: p(f.Region != ""),
stringer: s("Region=%s", f.Region),
match: func(r model.Resource) bool { return f.Region == r.Region },
},
{
name: "Tags",
present: p(f.Tags != nil), // Empty non-nil slice has special meaning
stringer: func() string {
if len(f.Tags) == 0 {
return "Tags=[]"
}

return out
var parts []string
for _, tag := range f.Tags {
if tag.Value == "" {
parts = append(parts, fmt.Sprintf("Tags[%s]", tag.Key))
} else {
parts = append(parts, fmt.Sprintf("Tags[%s]=%s", tag.Key, tag.Value))
}
}

return strings.Join(parts, ", ")
},
match: func(r model.Resource) bool {
// Treat empty slice as special "no tags" filter
if len(f.Tags) == 0 {
return len(r.Tags) == 0
}

tagMap := make(map[string]string)
for _, tag := range r.Tags {
tagMap[tag.Key] = tag.Value
}

for _, tag := range f.Tags {
val, has := tagMap[tag.Key]
if !has {
return false
}

if tag.Value == "" {
continue
}

if strings.TrimSpace(val) != strings.TrimSpace(tag.Value) {
return false
}
}

return true
},
},
{
name: "RawData",
present: p(len(f.RawData) > 0),
stringer: func() string {
rawParts := make([]string, 0, len(f.RawData))

for key, val := range f.RawData {
pair := fmt.Sprintf("%s=%v", key, val)
rawParts = append(rawParts, pair)
}
//sorting ensures consistent output for testing
sort.Strings(rawParts)

data := strings.Join(rawParts, ", ")
return fmt.Sprintf("RawData={%s}", data)
},
match: func(r model.Resource) bool {
var raw map[string]any
err := json.Unmarshal(r.RawData, &raw)
if err != nil {
panic(fmt.Errorf("cannot parse model.Resource.RawData: %s", r.Id))
}

for key, val := range f.RawData {
rawVal, has := raw[key]
if !has {
return false
}

if !reflect.DeepEqual(val, rawVal) {
return false
}
}

return true
},
},
}
}
Loading

0 comments on commit 12d344a

Please sign in to comment.