Skip to content

Commit 5dea924

Browse files
committed
factory: improve unit test coverage
1 parent 256dedc commit 5dea924

File tree

5 files changed

+379
-5
lines changed

5 files changed

+379
-5
lines changed

pkg/controller/factory/base_controller.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,12 @@ func (c *baseController) processNextWorkItem(queueCtx context.Context) {
139139
var ok bool
140140
syncCtx.queueKey, ok = key.(string)
141141
if !ok {
142-
klog.Warningf("Queue key is not string: %+v", key)
142+
utilruntime.HandleError(fmt.Errorf("%q controller failed to process key %q (not a string)", c.name, key))
143+
return
143144
}
144145

145146
if err := c.sync(queueCtx, syncCtx); err != nil {
146-
utilruntime.HandleError(fmt.Errorf("%s controller failed to sync %+v with: %w", c.name, key, err))
147+
utilruntime.HandleError(fmt.Errorf("%q controller failed to sync %q, err: %w", c.name, key, err))
147148
c.syncContext.Queue().AddRateLimited(key)
148149
return
149150
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package factory
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
"testing"
8+
"time"
9+
10+
"k8s.io/client-go/tools/cache"
11+
12+
"github.com/openshift/library-go/pkg/operator/events/eventstesting"
13+
)
14+
15+
type fakeInformer struct {
16+
hasSyncedDelay time.Duration
17+
eventHandler cache.ResourceEventHandler
18+
addEventHandlerCount int
19+
hasSyncedCount int
20+
sync.Mutex
21+
}
22+
23+
func (f *fakeInformer) AddEventHandler(handler cache.ResourceEventHandler) {
24+
f.Lock()
25+
defer func() { f.addEventHandlerCount++; f.Unlock() }()
26+
f.eventHandler = handler
27+
}
28+
29+
func (f *fakeInformer) HasSynced() bool {
30+
f.Lock()
31+
defer func() { f.hasSyncedCount++; f.Unlock() }()
32+
time.Sleep(f.hasSyncedDelay)
33+
return true
34+
}
35+
36+
func TestBaseController_Run(t *testing.T) {
37+
informer := &fakeInformer{hasSyncedDelay: 200 * time.Millisecond}
38+
controllerCtx, cancel := context.WithCancel(context.Background())
39+
syncCount := 0
40+
postStartHookSyncCount := 0
41+
postStartHookDone := false
42+
43+
c := &baseController{
44+
name: "test",
45+
cachesToSync: []cache.InformerSynced{informer.HasSynced},
46+
sync: func(ctx context.Context, syncCtx SyncContext) error {
47+
defer func() { syncCount++ }()
48+
defer t.Logf("Sync() call with %q", syncCtx.QueueKey())
49+
if syncCtx.QueueKey() == "postStartHookKey" {
50+
postStartHookSyncCount++
51+
return fmt.Errorf("test error")
52+
}
53+
return nil
54+
},
55+
syncContext: NewSyncContext("test", eventstesting.NewTestingEventRecorder(t)),
56+
resyncEvery: 200 * time.Millisecond,
57+
postStartHooks: []PostStartHook{func(ctx context.Context, syncContext SyncContext) error {
58+
defer func() {
59+
postStartHookDone = true
60+
}()
61+
syncContext.Queue().Add("postStartHookKey")
62+
<-ctx.Done()
63+
t.Logf("post start hook terminated")
64+
return nil
65+
}},
66+
}
67+
68+
time.AfterFunc(1*time.Second, func() {
69+
cancel()
70+
})
71+
c.Run(controllerCtx, 1)
72+
73+
informer.Lock()
74+
if informer.hasSyncedCount == 0 {
75+
t.Errorf("expected HasSynced() called at least once, got 0")
76+
}
77+
informer.Unlock()
78+
if syncCount == 0 {
79+
t.Errorf("expected at least one sync call, got 0")
80+
}
81+
if postStartHookSyncCount == 0 {
82+
t.Errorf("expected the post start hook queue key, got none")
83+
}
84+
if !postStartHookDone {
85+
t.Errorf("expected the post start hook to be terminated when context is cancelled")
86+
}
87+
}
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package factory
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync"
7+
"testing"
8+
"time"
9+
10+
corev1 "k8s.io/api/core/v1"
11+
"k8s.io/apimachinery/pkg/api/meta"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/apimachinery/pkg/runtime"
14+
"k8s.io/apimachinery/pkg/util/sets"
15+
"k8s.io/apimachinery/pkg/util/wait"
16+
"k8s.io/client-go/tools/cache"
17+
18+
"github.com/openshift/library-go/pkg/operator/events/eventstesting"
19+
)
20+
21+
type threadSafeStringSet struct {
22+
sets.String
23+
sync.Mutex
24+
}
25+
26+
func newThreadSafeStringSet() *threadSafeStringSet {
27+
return &threadSafeStringSet{
28+
String: sets.NewString(),
29+
}
30+
}
31+
32+
func (s *threadSafeStringSet) Len() int {
33+
s.Lock()
34+
defer s.Unlock()
35+
return s.String.Len()
36+
}
37+
38+
func (s *threadSafeStringSet) Insert(items ...string) *threadSafeStringSet {
39+
s.Lock()
40+
defer s.Unlock()
41+
s.String.Insert(items...)
42+
return s
43+
}
44+
45+
func TestSyncContext_eventHandler(t *testing.T) {
46+
tests := []struct {
47+
name string
48+
syncContext SyncContext
49+
queueKeyFunc ObjectQueueKeyFunc
50+
interestingNamespaces sets.String
51+
// event handler test
52+
53+
runEventHandlers func(cache.ResourceEventHandler)
54+
evalQueueItems func(*threadSafeStringSet, *testing.T)
55+
expectedItemCount int
56+
// func (c syncContext) eventHandler(queueKeyFunc ObjectQueueKeyFunc, interestingNamespaces sets.String) cache.ResourceEventHandler {
57+
58+
}{
59+
{
60+
name: "simple event handler",
61+
syncContext: NewSyncContext("test", eventstesting.NewTestingEventRecorder(t)),
62+
queueKeyFunc: func(object runtime.Object) string {
63+
m, _ := meta.Accessor(object)
64+
return fmt.Sprintf("%s/%s", m.GetNamespace(), m.GetName())
65+
},
66+
runEventHandlers: func(handler cache.ResourceEventHandler) {
67+
handler.OnAdd(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "add"}})
68+
handler.OnUpdate(nil, &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "update"}})
69+
handler.OnDelete(&corev1.Secret{ObjectMeta: metav1.ObjectMeta{Namespace: "foo", Name: "delete"}})
70+
},
71+
expectedItemCount: 3,
72+
evalQueueItems: func(s *threadSafeStringSet, t *testing.T) {
73+
expect := []string{"add", "update", "delete"}
74+
for _, e := range expect {
75+
if !s.Has("foo/" + e) {
76+
t.Errorf("expected %#v to has 'foo/%s'", s.List(), e)
77+
}
78+
}
79+
},
80+
},
81+
{
82+
name: "namespace event handler",
83+
syncContext: NewSyncContext("test", eventstesting.NewTestingEventRecorder(t)),
84+
queueKeyFunc: func(object runtime.Object) string {
85+
m, _ := meta.Accessor(object)
86+
return m.GetName()
87+
},
88+
interestingNamespaces: sets.NewString("add"),
89+
runEventHandlers: func(handler cache.ResourceEventHandler) {
90+
handler.OnAdd(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "add"}})
91+
handler.OnUpdate(nil, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "update"}})
92+
handler.OnDelete(&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "delete"}})
93+
},
94+
expectedItemCount: 1,
95+
evalQueueItems: func(s *threadSafeStringSet, t *testing.T) {
96+
if !s.Has("add") {
97+
t.Errorf("expected %#v to has only 'add'", s.List())
98+
}
99+
},
100+
},
101+
{
102+
name: "namespace from tombstone event handler",
103+
syncContext: NewSyncContext("test", eventstesting.NewTestingEventRecorder(t)),
104+
queueKeyFunc: func(object runtime.Object) string {
105+
m, _ := meta.Accessor(object)
106+
return m.GetName()
107+
},
108+
interestingNamespaces: sets.NewString("delete"),
109+
runEventHandlers: func(handler cache.ResourceEventHandler) {
110+
handler.OnDelete(cache.DeletedFinalStateUnknown{
111+
Obj: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "delete"}},
112+
})
113+
},
114+
expectedItemCount: 1,
115+
evalQueueItems: func(s *threadSafeStringSet, t *testing.T) {
116+
if !s.Has("delete") {
117+
t.Errorf("expected %#v to has only 'add'", s.List())
118+
}
119+
},
120+
},
121+
}
122+
123+
for _, test := range tests {
124+
t.Run(test.name, func(t *testing.T) {
125+
handler := test.syncContext.(syncContext).eventHandler(test.queueKeyFunc, test.interestingNamespaces)
126+
itemsReceived := newThreadSafeStringSet()
127+
queueCtx, shutdown := context.WithCancel(context.Background())
128+
c := &baseController{
129+
syncContext: test.syncContext,
130+
sync: func(ctx context.Context, controllerContext SyncContext) error {
131+
itemsReceived.Insert(controllerContext.QueueKey())
132+
return nil
133+
},
134+
}
135+
go c.runWorker(queueCtx)
136+
137+
// simulate events coming from informer
138+
test.runEventHandlers(handler)
139+
140+
// wait for expected items to be processed
141+
if err := wait.PollImmediate(10*time.Millisecond, 10*time.Second, func() (done bool, err error) {
142+
return itemsReceived.Len() == test.expectedItemCount, nil
143+
}); err != nil {
144+
t.Errorf("%w (received: %#v)", err, itemsReceived.List())
145+
shutdown()
146+
return
147+
}
148+
149+
// stop the worker
150+
shutdown()
151+
152+
if itemsReceived.Len() != test.expectedItemCount {
153+
t.Errorf("expected %d items received, got %d (%#v)", test.expectedItemCount, itemsReceived.Len(), itemsReceived.List())
154+
}
155+
// evaluate items received
156+
test.evalQueueItems(itemsReceived, t)
157+
})
158+
}
159+
}
160+
161+
func TestSyncContext_isInterestingNamespace(t *testing.T) {
162+
tests := []struct {
163+
name string
164+
obj interface{}
165+
namespaces sets.String
166+
expectNamespace bool
167+
expectInteresting bool
168+
}{
169+
{
170+
name: "got interesting namespace",
171+
obj: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}},
172+
namespaces: sets.NewString("test"),
173+
expectNamespace: true,
174+
expectInteresting: true,
175+
},
176+
{
177+
name: "got non-interesting namespace",
178+
obj: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}},
179+
namespaces: sets.NewString("non-test"),
180+
expectNamespace: true,
181+
expectInteresting: false,
182+
},
183+
{
184+
name: "got interesting namespace in tombstone",
185+
obj: cache.DeletedFinalStateUnknown{
186+
Obj: &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "test"}},
187+
},
188+
namespaces: sets.NewString("test"),
189+
expectNamespace: true,
190+
expectInteresting: true,
191+
},
192+
{
193+
name: "got secret in tombstone",
194+
obj: cache.DeletedFinalStateUnknown{
195+
Obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{Name: "test"}},
196+
},
197+
namespaces: sets.NewString("test"),
198+
expectNamespace: false,
199+
expectInteresting: false,
200+
},
201+
}
202+
203+
for _, test := range tests {
204+
t.Run(test.name, func(t *testing.T) {
205+
c := syncContext{}
206+
gotNamespace, isInteresting := c.isInterestingNamespace(test.obj, test.namespaces)
207+
if !gotNamespace && test.expectNamespace {
208+
t.Errorf("expected to get Namespace, got false")
209+
}
210+
if gotNamespace && !test.expectNamespace {
211+
t.Errorf("expected to not get Namespace, got true")
212+
}
213+
if !isInteresting && test.expectInteresting {
214+
t.Errorf("expected Namespace to be interesting, got false")
215+
}
216+
if isInteresting && !test.expectInteresting {
217+
t.Errorf("expected Namespace to not be interesting, got true")
218+
}
219+
})
220+
}
221+
}

pkg/controller/factory/factory.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ func (f *Factory) WithSyncContext(ctx SyncContext) *Factory {
125125
// Controller produce a runnable controller.
126126
func (f *Factory) ToController(name string, eventRecorder events.Recorder) Controller {
127127
if f.sync == nil {
128-
panic("Sync() function must be called before making controller")
128+
panic("WithSync() must be used before calling ToController()")
129129
}
130130

131131
var ctx SyncContext

0 commit comments

Comments
 (0)