Skip to content

Commit fc927bd

Browse files
committed
add controller integration test
1 parent c821619 commit fc927bd

3 files changed

Lines changed: 63 additions & 51 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ jobs:
4141
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
4242
with:
4343
go-version-file: go.mod
44+
- name: Install setup-envtest
45+
run: go install sigs.k8s.io/controller-runtime/tools/setup-envtest@latest
4446
- name: Test
4547
run: |
4648
make test

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,4 @@ fmt:
1919
go fmt ./...
2020

2121
test:
22-
go test ./...
22+
KUBEBUILDER_ASSETS=$$(setup-envtest use -p path) go test -count=1 ./...

internal/controller/controller_integration_test.go

Lines changed: 60 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ func (m *mockRecordPoster) PostOne(_ context.Context, record *deploymentrecord.D
3030
return m.err
3131
}
3232

33-
// Helper to read captured records safely
33+
// Helper that allows tests to read captured records safely
3434
func (m *mockRecordPoster) getRecords() []*deploymentrecord.DeploymentRecord {
3535
m.mu.Lock()
3636
defer m.mu.Unlock()
@@ -207,11 +207,52 @@ func deletePod(t *testing.T, clientset *kubernetes.Clientset, namespace, name st
207207
}
208208
}
209209

210+
// pollForRecords polls until the mock has at least minCount records, then returns them.
211+
func pollForRecords(t *testing.T, mock *mockRecordPoster, minCount int, timeout time.Duration) []*deploymentrecord.DeploymentRecord {
212+
t.Helper()
213+
deadline := time.After(timeout)
214+
for {
215+
records := mock.getRecords()
216+
if len(records) >= minCount {
217+
return records
218+
}
219+
select {
220+
case <-deadline:
221+
t.Fatalf("timed out waiting for at least %d records, got %d", minCount, len(records))
222+
case <-time.After(100 * time.Millisecond):
223+
}
224+
}
225+
}
226+
227+
// assertNoNewRecords polls for the given duration and fails if the record count deviates from expectedCount.
228+
func assertNoNewRecords(t *testing.T, mock *mockRecordPoster, expectedCount int, timeout time.Duration) {
229+
t.Helper()
230+
deadline := time.After(timeout)
231+
for {
232+
select {
233+
case <-deadline:
234+
if got := len(mock.getRecords()); got != expectedCount {
235+
t.Fatalf("expected %d records, got %d", expectedCount, got)
236+
}
237+
return
238+
case <-time.After(100 * time.Millisecond):
239+
if got := len(mock.getRecords()); got != expectedCount {
240+
t.Fatalf("expected %d records, got %d", expectedCount, got)
241+
}
242+
}
243+
}
244+
}
245+
210246
func TestControllerIntegration_KubernetesDeployment(t *testing.T) {
247+
if testing.Short() {
248+
t.Skip("skipping integration test in short mode")
249+
}
211250
namespace := "test-namespace"
212251
testEnv, cancel, clientset, mock := setup(t, namespace)
213-
defer testEnv.Stop()
214-
defer cancel()
252+
defer func(testEnv *envtest.Environment, cancelFunc context.CancelFunc) {
253+
_ = testEnv.Stop()
254+
cancel()
255+
}(testEnv, cancel)
215256

216257
// Create deployment, replicaset, and pod; expect 1 record
217258
deployment := makeDeployment(t, clientset, []metav1.OwnerReference{}, namespace, "test-deployment")
@@ -228,69 +269,38 @@ func TestControllerIntegration_KubernetesDeployment(t *testing.T) {
228269
UID: replicaSet.UID,
229270
}}, namespace, "test-deployment-123456-1")
230271

231-
pollTime := time.After(5 * time.Second)
232-
for {
233-
records := mock.getRecords()
234-
if len(records) > 0 {
235-
if len(records) != 1 {
236-
t.Fatalf("expected 1 record, got %d", len(records))
237-
}
238-
if records[0].Status != deploymentrecord.StatusDeployed {
239-
t.Errorf("expected %s, got %s", deploymentrecord.StatusDeployed, records[0].Status)
240-
}
241-
break
242-
}
243-
select {
244-
case <-pollTime:
245-
t.Fatal("timed out waiting for deployment record")
246-
case <-time.After(100 * time.Millisecond):
247-
}
272+
records := pollForRecords(t, mock, 1, 5*time.Second)
273+
if len(records) != 1 {
274+
t.Fatalf("expected 1 record, got %d", len(records))
275+
}
276+
if records[0].Status != deploymentrecord.StatusDeployed {
277+
t.Errorf("expected %s, got %s", deploymentrecord.StatusDeployed, records[0].Status)
248278
}
249279

250-
// Create another pod in replicaset; the dedup cache should prevent a new record
251-
// but since processing is async, both pods may be processed before the cache is set.
280+
// Create another pod in replicaset; the dedup cache should prevent a new record as there is only one worker
281+
// and no risk of multiple works processing before cache is set.
252282
_ = makePod(t, clientset, []metav1.OwnerReference{{
253283
APIVersion: "apps/v1",
254284
Kind: "ReplicaSet",
255285
Name: replicaSet.Name,
256286
UID: replicaSet.UID,
257287
}}, namespace, "test-deployment-123456-2")
258-
259-
time.Sleep(5 * time.Second)
260-
recordsAfterSecondPod := len(mock.getRecords())
261-
if recordsAfterSecondPod > 2 {
262-
t.Fatalf("expected at most 2 records after second pod, got %d", recordsAfterSecondPod)
263-
}
288+
assertNoNewRecords(t, mock, 1, 5*time.Second)
264289

265290
// Delete second pod; still expect 1 record
266291
deletePod(t, clientset, namespace, "test-deployment-123456-2")
267-
268-
time.Sleep(5 * time.Second) // wait to see if any new records are created
269-
if len(mock.getRecords()) != 1 {
270-
t.Fatalf("still expected 1 record after deleting second pod, got %d", len(mock.getRecords()))
271-
}
292+
assertNoNewRecords(t, mock, 1, 5*time.Second)
272293

273294
// Delete deployment, replicaset, and first pod; expect 2 records
274295
deleteDeployment(t, clientset, namespace, "test-deployment")
275296
deleteReplicaSet(t, clientset, namespace, "test-deployment-123456")
276297
deletePod(t, clientset, namespace, "test-deployment-123456-1")
277298

278-
pollTime = time.After(5 * time.Second)
279-
for {
280-
records := mock.getRecords()
281-
if len(records) > 1 {
282-
if len(records) != 2 {
283-
t.Fatalf("expected 2 records after deletion, got %d", len(records))
284-
}
285-
if records[1].Status != deploymentrecord.StatusDecommissioned {
286-
t.Errorf("expected second record to be %s, got %s", deploymentrecord.StatusDecommissioned, records[1].Status)
287-
}
288-
break
289-
}
290-
select {
291-
case <-pollTime:
292-
t.Fatalf("timed out waiting for more than one record, got %d", len(records))
293-
case <-time.After(100 * time.Millisecond):
294-
}
299+
records = pollForRecords(t, mock, 2, 5*time.Second)
300+
if len(records) != 2 {
301+
t.Fatalf("expected 2 records after deletion, got %d", len(records))
302+
}
303+
if records[1].Status != deploymentrecord.StatusDecommissioned {
304+
t.Errorf("expected second record to be %s, got %s", deploymentrecord.StatusDecommissioned, records[1].Status)
295305
}
296306
}

0 commit comments

Comments
 (0)