Skip to content

Commit aa4ca3b

Browse files
committed
add unit test
Signed-off-by: kangclzjc <kangz@nvidia.com>
1 parent 9acb016 commit aa4ca3b

File tree

6 files changed

+1329
-0
lines changed

6 files changed

+1329
-0
lines changed
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
// /*
2+
// Copyright 2025 The Grove 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+
"context"
21+
"testing"
22+
23+
"github.com/ai-dynamo/grove/operator/internal/schedulerbackend"
24+
"github.com/ai-dynamo/grove/operator/internal/schedulerbackend/kai"
25+
"github.com/ai-dynamo/grove/operator/internal/schedulerbackend/kube"
26+
testutils "github.com/ai-dynamo/grove/operator/test/utils"
27+
groveschedulerv1alpha1 "github.com/ai-dynamo/grove/scheduler/api/core/v1alpha1"
28+
29+
"github.com/go-logr/logr"
30+
"github.com/stretchr/testify/assert"
31+
"github.com/stretchr/testify/require"
32+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
33+
"k8s.io/apimachinery/pkg/types"
34+
"k8s.io/client-go/tools/record"
35+
ctrl "sigs.k8s.io/controller-runtime"
36+
"sigs.k8s.io/controller-runtime/pkg/client"
37+
)
38+
39+
// TestReconcile tests the Reconcile method.
40+
func TestReconcile(t *testing.T) {
41+
tests := []struct {
42+
name string
43+
podGang *groveschedulerv1alpha1.PodGang
44+
backendType string
45+
expectError bool
46+
setupMock func(*groveschedulerv1alpha1.PodGang)
47+
verifyBehavior func(*testing.T, ctrl.Result, error)
48+
}{
49+
{
50+
name: "reconcile new podgang with kai backend",
51+
podGang: &groveschedulerv1alpha1.PodGang{
52+
ObjectMeta: metav1.ObjectMeta{
53+
Name: "test-podgang",
54+
Namespace: "default",
55+
},
56+
Spec: groveschedulerv1alpha1.PodGangSpec{
57+
PodGroups: []groveschedulerv1alpha1.PodGroup{
58+
{
59+
Name: "group-0",
60+
MinReplicas: 3,
61+
},
62+
},
63+
},
64+
},
65+
backendType: "kai",
66+
expectError: false,
67+
},
68+
{
69+
name: "reconcile new podgang with kube backend",
70+
podGang: &groveschedulerv1alpha1.PodGang{
71+
ObjectMeta: metav1.ObjectMeta{
72+
Name: "test-podgang",
73+
Namespace: "default",
74+
},
75+
Spec: groveschedulerv1alpha1.PodGangSpec{
76+
PodGroups: []groveschedulerv1alpha1.PodGroup{
77+
{
78+
Name: "group-0",
79+
MinReplicas: 3,
80+
},
81+
},
82+
},
83+
},
84+
backendType: "kube",
85+
expectError: false,
86+
},
87+
{
88+
name: "reconcile deleted podgang",
89+
podGang: &groveschedulerv1alpha1.PodGang{
90+
ObjectMeta: metav1.ObjectMeta{
91+
Name: "test-podgang",
92+
Namespace: "default",
93+
DeletionTimestamp: &metav1.Time{},
94+
Finalizers: []string{"test-finalizer"},
95+
},
96+
Spec: groveschedulerv1alpha1.PodGangSpec{
97+
PodGroups: []groveschedulerv1alpha1.PodGroup{
98+
{
99+
Name: "group-0",
100+
MinReplicas: 3,
101+
},
102+
},
103+
},
104+
},
105+
backendType: "kai",
106+
expectError: false,
107+
},
108+
{
109+
name: "reconcile updated podgang",
110+
podGang: &groveschedulerv1alpha1.PodGang{
111+
ObjectMeta: metav1.ObjectMeta{
112+
Name: "test-podgang",
113+
Namespace: "default",
114+
Generation: 2,
115+
},
116+
Spec: groveschedulerv1alpha1.PodGangSpec{
117+
PodGroups: []groveschedulerv1alpha1.PodGroup{
118+
{
119+
Name: "group-0",
120+
MinReplicas: 5,
121+
},
122+
},
123+
},
124+
},
125+
backendType: "kai",
126+
expectError: false,
127+
},
128+
}
129+
130+
for _, tt := range tests {
131+
t.Run(tt.name, func(t *testing.T) {
132+
// Setup client with podgang
133+
cl := testutils.CreateDefaultFakeClient([]client.Object{tt.podGang})
134+
recorder := record.NewFakeRecorder(10)
135+
136+
// Create appropriate backend
137+
var backend schedulerbackend.SchedulerBackend
138+
if tt.backendType == "kai" {
139+
backend = kai.New(cl, cl.Scheme(), recorder, "kai-scheduler")
140+
} else {
141+
backend = kube.New(cl, cl.Scheme(), recorder, "default-scheduler")
142+
}
143+
144+
reconciler := &BackendReconciler{
145+
Client: cl,
146+
Scheme: cl.Scheme(),
147+
Backend: backend,
148+
Logger: logr.Discard(),
149+
}
150+
151+
// Execute reconcile
152+
ctx := context.Background()
153+
req := ctrl.Request{
154+
NamespacedName: types.NamespacedName{
155+
Name: tt.podGang.Name,
156+
Namespace: tt.podGang.Namespace,
157+
},
158+
}
159+
160+
result, err := reconciler.Reconcile(ctx, req)
161+
162+
// Verify results
163+
if tt.expectError {
164+
require.Error(t, err)
165+
} else {
166+
require.NoError(t, err)
167+
assert.Equal(t, ctrl.Result{}, result)
168+
}
169+
170+
// Custom verification if provided
171+
if tt.verifyBehavior != nil {
172+
tt.verifyBehavior(t, result, err)
173+
}
174+
})
175+
}
176+
}
177+
178+
// TestReconcilePodGangNotFound tests reconciling a non-existent PodGang.
179+
func TestReconcilePodGangNotFound(t *testing.T) {
180+
// Setup client without any podgang
181+
cl := testutils.CreateDefaultFakeClient(nil)
182+
recorder := record.NewFakeRecorder(10)
183+
backend := kai.New(cl, cl.Scheme(), recorder, "kai-scheduler")
184+
185+
reconciler := &BackendReconciler{
186+
Client: cl,
187+
Scheme: cl.Scheme(),
188+
Backend: backend,
189+
Logger: logr.Discard(),
190+
}
191+
192+
ctx := context.Background()
193+
req := ctrl.Request{
194+
NamespacedName: types.NamespacedName{
195+
Name: "non-existent",
196+
Namespace: "default",
197+
},
198+
}
199+
200+
result, err := reconciler.Reconcile(ctx, req)
201+
202+
// Should not error when PodGang is not found (likely deleted)
203+
require.NoError(t, err)
204+
assert.Equal(t, ctrl.Result{}, result)
205+
}
206+
207+
// TestReconcilePodGangWithDeletionTimestamp tests reconciling a PodGang being deleted.
208+
func TestReconcilePodGangWithDeletionTimestamp(t *testing.T) {
209+
now := metav1.Now()
210+
podGang := &groveschedulerv1alpha1.PodGang{
211+
ObjectMeta: metav1.ObjectMeta{
212+
Name: "test-podgang",
213+
Namespace: "default",
214+
DeletionTimestamp: &now,
215+
Finalizers: []string{"test-finalizer"},
216+
},
217+
Spec: groveschedulerv1alpha1.PodGangSpec{
218+
PodGroups: []groveschedulerv1alpha1.PodGroup{
219+
{
220+
Name: "group-0",
221+
MinReplicas: 3,
222+
},
223+
},
224+
},
225+
}
226+
227+
cl := testutils.CreateDefaultFakeClient([]client.Object{podGang})
228+
recorder := record.NewFakeRecorder(10)
229+
backend := kai.New(cl, cl.Scheme(), recorder, "kai-scheduler")
230+
231+
reconciler := &BackendReconciler{
232+
Client: cl,
233+
Scheme: cl.Scheme(),
234+
Backend: backend,
235+
Logger: logr.Discard(),
236+
}
237+
238+
ctx := context.Background()
239+
req := ctrl.Request{
240+
NamespacedName: types.NamespacedName{
241+
Name: podGang.Name,
242+
Namespace: podGang.Namespace,
243+
},
244+
}
245+
246+
result, err := reconciler.Reconcile(ctx, req)
247+
248+
// Should handle deletion without error
249+
require.NoError(t, err)
250+
assert.Equal(t, ctrl.Result{}, result)
251+
}
252+
253+
// TestNewReconcilerWithBackend tests creating a reconciler with explicit backend.
254+
func TestNewReconcilerWithBackend(t *testing.T) {
255+
cl := testutils.CreateDefaultFakeClient(nil)
256+
recorder := record.NewFakeRecorder(10)
257+
backend := kai.New(cl, cl.Scheme(), recorder, "kai-scheduler")
258+
259+
reconciler := NewReconcilerWithBackend(cl, cl.Scheme(), backend)
260+
261+
require.NotNil(t, reconciler)
262+
assert.Equal(t, cl, reconciler.Client)
263+
assert.Equal(t, cl.Scheme(), reconciler.Scheme)
264+
assert.Equal(t, backend, reconciler.Backend)
265+
}
266+
267+
// TestPodGangSpecChangePredicate tests the event filter for PodGang changes.
268+
func TestPodGangSpecChangePredicate(t *testing.T) {
269+
predicate := podGangSpecChangePredicate()
270+
271+
// Verify that predicate is not nil
272+
require.NotNil(t, predicate)
273+
274+
// The predicate is tested indirectly through controller integration
275+
// Direct testing of event filters requires complex event setup
276+
// This test ensures the predicate creation doesn't panic
277+
assert.NotNil(t, predicate)
278+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// /*
2+
// Copyright 2025 The Grove 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+
"testing"
21+
22+
"github.com/ai-dynamo/grove/operator/internal/schedulerbackend"
23+
"github.com/ai-dynamo/grove/operator/internal/schedulerbackend/kai"
24+
"github.com/ai-dynamo/grove/operator/internal/schedulerbackend/kube"
25+
testutils "github.com/ai-dynamo/grove/operator/test/utils"
26+
27+
"github.com/stretchr/testify/assert"
28+
"github.com/stretchr/testify/require"
29+
"k8s.io/client-go/tools/record"
30+
)
31+
32+
// TestNewReconcilerWithBackendKai tests creating a reconciler with explicit kai backend.
33+
func TestNewReconcilerWithBackendKai(t *testing.T) {
34+
tests := []struct {
35+
name string
36+
backendType string
37+
expectedName string
38+
}{
39+
{
40+
name: "create reconciler with kai backend",
41+
backendType: "kai",
42+
expectedName: "KAI-Scheduler",
43+
},
44+
{
45+
name: "create reconciler with kube backend",
46+
backendType: "kube",
47+
expectedName: "Kube-Scheduler",
48+
},
49+
}
50+
51+
for _, tt := range tests {
52+
t.Run(tt.name, func(t *testing.T) {
53+
cl := testutils.CreateDefaultFakeClient(nil)
54+
recorder := record.NewFakeRecorder(10)
55+
56+
var backend schedulerbackend.SchedulerBackend
57+
if tt.backendType == "kai" {
58+
backend = kai.New(cl, cl.Scheme(), recorder, "kai-scheduler")
59+
} else {
60+
backend = kube.New(cl, cl.Scheme(), recorder, "default-scheduler")
61+
}
62+
63+
reconciler := NewReconcilerWithBackend(cl, cl.Scheme(), backend)
64+
65+
require.NotNil(t, reconciler)
66+
assert.Equal(t, cl, reconciler.Client)
67+
assert.Equal(t, cl.Scheme(), reconciler.Scheme)
68+
assert.Equal(t, backend, reconciler.Backend)
69+
assert.Equal(t, tt.expectedName, reconciler.Backend.Name())
70+
})
71+
}
72+
}
73+
74+
// TestNewReconcilerWithBackendNilBackend tests NewReconcilerWithBackend with nil backend.
75+
func TestNewReconcilerWithBackendNilBackend(t *testing.T) {
76+
cl := testutils.CreateDefaultFakeClient(nil)
77+
78+
// Should not panic even with nil backend
79+
assert.NotPanics(t, func() {
80+
reconciler := NewReconcilerWithBackend(cl, cl.Scheme(), nil)
81+
assert.NotNil(t, reconciler)
82+
assert.Nil(t, reconciler.Backend)
83+
})
84+
}
85+
86+
// TestNewReconcilerWithBackendFields tests that all fields are correctly set.
87+
func TestNewReconcilerWithBackendFields(t *testing.T) {
88+
cl := testutils.CreateDefaultFakeClient(nil)
89+
recorder := record.NewFakeRecorder(10)
90+
backend := kai.New(cl, cl.Scheme(), recorder, "kai-scheduler")
91+
92+
reconciler := NewReconcilerWithBackend(cl, cl.Scheme(), backend)
93+
94+
// Verify all fields are set correctly
95+
require.NotNil(t, reconciler)
96+
assert.NotNil(t, reconciler.Client, "Client should not be nil")
97+
assert.NotNil(t, reconciler.Scheme, "Scheme should not be nil")
98+
assert.NotNil(t, reconciler.Backend, "Backend should not be nil")
99+
assert.Equal(t, "KAI-Scheduler", reconciler.Backend.Name())
100+
}

0 commit comments

Comments
 (0)