Skip to content

Commit 5f52c9d

Browse files
authored
test: replace custom WaitForCondition with testify assertions (containers#550)
Signed-off-by: Marc Nuri <[email protected]>
1 parent c9e57bb commit 5f52c9d

File tree

3 files changed

+66
-84
lines changed

3 files changed

+66
-84
lines changed

internal/test/test.go

Lines changed: 0 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,3 @@ func WaitForHealthz(tcpAddr *net.TCPAddr) error {
7171
}
7272
return fmt.Errorf("healthz endpoint returned 404 after retries")
7373
}
74-
75-
func WaitForCondition(timeout time.Duration, condition func() bool) error {
76-
done := make(chan struct{})
77-
go func() {
78-
for {
79-
if condition() {
80-
close(done)
81-
return
82-
}
83-
time.Sleep(time.Millisecond)
84-
}
85-
}()
86-
87-
select {
88-
case <-done:
89-
// Condition met
90-
case <-time.After(timeout):
91-
return fmt.Errorf("timeout waiting for condition")
92-
}
93-
return nil
94-
}

pkg/kubernetes/watcher/cluster_test.go

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,19 @@ import (
77
"testing"
88
"time"
99

10-
"github.com/containers/kubernetes-mcp-server/internal/test"
1110
"github.com/stretchr/testify/suite"
1211
"k8s.io/client-go/discovery"
1312
"k8s.io/client-go/discovery/cached/memory"
13+
14+
"github.com/containers/kubernetes-mcp-server/internal/test"
1415
)
1516

1617
const (
18+
// eventuallyTick is the polling interval for Eventually assertions
19+
eventuallyTick = time.Millisecond
1720
// watcherStateTimeout is the maximum time to wait for the watcher to capture initial state
1821
watcherStateTimeout = 100 * time.Millisecond
22+
watcherPollTimeout = 250 * time.Millisecond
1923
)
2024

2125
type ClusterStateTestSuite struct {
@@ -33,13 +37,13 @@ func (s *ClusterStateTestSuite) TearDownTest() {
3337
}
3438
}
3539

