Skip to content

Commit 2058178

Browse files
authored
feat: extend testing library (#80)
* extend testing library (uid injection) * extend testing library (error matching with ShouldNotReconcile) * use constant-like identifier for better readability
1 parent 264bad4 commit 2058178

File tree

3 files changed

+132
-8
lines changed

3 files changed

+132
-8
lines changed

pkg/testing/complex_environment.go

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,18 @@ import (
88
"time"
99

1010
"github.com/onsi/gomega"
11+
"github.com/onsi/gomega/types"
1112
"k8s.io/apimachinery/pkg/runtime"
13+
"k8s.io/apimachinery/pkg/util/uuid"
1214
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
1315
ctrl "sigs.k8s.io/controller-runtime"
1416
"sigs.k8s.io/controller-runtime/pkg/client"
1517
"sigs.k8s.io/controller-runtime/pkg/client/fake"
18+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
1619
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1720

1821
"github.com/openmcp-project/controller-utils/pkg/logging"
22+
"github.com/openmcp-project/controller-utils/pkg/testing/matchers"
1923
)
2024

2125
/////////////////
@@ -31,6 +35,8 @@ func DefaultScheme() *runtime.Scheme {
3135
return sc
3236
}
3337

38+
var noMatcher types.GomegaMatcher = nil
39+
3440
///////////////////////////
3541
/// COMPLEX ENVIRONMENT ///
3642
///////////////////////////
@@ -82,27 +88,37 @@ func (e *ComplexEnvironment) shouldEventuallyReconcile(reconciler string, req re
8288

8389
// ShouldNotReconcile calls the given reconciler with the given request and expects an error.
8490
func (e *ComplexEnvironment) ShouldNotReconcile(reconciler string, req reconcile.Request, optionalDescription ...interface{}) reconcile.Result {
85-
return e.shouldNotReconcile(reconciler, req, optionalDescription...)
91+
return e.shouldNotReconcile(reconciler, req, noMatcher, optionalDescription...)
92+
}
93+
94+
// ShouldNotReconcileWithError calls the given reconciler with the given request and expects an error that matches the given matcher.
95+
func (e *ComplexEnvironment) ShouldNotReconcileWithError(reconciler string, req reconcile.Request, matcher types.GomegaMatcher, optionalDescription ...interface{}) reconcile.Result {
96+
return e.shouldNotReconcile(reconciler, req, matcher, optionalDescription...)
8697
}
8798

88-
func (e *ComplexEnvironment) shouldNotReconcile(reconciler string, req reconcile.Request, optionalDescription ...interface{}) reconcile.Result {
99+
func (e *ComplexEnvironment) shouldNotReconcile(reconciler string, req reconcile.Request, matcher types.GomegaMatcher, optionalDescription ...interface{}) reconcile.Result {
89100
res, err := e.Reconcilers[reconciler].Reconcile(e.Ctx, req)
90-
gomega.ExpectWithOffset(2, err).To(gomega.HaveOccurred(), optionalDescription...)
101+
gomega.ExpectWithOffset(2, err).To(gomega.And(gomega.HaveOccurred(), matchers.MaybeMatch(matcher)), optionalDescription...)
91102
return res
92103
}
93104

94105
// ShouldEventuallyNotReconcile calls the given reconciler with the given request and retries until an error occurred or the timeout is reached.
95106
func (e *ComplexEnvironment) ShouldEventuallyNotReconcile(reconciler string, req reconcile.Request, timeout, poll time.Duration, optionalDescription ...interface{}) reconcile.Result {
96-
return e.shouldEventuallyNotReconcile(reconciler, req, timeout, poll, optionalDescription...)
107+
return e.shouldEventuallyNotReconcile(reconciler, req, noMatcher, timeout, poll, optionalDescription...)
108+
}
109+
110+
// ShouldEventuallyNotReconcileWithError calls the given reconciler with the given request and retries until an error that matches the given matcher occurred or the timeout is reached.
111+
func (e *ComplexEnvironment) ShouldEventuallyNotReconcileWithError(reconciler string, req reconcile.Request, matcher types.GomegaMatcher, timeout, poll time.Duration, optionalDescription ...interface{}) reconcile.Result {
112+
return e.shouldEventuallyNotReconcile(reconciler, req, matcher, timeout, poll, optionalDescription...)
97113
}
98114

99-
func (e *ComplexEnvironment) shouldEventuallyNotReconcile(reconciler string, req reconcile.Request, timeout, poll time.Duration, optionalDescription ...interface{}) reconcile.Result {
115+
func (e *ComplexEnvironment) shouldEventuallyNotReconcile(reconciler string, req reconcile.Request, matcher types.GomegaMatcher, timeout, poll time.Duration, optionalDescription ...interface{}) reconcile.Result {
100116
var err error
101117
var res reconcile.Result
102118
gomega.EventuallyWithOffset(1, func() error {
103119
res, err = e.Reconcilers[reconciler].Reconcile(e.Ctx, req)
104120
return err
105-
}, timeout, poll).ShouldNot(gomega.Succeed(), optionalDescription...)
121+
}, timeout, poll).ShouldNot(gomega.And(gomega.Succeed(), matchers.MaybeMatch(matcher)), optionalDescription...)
106122
return res
107123
}
108124

@@ -121,6 +137,7 @@ type ComplexEnvironmentBuilder struct {
121137
ClusterInitObjectPaths map[string][]string
122138
ClientCreationCallbacks map[string][]func(client.Client)
123139
loggerIsSet bool
140+
InjectUIDs map[string]bool
124141
}
125142

126143
type ClusterEnvironment struct {
@@ -163,6 +180,7 @@ func NewComplexEnvironmentBuilder() *ComplexEnvironmentBuilder {
163180
ClusterStatusObjects: map[string][]client.Object{},
164181
ClusterInitObjectPaths: map[string][]string{},
165182
ClientCreationCallbacks: map[string][]func(client.Client){},
183+
InjectUIDs: map[string]bool{},
166184
}
167185
}
168186

@@ -264,6 +282,16 @@ func (eb *ComplexEnvironmentBuilder) WithAfterClientCreationCallback(name string
264282
return eb
265283
}
266284

285+
// WithUIDs enables UID injection for the specified cluster.
286+
// All objects that are initially loaded or afterwards created via the client's 'Create' method will have a random UID injected, if they do not already have one.
287+
// Note that this function registers an interceptor function, which will be overwritten if 'WithFakeClientBuilderCall(..., "WithInterceptorFuncs", ...)' is also called.
288+
// This would lead to newly created objects not having a UID injected.
289+
// To avoid this, pass 'InjectUIDOnObjectCreation(...)' into the interceptor.Funcs' Create field. The argument allows to inject your own additional Create logic, if desired.
290+
func (eb *ComplexEnvironmentBuilder) WithUIDs(name string) *ComplexEnvironmentBuilder {
291+
eb.InjectUIDs[name] = true
292+
return eb
293+
}
294+
267295
// WithFakeClientBuilderCall allows to inject method calls to fake.ClientBuilder when the fake clients are created during Build().
268296
// The fake clients are usually created using WithScheme(...).WithObjects(...).WithStatusSubresource(...).Build().
269297
// This function allows to inject additional method calls. It is only required for advanced use-cases.
@@ -284,6 +312,8 @@ func (eb *ComplexEnvironmentBuilder) WithFakeClientBuilderCall(name string, meth
284312
// Build constructs the environment from the builder.
285313
// Note that this function panics instead of throwing an error,
286314
// as it is intended to be used in tests, where all information is static anyway.
315+
//
316+
//nolint:gocyclo
287317
func (eb *ComplexEnvironmentBuilder) Build() *ComplexEnvironment {
288318
res := eb.internal
289319

@@ -335,6 +365,18 @@ func (eb *ComplexEnvironmentBuilder) Build() *ComplexEnvironment {
335365
if len(eb.ClusterInitObjects) > 0 {
336366
objs = append(objs, eb.ClusterInitObjects[name]...)
337367
}
368+
if eb.InjectUIDs[name] {
369+
// ensure that objects have a uid
370+
for _, obj := range objs {
371+
if obj.GetUID() == "" {
372+
// set a random UID if not already set
373+
obj.SetUID(uuid.NewUUID())
374+
}
375+
}
376+
fcb.WithInterceptorFuncs(interceptor.Funcs{
377+
Create: InjectUIDOnObjectCreation(nil),
378+
})
379+
}
338380
statusObjs := []client.Object{}
339381
statusObjs = append(statusObjs, objs...)
340382
statusObjs = append(statusObjs, eb.ClusterStatusObjects[name]...)
@@ -396,3 +438,19 @@ func (eb *ComplexEnvironmentBuilder) Build() *ComplexEnvironment {
396438

397439
return res
398440
}
441+
442+
// InjectUIDOnObjectCreation returns an interceptor function for Create which injects a random UID into the object, if it does not already have one.
443+
// If additionalLogic is nil, the object is created regularly afterwards.
444+
// Otherwise, additionalLogic is called.
445+
// If you called 'WithUIDs(...)' on the ComplexEnvironmentBuilder AND 'WithFakeClientBuilderCall(..., "WithInterceptorFuncs", ...)', then you need to pass this function into the interceptor.Funcs' Create field, optionally adding your own creation logic via additionalLogic.
446+
func InjectUIDOnObjectCreation(additionalLogic func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error) func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
447+
return func(ctx context.Context, client client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
448+
if obj.GetUID() == "" {
449+
obj.SetUID(uuid.NewUUID())
450+
}
451+
if additionalLogic != nil {
452+
return additionalLogic(ctx, client, obj, opts...)
453+
}
454+
return client.Create(ctx, obj, opts...)
455+
}
456+
}

pkg/testing/environment.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"sigs.k8s.io/controller-runtime/pkg/client"
99
"sigs.k8s.io/controller-runtime/pkg/reconcile"
1010

11+
"github.com/onsi/gomega/types"
12+
1113
"github.com/openmcp-project/controller-utils/pkg/logging"
1214
)
1315

@@ -48,12 +50,22 @@ func (e *Environment) ShouldEventuallyReconcile(req reconcile.Request, timeout,
4850

4951
// ShouldNotReconcile calls the given reconciler with the given request and expects an error.
5052
func (e *Environment) ShouldNotReconcile(req reconcile.Request, optionalDescription ...interface{}) reconcile.Result {
51-
return e.shouldNotReconcile(SimpleEnvironmentDefaultKey, req, optionalDescription...)
53+
return e.shouldNotReconcile(SimpleEnvironmentDefaultKey, req, nil, optionalDescription...)
5254
}
5355

5456
// ShouldEventuallyNotReconcile calls the given reconciler with the given request and retries until an error occurred or the timeout is reached.
5557
func (e *Environment) ShouldEventuallyNotReconcile(req reconcile.Request, timeout, poll time.Duration, optionalDescription ...interface{}) reconcile.Result {
56-
return e.shouldEventuallyNotReconcile(SimpleEnvironmentDefaultKey, req, timeout, poll, optionalDescription...)
58+
return e.shouldEventuallyNotReconcile(SimpleEnvironmentDefaultKey, req, nil, timeout, poll, optionalDescription...)
59+
}
60+
61+
// ShouldNotReconcileWithError calls the given reconciler with the given request and expects an error that matches the given matcher.
62+
func (e *Environment) ShouldNotReconcileWithError(req reconcile.Request, matcher types.GomegaMatcher, optionalDescription ...interface{}) reconcile.Result {
63+
return e.shouldNotReconcile(SimpleEnvironmentDefaultKey, req, matcher, optionalDescription...)
64+
}
65+
66+
// ShouldEventuallyNotReconcileWithError calls the given reconciler with the given request and retries until an error that matches the given matcher occurred or the timeout is reached.
67+
func (e *Environment) ShouldEventuallyNotReconcileWithError(req reconcile.Request, matcher types.GomegaMatcher, timeout, poll time.Duration, optionalDescription ...interface{}) reconcile.Result {
68+
return e.shouldEventuallyNotReconcile(SimpleEnvironmentDefaultKey, req, matcher, timeout, poll, optionalDescription...)
5769
}
5870

5971
//////////////////////////////////
@@ -153,6 +165,16 @@ func (eb *EnvironmentBuilder) WithAfterClientCreationCallback(callback func(clie
153165
return eb
154166
}
155167

168+
// WithUIDs enables UID injection.
169+
// All objects that are initially loaded or afterwards created via the client's 'Create' method will have a random UID injected, if they do not already have one.
170+
// Note that this function registers an interceptor function, which will be overwritten if 'WithFakeClientBuilderCall("WithInterceptorFuncs", ...)' is also called.
171+
// This would lead to newly created objects not having a UID injected.
172+
// To avoid this, pass 'InjectUIDOnObjectCreation(...)' into the interceptor.Funcs' Create field. The argument allows to inject your own additional Create logic, if desired.
173+
func (eb *EnvironmentBuilder) WithUIDs() *EnvironmentBuilder {
174+
eb.ComplexEnvironmentBuilder.WithUIDs(SimpleEnvironmentDefaultKey)
175+
return eb
176+
}
177+
156178
// WithFakeClientBuilderCall allows to inject method calls to fake.ClientBuilder when the fake client is created during Build().
157179
// The fake client is usually created using WithScheme(...).WithObjects(...).WithStatusSubresource(...).Build().
158180
// This function allows to inject additional method calls. It is only required for advanced use-cases.

pkg/testing/matchers/maybematch.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package matchers
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/onsi/gomega/types"
7+
)
8+
9+
// MaybeMatch returns a Gomega matcher that passes the matching logic to the provided matcher,
10+
// but always succeeds if the passed in matcher is nil.
11+
func MaybeMatch(matcher types.GomegaMatcher) types.GomegaMatcher {
12+
return &maybeMatcher{matcher: matcher}
13+
}
14+
15+
type maybeMatcher struct {
16+
matcher types.GomegaMatcher
17+
}
18+
19+
func (m *maybeMatcher) GomegaString() string {
20+
if m == nil || m.matcher == nil {
21+
return "<nil>"
22+
}
23+
return fmt.Sprintf("MaybeMatch(%v)", m.matcher)
24+
}
25+
26+
var _ types.GomegaMatcher = &maybeMatcher{}
27+
28+
// Match implements types.GomegaMatcher.
29+
func (m *maybeMatcher) Match(actualRaw any) (success bool, err error) {
30+
if m.matcher == nil {
31+
return true, nil
32+
}
33+
return m.matcher.Match(actualRaw)
34+
}
35+
36+
// FailureMessage implements types.GomegaMatcher.
37+
func (m *maybeMatcher) FailureMessage(actual any) (message string) {
38+
return m.matcher.FailureMessage(actual)
39+
}
40+
41+
// NegatedFailureMessage implements types.GomegaMatcher.
42+
func (m *maybeMatcher) NegatedFailureMessage(actual any) (message string) {
43+
return m.matcher.NegatedFailureMessage(actual)
44+
}

0 commit comments

Comments
 (0)