From c52886b2cda32a1a26ce0580d489ecbe3942280a Mon Sep 17 00:00:00 2001 From: Jakob Schrettenbrunner Date: Fri, 7 Jan 2022 00:37:17 +0100 Subject: [PATCH] refactor komega package rename Matcher to komega since it's not really a matcher komega.With... methods now return copies allow to specify a Gomega instance to use --- pkg/envtest/komega/interfaces.go | 28 ++-- pkg/envtest/komega/komega.go | 260 +++++++++++++++++++++++++++++++ pkg/envtest/komega/matcher.go | 236 ---------------------------- pkg/envtest/komega/transforms.go | 10 +- 4 files changed, 274 insertions(+), 260 deletions(-) create mode 100644 pkg/envtest/komega/komega.go delete mode 100644 pkg/envtest/komega/matcher.go diff --git a/pkg/envtest/komega/interfaces.go b/pkg/envtest/komega/interfaces.go index 8b355637a0..b61520e71b 100644 --- a/pkg/envtest/komega/interfaces.go +++ b/pkg/envtest/komega/interfaces.go @@ -27,30 +27,20 @@ import ( // Komega is the root interface that the Matcher implements. type Komega interface { - KomegaAsync - KomegaSync - WithContext(context.Context) Komega -} - -// KomegaSync is the interface for any sync assertions that -// the matcher implements. -type KomegaSync interface { Create(client.Object, ...client.CreateOption) gomega.GomegaAssertion - Delete(client.Object, ...client.DeleteOption) gomega.GomegaAssertion - WithExtras(...interface{}) KomegaSync -} - -// KomegaAsync is the interface for any async assertions that -// the matcher implements. -type KomegaAsync interface { - Consistently(runtime.Object, ...client.ListOption) gomega.AsyncAssertion - Eventually(runtime.Object, ...client.ListOption) gomega.AsyncAssertion Get(client.Object) gomega.AsyncAssertion List(client.ObjectList, ...client.ListOption) gomega.AsyncAssertion Update(client.Object, UpdateFunc, ...client.UpdateOption) gomega.AsyncAssertion UpdateStatus(client.Object, UpdateFunc, ...client.UpdateOption) gomega.AsyncAssertion - WithTimeout(time.Duration) KomegaAsync - WithPollInterval(time.Duration) KomegaAsync + Delete(client.Object, ...client.DeleteOption) gomega.GomegaAssertion + + Consistently(runtime.Object, ...client.ListOption) gomega.AsyncAssertion + Eventually(runtime.Object, ...client.ListOption) gomega.AsyncAssertion + + WithExtras(...interface{}) Komega + WithTimeout(time.Duration) Komega + WithPollInterval(time.Duration) Komega + WithContext(context.Context) Komega } // UpdateFunc modifies the object fetched from the API server before sending diff --git a/pkg/envtest/komega/komega.go b/pkg/envtest/komega/komega.go new file mode 100644 index 0000000000..71670f89f3 --- /dev/null +++ b/pkg/envtest/komega/komega.go @@ -0,0 +1,260 @@ +/* +Copyright 2021 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package komega + +import ( + "context" + "testing" + "time" + + "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// komega has Gomega Matchers that use the controller-runtime client. +type komega struct { + ctx context.Context + g gomega.Gomega + client client.Client + extras []interface{} + timeout time.Duration + pollInterval time.Duration +} + +var _ Komega = &komega{} + +// NewKomega creates a new instance with a given client. +func NewKomega(c client.Client) Komega { + return &komega{ + g: gomega.Default, + client: c, + } +} + +// NewKomegaWithT creates a new instance with a given client and a Gomega derived from a given testing.T. +func NewKomegaWithT(t *testing.T, c client.Client) Komega { + return &komega{ + g: gomega.NewWithT(t), + client: c, + } +} + +// NewKomegaWithG creates a new instance with a given client and gomega. +func NewKomegaWithG(g gomega.Gomega, c client.Client) Komega { + return &komega{ + g: g, + client: c, + } +} + +// WithContext returns a copy using the given Context. +func (k komega) WithContext(ctx context.Context) Komega { + k.ctx = ctx + return &k +} + +// WithExtras returns a copy using the given extras. +func (k komega) WithExtras(extras ...interface{}) Komega { + k.extras = extras + return &k +} + +// WithPollInterval returns a copy using the given poll interval. +func (k komega) WithPollInterval(interval time.Duration) Komega { + k.pollInterval = interval + return &k +} + +// WithTimeout returns a copy using the given timeout. +func (k komega) WithTimeout(timeout time.Duration) Komega { + k.timeout = timeout + return &k +} + +// context returns the matcher context if one has been set. +// Else it returns the context.TODO(). +func (k *komega) context() context.Context { + if k.ctx == nil { + return context.Background() + } + return k.ctx +} + +// intervals constructs the intervals for async assertions. +// If no timeout is set, the list will be empty. +func (k *komega) intervals() []interface{} { + if k.timeout == 0 { + return []interface{}{} + } + out := []interface{}{k.timeout} + if k.pollInterval != 0 { + out = append(out, k.pollInterval) + } + return out +} + +// Create creates an object and forwards the error for matching. +func (k *komega) Create(obj client.Object, opts ...client.CreateOption) gomega.GomegaAssertion { + err := k.client.Create(k.context(), obj, opts...) + return k.g.Expect(err, k.extras...) +} + +// Get fetches an object until the forwarded error matches. +func (k *komega) Get(obj client.Object) gomega.GomegaAsyncAssertion { + key := types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + } + get := func() error { + return k.client.Get(k.context(), key, obj) + } + return k.g.Eventually(get, k.intervals()...) +} + +// List fetches a list until the forwarded error matches. +func (k *komega) List(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion { + list := func() error { + return k.client.List(k.context(), obj, opts...) + } + return k.g.Eventually(list, k.intervals()...) +} + +// Update tries to update an object by applying the updateFunc until the forwarded error matches. +func (k *komega) Update(obj client.Object, updateFunc UpdateFunc, opts ...client.UpdateOption) gomega.GomegaAsyncAssertion { + key := types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + } + update := func() error { + err := k.client.Get(k.context(), key, obj) + if err != nil { + return err + } + return k.client.Update(k.context(), updateFunc(obj), opts...) + } + return k.g.Eventually(update, k.intervals()...) +} + +// UpdateStatus tries to update an object's status by applying the updateFunc until the forwarded error matches. +func (k *komega) UpdateStatus(obj client.Object, updateFunc UpdateFunc, opts ...client.UpdateOption) gomega.GomegaAsyncAssertion { + key := types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + } + update := func() error { + err := k.client.Get(k.context(), key, obj) + if err != nil { + return err + } + return k.client.Status().Update(k.context(), updateFunc(obj), opts...) + } + return k.g.Eventually(update, k.intervals()...) +} + +// Delete deletes an object and forwards the error for matching. +func (k *komega) Delete(obj client.Object, opts ...client.DeleteOption) gomega.GomegaAssertion { + err := k.client.Delete(k.context(), obj, opts...) + return gomega.Expect(err, k.extras...) +} + +// Consistently gets an object using Gomega's Consistently. +// See https://onsi.github.io/gomega/#consistently for how it works. +// It supports listing objects as well. +func (k *komega) Consistently(obj runtime.Object, opts ...client.ListOption) gomega.GomegaAsyncAssertion { + // If the object is a list, return a list + if o, ok := obj.(client.ObjectList); ok { + return k.consistentlyList(o, opts...) + } + if o, ok := obj.(client.Object); ok { + return k.consistentlyObject(o) + } + //Should not get here + panic("Unknown object.") +} + +// consistentlyclient.Object gets an individual object from the API server. +func (k *komega) consistentlyObject(obj client.Object) gomega.GomegaAsyncAssertion { + key := types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + } + get := func() client.Object { + err := k.client.Get(k.context(), key, obj) + if err != nil { + panic(err) + } + return obj + } + return k.g.Consistently(get, k.intervals()...) +} + +// consistentlyList gets an list of objects from the API server. +func (k *komega) consistentlyList(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion { + list := func() client.ObjectList { + err := k.client.List(k.context(), obj, opts...) + if err != nil { + panic(err) + } + return obj + } + return k.g.Consistently(list, k.intervals()...) +} + +// Eventually gets an object repeatedly until it matches. +// See https://onsi.github.io/gomega/#eventually for how it works. +// It supports listing objects as well. +func (k *komega) Eventually(obj runtime.Object, opts ...client.ListOption) gomega.GomegaAsyncAssertion { + // If the object is a list, return a list + if o, ok := obj.(client.ObjectList); ok { + return k.eventuallyList(o, opts...) + } + if o, ok := obj.(client.Object); ok { + return k.eventuallyObject(o) + } + //Should not get here + panic("Unknown object.") +} + +// eventuallyObject gets an individual object from the API server. +func (k *komega) eventuallyObject(obj client.Object) gomega.GomegaAsyncAssertion { + key := types.NamespacedName{ + Name: obj.GetName(), + Namespace: obj.GetNamespace(), + } + get := func() client.Object { + err := k.client.Get(k.context(), key, obj) + if err != nil { + panic(err) + } + return obj + } + return k.g.Eventually(get, k.intervals()...) +} + +// eventuallyList gets a list type from the API server. +func (k *komega) eventuallyList(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion { + list := func() client.ObjectList { + err := k.client.List(k.context(), obj, opts...) + if err != nil { + panic(err) + } + return obj + } + return k.g.Eventually(list, k.intervals()...) +} diff --git a/pkg/envtest/komega/matcher.go b/pkg/envtest/komega/matcher.go deleted file mode 100644 index 4f835501aa..0000000000 --- a/pkg/envtest/komega/matcher.go +++ /dev/null @@ -1,236 +0,0 @@ -/* -Copyright 2021 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package komega - -import ( - "context" - "time" - - "github.com/onsi/gomega" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" -) - -// Matcher has Gomega Matchers that use the controller-runtime client. -type Matcher struct { - Client client.Client - ctx context.Context - extras []interface{} - timeout time.Duration - pollInterval time.Duration -} - -// WithContext sets the context to be used for the underlying client -// during assertions. -func (m *Matcher) WithContext(ctx context.Context) Komega { - m.ctx = ctx - return m -} - -// context returns the matcher context if one has been set. -// Else it returns the context.TODO(). -func (m *Matcher) context() context.Context { - if m.ctx == nil { - return context.TODO() - } - return m.ctx -} - -// WithExtras sets extra arguments for sync assertions. -// Any extras passed will be expected to be nil during assertion. -func (m *Matcher) WithExtras(extras ...interface{}) KomegaSync { - m.extras = extras - return m -} - -// WithTimeout sets the timeout for any async assertions. -func (m *Matcher) WithTimeout(timeout time.Duration) KomegaAsync { - m.timeout = timeout - return m -} - -// WithPollInterval sets the poll interval for any async assertions. -// Note: This will only work if an explicit timeout has been set with WithTimeout. -func (m *Matcher) WithPollInterval(pollInterval time.Duration) KomegaAsync { - m.pollInterval = pollInterval - return m -} - -// intervals constructs the intervals for async assertions. -// If no timeout is set, the list will be empty. -func (m *Matcher) intervals() []interface{} { - if m.timeout == 0 { - return []interface{}{} - } - out := []interface{}{m.timeout} - if m.pollInterval != 0 { - out = append(out, m.pollInterval) - } - return out -} - -// Create creates the object on the API server. -func (m *Matcher) Create(obj client.Object, opts ...client.CreateOption) gomega.GomegaAssertion { - err := m.Client.Create(m.context(), obj, opts...) - return gomega.Expect(err, m.extras...) -} - -// Delete deletes the object from the API server. -func (m *Matcher) Delete(obj client.Object, opts ...client.DeleteOption) gomega.GomegaAssertion { - err := m.Client.Delete(m.context(), obj, opts...) - return gomega.Expect(err, m.extras...) -} - -// Update udpates the object on the API server by fetching the object -// and applying a mutating UpdateFunc before sending the update. -func (m *Matcher) Update(obj client.Object, fn UpdateFunc, opts ...client.UpdateOption) gomega.GomegaAsyncAssertion { - key := types.NamespacedName{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - } - update := func() error { - err := m.Client.Get(m.context(), key, obj) - if err != nil { - return err - } - return m.Client.Update(m.context(), fn(obj), opts...) - } - return gomega.Eventually(update, m.intervals()...) -} - -// UpdateStatus udpates the object's status subresource on the API server by -// fetching the object and applying a mutating UpdateFunc before sending the -// update. -func (m *Matcher) UpdateStatus(obj client.Object, fn UpdateFunc, opts ...client.UpdateOption) gomega.GomegaAsyncAssertion { - key := types.NamespacedName{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - } - update := func() error { - err := m.Client.Get(m.context(), key, obj) - if err != nil { - return err - } - return m.Client.Status().Update(m.context(), fn(obj), opts...) - } - return gomega.Eventually(update, m.intervals()...) -} - -// Get gets the object from the API server. -func (m *Matcher) Get(obj client.Object) gomega.GomegaAsyncAssertion { - key := types.NamespacedName{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - } - get := func() error { - return m.Client.Get(m.context(), key, obj) - } - return gomega.Eventually(get, m.intervals()...) -} - -// List gets the list object from the API server. -func (m *Matcher) List(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion { - list := func() error { - return m.Client.List(m.context(), obj, opts...) - } - return gomega.Eventually(list, m.intervals()...) -} - -// Consistently continually gets the object from the API for comparison. -// It can be used to check for either List types or regular Objects. -func (m *Matcher) Consistently(obj runtime.Object, opts ...client.ListOption) gomega.GomegaAsyncAssertion { - // If the object is a list, return a list - if o, ok := obj.(client.ObjectList); ok { - return m.consistentlyList(o, opts...) - } - if o, ok := obj.(client.Object); ok { - return m.consistentlyObject(o) - } - //Should not get here - panic("Unknown object.") -} - -// consistentlyclient.Object gets an individual object from the API server. -func (m *Matcher) consistentlyObject(obj client.Object) gomega.GomegaAsyncAssertion { - key := types.NamespacedName{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - } - get := func() client.Object { - err := m.Client.Get(m.context(), key, obj) - if err != nil { - panic(err) - } - return obj - } - return gomega.Consistently(get, m.intervals()...) -} - -// consistentlyList gets an list of objects from the API server. -func (m *Matcher) consistentlyList(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion { - list := func() client.ObjectList { - err := m.Client.List(m.context(), obj, opts...) - if err != nil { - panic(err) - } - return obj - } - return gomega.Consistently(list, m.intervals()...) -} - -// Eventually continually gets the object from the API for comparison. -// It can be used to check for either List types or regular Objects. -func (m *Matcher) Eventually(obj runtime.Object, opts ...client.ListOption) gomega.GomegaAsyncAssertion { - // If the object is a list, return a list - if o, ok := obj.(client.ObjectList); ok { - return m.eventuallyList(o, opts...) - } - if o, ok := obj.(client.Object); ok { - return m.eventuallyObject(o) - } - //Should not get here - panic("Unknown object.") -} - -// eventuallyObject gets an individual object from the API server. -func (m *Matcher) eventuallyObject(obj client.Object) gomega.GomegaAsyncAssertion { - key := types.NamespacedName{ - Name: obj.GetName(), - Namespace: obj.GetNamespace(), - } - get := func() client.Object { - err := m.Client.Get(m.context(), key, obj) - if err != nil { - panic(err) - } - return obj - } - return gomega.Eventually(get, m.intervals()...) -} - -// eventuallyList gets a list type from the API server. -func (m *Matcher) eventuallyList(obj client.ObjectList, opts ...client.ListOption) gomega.GomegaAsyncAssertion { - list := func() client.ObjectList { - err := m.Client.List(m.context(), obj, opts...) - if err != nil { - panic(err) - } - return obj - } - return gomega.Eventually(list, m.intervals()...) -} diff --git a/pkg/envtest/komega/transforms.go b/pkg/envtest/komega/transforms.go index 165dd55d7d..7dfb44686c 100644 --- a/pkg/envtest/komega/transforms.go +++ b/pkg/envtest/komega/transforms.go @@ -25,20 +25,20 @@ import ( gtypes "github.com/onsi/gomega/types" ) -// WithField gets the value of the named field from the object. +// HaveField gets the value of the named field from the object. // This is intended to be used in assertions with the Matcher make it easy // to check the value of a particular field in a resource. // To access nested fields uses a `.` separator. // Eg. -// m.Eventually(deployment).Should(WithField("spec.replicas", BeZero())) +// m.Eventually(deployment).Should(HaveField("spec.replicas", BeZero())) // To access nested lists, use one of the Gomega list matchers in conjunction with this. // Eg. -// m.Eventually(deploymentList).Should(WithField("items", ConsistOf(...))) -func WithField(field string, matcher gtypes.GomegaMatcher) gtypes.GomegaMatcher { +// m.Eventually(deploymentList).Should(HaveField("items", ConsistOf(...))) +func HaveField(field string, matcher gtypes.GomegaMatcher) gtypes.GomegaMatcher { // Addressing Field by . can be recursed fields := strings.SplitN(field, ".", 2) if len(fields) == 2 { - matcher = WithField(fields[1], matcher) + matcher = HaveField(fields[1], matcher) } return gomega.WithTransform(func(obj interface{}) interface{} {