Skip to content

Commit 1c67ed0

Browse files
pohlygoogs1025
authored andcommitted
feature(dra): add mock workqueue for controller unit test
1 parent dc481fe commit 1c67ed0

File tree

1 file changed

+165
-0
lines changed

1 file changed

+165
-0
lines changed
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
/*
2+
Copyright 2024 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package controller
18+
19+
import (
20+
"slices"
21+
"time"
22+
23+
"k8s.io/client-go/util/workqueue"
24+
)
25+
26+
// TODO (pohly): move this to k8s.io/client-go/util/workqueue/workqueue.go
27+
// if it turns out to be generally useful. Doc comments are already written
28+
// as if the code was there.
29+
30+
// MockQueue is an implementation of [TypedRateLimitingInterface] which
31+
// can be used to test a function which pulls work items out of a queue
32+
// and processes them.
33+
//
34+
// A null instance is directly usable. The usual usage is:
35+
//
36+
// var m workqueue.Mock[string]
37+
// m.SyncOne("some-item", func(queue workqueue.TypedRateLimitingInterface[string]) { ... } )
38+
// if diff := cmp.Diff(workqueue.Mock[string]{}, m); diff != "" {
39+
// t.Errorf("unexpected state of mock work queue after sync (-want, +got):\n%s", diff)
40+
// }
41+
//
42+
// All slices get reset to nil when they become empty, so there are no spurious
43+
// differences because of the nil vs. empty slice.
44+
type Mock[T comparable] struct {
45+
// Ready contains the items which are ready for processing.
46+
Ready []T
47+
48+
// InFlight contains the items which are currently being processed (= Get
49+
// was called, Done not yet).
50+
InFlight []T
51+
52+
// MismatchedDone contains the items for which Done was called without
53+
// a matching Get.
54+
MismatchedDone []T
55+
56+
// Later contains the items which are meant to be added to the queue after
57+
// a certain delay (= AddAfter was called for them).
58+
Later []MockDelayedItem[T]
59+
60+
// Failures contains the items and their retry count which failed to be
61+
// processed (AddRateLimited called at least once, Forget not yet).
62+
// The retry count is always larger than zero.
63+
Failures map[T]int
64+
65+
// ShutDownCalled tracks how often ShutDown got called.
66+
ShutDownCalled int
67+
68+
// ShutDownWithDrainCalled tracks how often ShutDownWithDrain got called.
69+
ShutDownWithDrainCalled int
70+
}
71+
72+
// MockDelayedItem is an item which was queue for later processing.
73+
type MockDelayedItem[T comparable] struct {
74+
Item T
75+
Duration time.Duration
76+
}
77+
78+
// SyncOne adds the item to the work queue and calls sync.
79+
// That sync function can pull one or more items from the work
80+
// queue until the queue is empty. Then it is told that the queue
81+
// is shutting down, which must cause it to return.
82+
//
83+
// The test can then retrieve the state of the queue to check the result.
84+
func (m *Mock[T]) SyncOne(item T, sync func(workqueue.TypedRateLimitingInterface[T])) {
85+
m.Ready = append(m.Ready, item)
86+
sync(m)
87+
}
88+
89+
// Add implements [TypedInterface].
90+
func (m *Mock[T]) Add(item T) {
91+
m.Ready = append(m.Ready, item)
92+
}
93+
94+
// Len implements [TypedInterface].
95+
func (m *Mock[T]) Len() int {
96+
return len(m.Ready)
97+
}
98+
99+
// Get implements [TypedInterface].
100+
func (m *Mock[T]) Get() (item T, shutdown bool) {
101+
if len(m.Ready) == 0 {
102+
shutdown = true
103+
return
104+
}
105+
item = m.Ready[0]
106+
m.Ready = m.Ready[1:]
107+
if len(m.Ready) == 0 {
108+
m.Ready = nil
109+
}
110+
m.InFlight = append(m.InFlight, item)
111+
return item, false
112+
}
113+
114+
// Done implements [TypedInterface].
115+
func (m *Mock[T]) Done(item T) {
116+
index := slices.Index(m.InFlight, item)
117+
if index < 0 {
118+
m.MismatchedDone = append(m.MismatchedDone, item)
119+
}
120+
m.InFlight = slices.Delete(m.InFlight, index, index+1)
121+
if len(m.InFlight) == 0 {
122+
m.InFlight = nil
123+
}
124+
}
125+
126+
// ShutDown implements [TypedInterface].
127+
func (m *Mock[T]) ShutDown() {
128+
m.ShutDownCalled++
129+
}
130+
131+
// ShutDownWithDrain implements [TypedInterface].
132+
func (m *Mock[T]) ShutDownWithDrain() {
133+
m.ShutDownWithDrainCalled++
134+
}
135+
136+
// ShuttingDown implements [TypedInterface].
137+
func (m *Mock[T]) ShuttingDown() bool {
138+
return m.ShutDownCalled > 0 || m.ShutDownWithDrainCalled > 0
139+
}
140+
141+
// AddAfter implements [TypedDelayingInterface.AddAfter]
142+
func (m *Mock[T]) AddAfter(item T, duration time.Duration) {
143+
m.Later = append(m.Later, MockDelayedItem[T]{Item: item, Duration: duration})
144+
}
145+
146+
// AddRateLimited implements [TypedRateLimitingInterface.AddRateLimited].
147+
func (m *Mock[T]) AddRateLimited(item T) {
148+
if m.Failures == nil {
149+
m.Failures = make(map[T]int)
150+
}
151+
m.Failures[item]++
152+
}
153+
154+
// Forget implements [TypedRateLimitingInterface.Forget].
155+
func (m *Mock[T]) Forget(item T) {
156+
if m.Failures == nil {
157+
return
158+
}
159+
delete(m.Failures, item)
160+
}
161+
162+
// NumRequeues implements [TypedRateLimitingInterface.NumRequeues].
163+
func (m *Mock[T]) NumRequeues(item T) int {
164+
return m.Failures[item]
165+
}

0 commit comments

Comments
 (0)