diff --git a/internal/catalogmetadata/filter/bundle_predicates.go b/internal/catalogmetadata/filter/bundle_predicates.go index ef4a24787..e7cf5323a 100644 --- a/internal/catalogmetadata/filter/bundle_predicates.go +++ b/internal/catalogmetadata/filter/bundle_predicates.go @@ -2,14 +2,14 @@ package filter import ( mmsemver "github.com/Masterminds/semver/v3" + filterutils "github.com/operator-framework/operator-controller/internal/util/filter" "github.com/operator-framework/operator-registry/alpha/declcfg" "github.com/operator-framework/operator-controller/internal/bundleutil" - slicesutil "github.com/operator-framework/operator-controller/internal/util/slices" ) -func InMastermindsSemverRange(semverRange *mmsemver.Constraints) slicesutil.Predicate[declcfg.Bundle] { +func InMastermindsSemverRange(semverRange *mmsemver.Constraints) filterutils.Predicate[declcfg.Bundle] { return func(b declcfg.Bundle) bool { bVersion, err := bundleutil.GetVersion(b) if err != nil { @@ -27,7 +27,7 @@ func InMastermindsSemverRange(semverRange *mmsemver.Constraints) slicesutil.Pred } } -func InAnyChannel(channels ...declcfg.Channel) slicesutil.Predicate[declcfg.Bundle] { +func InAnyChannel(channels ...declcfg.Channel) filterutils.Predicate[declcfg.Bundle] { return func(bundle declcfg.Bundle) bool { for _, ch := range channels { for _, entry := range ch.Entries { diff --git a/internal/catalogmetadata/filter/successors.go b/internal/catalogmetadata/filter/successors.go index 28064cdc7..8e47a5983 100644 --- a/internal/catalogmetadata/filter/successors.go +++ b/internal/catalogmetadata/filter/successors.go @@ -2,6 +2,7 @@ package filter import ( "fmt" + filterutils "github.com/operator-framework/operator-controller/internal/util/filter" mmsemver "github.com/Masterminds/semver/v3" bsemver "github.com/blang/semver/v4" @@ -9,10 +10,9 @@ import ( "github.com/operator-framework/operator-registry/alpha/declcfg" ocv1 "github.com/operator-framework/operator-controller/api/v1" - slicesutil "github.com/operator-framework/operator-controller/internal/util/slices" ) -func SuccessorsOf(installedBundle ocv1.BundleMetadata, channels ...declcfg.Channel) (slicesutil.Predicate[declcfg.Bundle], error) { +func SuccessorsOf(installedBundle ocv1.BundleMetadata, channels ...declcfg.Channel) (filterutils.Predicate[declcfg.Bundle], error) { installedBundleVersion, err := mmsemver.NewVersion(installedBundle.Version) if err != nil { return nil, fmt.Errorf("parsing installed bundle %q version %q: %w", installedBundle.Name, installedBundle.Version, err) @@ -29,13 +29,13 @@ func SuccessorsOf(installedBundle ocv1.BundleMetadata, channels ...declcfg.Chann } // We need either successors or current version (no upgrade) - return slicesutil.Or( + return filterutils.Or( successorsPredicate, InMastermindsSemverRange(installedVersionConstraint), ), nil } -func legacySuccessor(installedBundle ocv1.BundleMetadata, channels ...declcfg.Channel) (slicesutil.Predicate[declcfg.Bundle], error) { +func legacySuccessor(installedBundle ocv1.BundleMetadata, channels ...declcfg.Channel) (filterutils.Predicate[declcfg.Bundle], error) { installedBundleVersion, err := bsemver.Parse(installedBundle.Version) if err != nil { return nil, fmt.Errorf("error parsing installed bundle version: %w", err) diff --git a/internal/resolve/catalog.go b/internal/resolve/catalog.go index 70c8a8ddf..ce046cf78 100644 --- a/internal/resolve/catalog.go +++ b/internal/resolve/catalog.go @@ -3,6 +3,7 @@ package resolve import ( "context" "fmt" + filterutils "github.com/operator-framework/operator-controller/internal/util/filter" "slices" "sort" "strings" @@ -76,7 +77,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio var catStats []*catStat - resolvedBundles := []foundBundle{} + var resolvedBundles []foundBundle var priorDeprecation *declcfg.Deprecation listOptions := []client.ListOption{ @@ -97,7 +98,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio cs.PackageFound = true cs.TotalBundles = len(packageFBC.Bundles) - var predicates []slicesutil.Predicate[declcfg.Bundle] + var predicates []filterutils.Predicate[declcfg.Bundle] if len(channels) > 0 { channelSet := sets.New(channels...) filteredChannels := slices.DeleteFunc(packageFBC.Channels, func(c declcfg.Channel) bool { @@ -119,7 +120,7 @@ func (r *CatalogResolver) Resolve(ctx context.Context, ext *ocv1.ClusterExtensio } // Apply the predicates to get the candidate bundles - packageFBC.Bundles = slicesutil.RemoveInPlace(packageFBC.Bundles, slicesutil.And(predicates...)) + packageFBC.Bundles = slicesutil.RemoveInPlace(packageFBC.Bundles, filterutils.And(predicates...)) cs.MatchedBundles = len(packageFBC.Bundles) if len(packageFBC.Bundles) == 0 { return nil diff --git a/internal/util/slices/filter.go b/internal/util/filter/predicates.go similarity index 52% rename from internal/util/slices/filter.go rename to internal/util/filter/predicates.go index dec44f9b0..bee339d67 100644 --- a/internal/util/slices/filter.go +++ b/internal/util/filter/predicates.go @@ -1,29 +1,8 @@ -package slices - -import ( - "slices" -) +package filter // Predicate returns true if the object should be kept when filtering type Predicate[T any] func(entity T) bool -// Filter creates a new slice with all elements from s for which the test returns true -func Filter[T any](s []T, test Predicate[T]) []T { - out := make([]T, 0, len(s)) - for i := 0; i < len(s); i++ { - if test(s[i]) { - out = append(out, s[i]) - } - } - return slices.Clip(out) -} - -// RemoveInPlace removes all elements from s for which test returns true. -// Elements between new length and original length are zeroed out. -func RemoveInPlace[T any](s []T, test Predicate[T]) []T { - return slices.DeleteFunc(s, Not(test)) -} - func And[T any](predicates ...Predicate[T]) Predicate[T] { return func(obj T) bool { for _, predicate := range predicates { diff --git a/internal/util/filter/predicates_test.go b/internal/util/filter/predicates_test.go new file mode 100644 index 000000000..eb3e7ed54 --- /dev/null +++ b/internal/util/filter/predicates_test.go @@ -0,0 +1,145 @@ +package filter_test + +import ( + "github.com/operator-framework/operator-controller/internal/util/filter" + "github.com/stretchr/testify/require" + "testing" +) + +func TestAnd(t *testing.T) { + tests := []struct { + name string + predicates []filter.Predicate[int] + input int + want bool + }{ + { + name: "all true", + predicates: []filter.Predicate[int]{ + func(i int) bool { return i > 0 }, + func(i int) bool { return i < 10 }, + }, + input: 5, + want: true, + }, + { + name: "one false", + predicates: []filter.Predicate[int]{ + func(i int) bool { return i > 0 }, + func(i int) bool { return i < 5 }, + }, + input: 5, + want: false, + }, + { + name: "all false", + predicates: []filter.Predicate[int]{ + func(i int) bool { return i > 10 }, + func(i int) bool { return i < 0 }, + }, + input: 5, + want: false, + }, + { + name: "no predicates", + predicates: []filter.Predicate[int]{}, + input: 5, + want: true, + }, + { + name: "nil predicates", + predicates: nil, + input: 5, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := filter.And(tt.predicates...)(tt.input) + require.Equal(t, tt.want, got, "And() = %v, want %v", got, tt.want) + }) + } +} + +func TestOr(t *testing.T) { + tests := []struct { + name string + predicates []filter.Predicate[int] + input int + want bool + }{ + { + name: "all true", + predicates: []filter.Predicate[int]{ + func(i int) bool { return i > 0 }, + func(i int) bool { return i < 10 }, + }, + input: 5, + want: true, + }, + { + name: "one false", + predicates: []filter.Predicate[int]{ + func(i int) bool { return i > 0 }, + func(i int) bool { return i < 5 }, + }, + input: 5, + want: true, + }, + { + name: "all false", + predicates: []filter.Predicate[int]{ + func(i int) bool { return i > 10 }, + func(i int) bool { return i < 0 }, + }, + input: 5, + want: false, + }, + { + name: "no predicates", + predicates: []filter.Predicate[int]{}, + input: 5, + want: false, + }, + { + name: "nil predicates", + predicates: nil, + input: 5, + want: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := filter.Or(tt.predicates...)(tt.input) + require.Equal(t, tt.want, got, "Or() = %v, want %v", got, tt.want) + }) + } +} + +func TestNot(t *testing.T) { + tests := []struct { + name string + predicate filter.Predicate[int] + input int + want bool + }{ + { + name: "predicate is true", + predicate: func(i int) bool { return i > 0 }, + input: 5, + want: false, + }, + { + name: "predicate is false", + predicate: func(i int) bool { return i > 3 }, + input: 2, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := filter.Not(tt.predicate)(tt.input) + require.Equal(t, tt.want, got, "Not() = %v, want %v", got, tt.want) + }) + } +} diff --git a/internal/util/slices/filter_test.go b/internal/util/slices/filter_test.go deleted file mode 100644 index 1d7e7759b..000000000 --- a/internal/util/slices/filter_test.go +++ /dev/null @@ -1,239 +0,0 @@ -package slices_test - -import ( - "testing" - - "github.com/stretchr/testify/require" - - "github.com/operator-framework/operator-controller/internal/util/slices" -) - -func TestAnd(t *testing.T) { - tests := []struct { - name string - predicates []slices.Predicate[int] - input int - want bool - }{ - { - name: "all true", - predicates: []slices.Predicate[int]{ - func(i int) bool { return i > 0 }, - func(i int) bool { return i < 10 }, - }, - input: 5, - want: true, - }, - { - name: "one false", - predicates: []slices.Predicate[int]{ - func(i int) bool { return i > 0 }, - func(i int) bool { return i < 5 }, - }, - input: 5, - want: false, - }, - { - name: "all false", - predicates: []slices.Predicate[int]{ - func(i int) bool { return i > 10 }, - func(i int) bool { return i < 0 }, - }, - input: 5, - want: false, - }, - { - name: "no predicates", - predicates: []slices.Predicate[int]{}, - input: 5, - want: true, - }, - { - name: "nil predicates", - predicates: nil, - input: 5, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := slices.And(tt.predicates...)(tt.input) - require.Equal(t, tt.want, got, "And() = %v, want %v", got, tt.want) - }) - } -} - -func TestOr(t *testing.T) { - tests := []struct { - name string - predicates []slices.Predicate[int] - input int - want bool - }{ - { - name: "all true", - predicates: []slices.Predicate[int]{ - func(i int) bool { return i > 0 }, - func(i int) bool { return i < 10 }, - }, - input: 5, - want: true, - }, - { - name: "one false", - predicates: []slices.Predicate[int]{ - func(i int) bool { return i > 0 }, - func(i int) bool { return i < 5 }, - }, - input: 5, - want: true, - }, - { - name: "all false", - predicates: []slices.Predicate[int]{ - func(i int) bool { return i > 10 }, - func(i int) bool { return i < 0 }, - }, - input: 5, - want: false, - }, - { - name: "no predicates", - predicates: []slices.Predicate[int]{}, - input: 5, - want: false, - }, - { - name: "nil predicates", - predicates: nil, - input: 5, - want: false, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := slices.Or(tt.predicates...)(tt.input) - require.Equal(t, tt.want, got, "Or() = %v, want %v", got, tt.want) - }) - } -} - -func TestNot(t *testing.T) { - tests := []struct { - name string - predicate slices.Predicate[int] - input int - want bool - }{ - { - name: "predicate is true", - predicate: func(i int) bool { return i > 0 }, - input: 5, - want: false, - }, - { - name: "predicate is false", - predicate: func(i int) bool { return i > 3 }, - input: 2, - want: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := slices.Not(tt.predicate)(tt.input) - require.Equal(t, tt.want, got, "Not() = %v, want %v", got, tt.want) - }) - } -} - -func TestFilter(t *testing.T) { - tests := []struct { - name string - slice []int - predicate slices.Predicate[int] - want []int - }{ - { - name: "all match", - slice: []int{1, 2, 3, 4, 5}, - predicate: func(i int) bool { return i > 0 }, - want: []int{1, 2, 3, 4, 5}, - }, - { - name: "some match", - slice: []int{1, 2, 3, 4, 5}, - predicate: func(i int) bool { return i > 3 }, - want: []int{4, 5}, - }, - { - name: "none match", - slice: []int{1, 2, 3, 4, 5}, - predicate: func(i int) bool { return i > 5 }, - want: []int{}, - }, - { - name: "empty slice", - slice: []int{}, - predicate: func(i int) bool { return i > 5 }, - want: []int{}, - }, - { - name: "nil slice", - slice: nil, - predicate: func(i int) bool { return i > 5 }, - want: []int{}, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := slices.Filter(tt.slice, tt.predicate) - require.Equal(t, tt.want, got, "Filter() = %v, want %v", got, tt.want) - }) - } -} - -func TestRemoveInPlace(t *testing.T) { - tests := []struct { - name string - slice []int - predicate slices.Predicate[int] - want []int - }{ - { - name: "all match", - slice: []int{1, 2, 3, 4, 5}, - predicate: func(i int) bool { return i > 0 }, - want: []int{1, 2, 3, 4, 5}, - }, - { - name: "some match", - slice: []int{1, 2, 3, 4, 5}, - predicate: func(i int) bool { return i > 3 }, - want: []int{4, 5, 0, 0, 0}, - }, - { - name: "none match", - slice: []int{1, 2, 3, 4, 5}, - predicate: func(i int) bool { return i > 5 }, - want: []int{0, 0, 0, 0, 0}, - }, - { - name: "empty slice", - slice: []int{}, - predicate: func(i int) bool { return i > 5 }, - want: []int{}, - }, - { - name: "nil slice", - slice: nil, - predicate: func(i int) bool { return i > 5 }, - want: nil, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - _ = slices.RemoveInPlace(tt.slice, tt.predicate) - require.Equal(t, tt.want, tt.slice, "Filter() = %v, want %v", tt.slice, tt.want) - }) - } -} diff --git a/internal/util/slices/slices.go b/internal/util/slices/slices.go new file mode 100644 index 000000000..c2cc5452c --- /dev/null +++ b/internal/util/slices/slices.go @@ -0,0 +1,23 @@ +package slices + +import ( + "github.com/operator-framework/operator-controller/internal/util/filter" + "slices" +) + +// Filter creates a new slice with all elements from s for which the test returns true +func Filter[T any](s []T, test filter.Predicate[T]) []T { + out := make([]T, 0, len(s)) + for i := 0; i < len(s); i++ { + if test(s[i]) { + out = append(out, s[i]) + } + } + return slices.Clip(out) +} + +// RemoveInPlace removes all elements from s for which test returns true. +// Elements between new length and original length are zeroed out. +func RemoveInPlace[T any](s []T, test filter.Predicate[T]) []T { + return slices.DeleteFunc(s, filter.Not(test)) +} diff --git a/internal/util/slices/slices_test.go b/internal/util/slices/slices_test.go new file mode 100644 index 000000000..2e5c664ec --- /dev/null +++ b/internal/util/slices/slices_test.go @@ -0,0 +1,102 @@ +package slices_test + +import ( + "github.com/operator-framework/operator-controller/internal/util/filter" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/operator-framework/operator-controller/internal/util/slices" +) + +func TestFilter(t *testing.T) { + tests := []struct { + name string + slice []int + predicate filter.Predicate[int] + want []int + }{ + { + name: "all match", + slice: []int{1, 2, 3, 4, 5}, + predicate: func(i int) bool { return i > 0 }, + want: []int{1, 2, 3, 4, 5}, + }, + { + name: "some match", + slice: []int{1, 2, 3, 4, 5}, + predicate: func(i int) bool { return i > 3 }, + want: []int{4, 5}, + }, + { + name: "none match", + slice: []int{1, 2, 3, 4, 5}, + predicate: func(i int) bool { return i > 5 }, + want: []int{}, + }, + { + name: "empty slice", + slice: []int{}, + predicate: func(i int) bool { return i > 5 }, + want: []int{}, + }, + { + name: "nil slice", + slice: nil, + predicate: func(i int) bool { return i > 5 }, + want: []int{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := slices.Filter(tt.slice, tt.predicate) + require.Equal(t, tt.want, got, "Filter() = %v, want %v", got, tt.want) + }) + } +} + +func TestRemoveInPlace(t *testing.T) { + tests := []struct { + name string + slice []int + predicate filter.Predicate[int] + want []int + }{ + { + name: "all match", + slice: []int{1, 2, 3, 4, 5}, + predicate: func(i int) bool { return i > 0 }, + want: []int{1, 2, 3, 4, 5}, + }, + { + name: "some match", + slice: []int{1, 2, 3, 4, 5}, + predicate: func(i int) bool { return i > 3 }, + want: []int{4, 5, 0, 0, 0}, + }, + { + name: "none match", + slice: []int{1, 2, 3, 4, 5}, + predicate: func(i int) bool { return i > 5 }, + want: []int{0, 0, 0, 0, 0}, + }, + { + name: "empty slice", + slice: []int{}, + predicate: func(i int) bool { return i > 5 }, + want: []int{}, + }, + { + name: "nil slice", + slice: nil, + predicate: func(i int) bool { return i > 5 }, + want: nil, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + _ = slices.RemoveInPlace(tt.slice, tt.predicate) + require.Equal(t, tt.want, tt.slice, "Filter() = %v, want %v", tt.slice, tt.want) + }) + } +}