36-
// waitForWatcherState waits for the watcher to capture initial state
37-
func (s *ClusterStateTestSuite) waitForWatcherState(watcher *ClusterState) {
38-
s.NoError(test.WaitForCondition(watcherStateTimeout, func() bool {
40+
// waitForWatcherInitialState waits for the watcher to capture initial state
41+
func (s *ClusterStateTestSuite) waitForWatcherInitialState(watcher *ClusterState) {
42+
s.Eventually(func() bool {
3943
watcher.mu.Lock()
4044
defer watcher.mu.Unlock()
4145
return len(watcher.lastKnownState.apiGroups) > 0
42-
}), "timeout waiting for watcher to capture initial state")
46+
}, watcherStateTimeout, eventuallyTick, "timeout waiting for watcher to capture initial state")
4347
}
4448

4549
func (s *ClusterStateTestSuite) TestNewClusterState() {
@@ -175,8 +179,7 @@ func (s *ClusterStateTestSuite) TestWatch() {
175179
}()
176180
s.T().Cleanup(watcher.Close)
177181

178-
// Wait for the watcher to capture initial state
179-
s.waitForWatcherState(watcher)
182+
s.waitForWatcherInitialState(watcher)
180183

181184
s.Run("captures API groups", func() {
182185
s.NotEmpty(watcher.lastKnownState.apiGroups, "should capture at least one API group (apps)")
@@ -212,18 +215,17 @@ func (s *ClusterStateTestSuite) TestWatch() {
212215
}()
213216
s.T().Cleanup(watcher.Close)
214217

215-
// Wait for initial state capture
216-
s.waitForWatcherState(watcher)
218+
s.waitForWatcherInitialState(watcher)
217219

218220
// Modify the handler to add new API groups
219221
handler.Groups = []string{
220222
`{"name":"custom.example.com","versions":[{"groupVersion":"custom.example.com/v1","version":"v1"}],"preferredVersion":{"groupVersion":"custom.example.com/v1","version":"v1"}}`,
221223
}
222224

223225
// Wait for change detection - the watcher invalidates the cache on each poll
224-
s.Require().NoError(test.WaitForCondition(500*time.Millisecond, func() bool {
226+
s.Eventually(func() bool {
225227
return callCount.Load() >= 1
226-
}), "timeout waiting for onChange callback")
228+
}, watcherPollTimeout, eventuallyTick, "timeout waiting for onChange callback")
227229

228230
s.GreaterOrEqual(callCount.Load(), int32(1), "onChange should be called at least once")
229231
})
@@ -247,7 +249,7 @@ func (s *ClusterStateTestSuite) TestWatch() {
247249
s.T().Cleanup(watcher.Close)
248250

249251
// Wait for the watcher to capture initial state
250-
s.waitForWatcherState(watcher)
252+
s.waitForWatcherInitialState(watcher)
251253

252254
s.Run("detects OpenShift via API groups", func() {
253255
s.True(watcher.lastKnownState.isOpenShift)
@@ -280,17 +282,17 @@ func (s *ClusterStateTestSuite) TestWatch() {
280282
s.T().Cleanup(watcher.Close)
281283

282284
// Wait for the watcher to start and capture initial state
283-
s.waitForWatcherState(watcher)
285+
s.waitForWatcherInitialState(watcher)
284286

285287
// Modify the handler to trigger a change
286288
handler.Groups = []string{
287289
`{"name":"error.trigger","versions":[{"groupVersion":"error.trigger/v1","version":"v1"}],"preferredVersion":{"groupVersion":"error.trigger/v1","version":"v1"}}`,
288290
}
289291

290292
// Wait for onChange to be called (which returns an error)
291-
s.Require().NoError(test.WaitForCondition(500*time.Millisecond, func() bool {
293+
s.Eventually(func() bool {
292294
return errorCallCount.Load() >= 1
293-
}), "timeout waiting for onChange callback")
295+
}, watcherPollTimeout, eventuallyTick, "timeout waiting for onChange callback")
294296

295297
s.GreaterOrEqual(errorCallCount.Load(), int32(1), "onChange should be called even when it returns an error")
296298
})
@@ -303,6 +305,7 @@ func (s *ClusterStateTestSuite) TestClose() {
303305

304306
watcher := NewClusterState(discoveryClient)
305307
watcher.pollInterval = 50 * time.Millisecond
308+
watcher.debounceWindow = 10 * time.Millisecond
306309

307310
var callCount atomic.Int32
308311
onChange := func() error {
@@ -315,18 +318,17 @@ func (s *ClusterStateTestSuite) TestClose() {
315318
}()
316319

317320
// Wait for the watcher to start
318-
s.waitForWatcherState(watcher)
321+
s.waitForWatcherInitialState(watcher)
319322

320323
watcher.Close()
321324

322325
s.Run("stops polling", func() {
323326
beforeCount := callCount.Load()
324327
// Wait longer than poll interval to verify no more polling
325-
// We expect this to timeout because no callbacks should be triggered after close
326-
err := test.WaitForCondition(150*time.Millisecond, func() bool {
328+
// We expect this to never happen because no callbacks should be triggered after close
329+
s.Never(func() bool {
327330
return callCount.Load() > beforeCount
328-
})
329-
s.Error(err, "should timeout because no polling occurs after close")
331+
}, watcherPollTimeout, eventuallyTick, "should not poll after close")
330332
afterCount := callCount.Load()
331333
s.Equal(beforeCount, afterCount, "should not poll after close")
332334
})
@@ -367,19 +369,19 @@ func (s *ClusterStateTestSuite) TestClose() {
367369
}()
368370

369371
// Wait for the watcher to start
370-
s.waitForWatcherState(watcher)
372+
s.waitForWatcherInitialState(watcher)
371373

372374
// Modify the handler to trigger a change and start the debounce timer
373375
handler.Groups = []string{
374376
`{"name":"trigger.change","versions":[{"groupVersion":"trigger.change/v1","version":"v1"}],"preferredVersion":{"groupVersion":"trigger.change/v1","version":"v1"}}`,
375377
}
376378

377379
// Wait for the change to be detected (debounce timer starts)
378-
s.Require().NoError(test.WaitForCondition(200*time.Millisecond, func() bool {
380+
s.Eventually(func() bool {
379381
watcher.mu.Lock()
380382
defer watcher.mu.Unlock()
381383
return watcher.debounceTimer != nil
382-
}), "timeout waiting for debounce timer to start")
384+
}, watcherPollTimeout, eventuallyTick, "timeout waiting for debounce timer to start")
383385

384386
// Close the watcher before debounce window expires
385387
watcher.Close()

0 commit comments

Comments
 (0)