Skip to content

Commit 26df233

Browse files
committed
Enhance the nodeselector and tests
1 parent 88b971a commit 26df233

2 files changed

Lines changed: 347 additions & 1 deletion

File tree

pkg/utils/node/node.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func parseCondition(expr string) (*Condition, error) {
115115
/// Default to ==
116116
qty, err := resource.ParseQuantity(expr)
117117
if err != nil {
118-
return nil, fmt.Errorf("invalid expression %q: must be like '>=100Mi', '==4', etc.", expr)
118+
return nil, fmt.Errorf("invalid expression %q: must be like '>=100Mi', '==4', etc", expr)
119119
}
120120
return &Condition{Op: "==", Val: qty}, nil
121121
}

pkg/utils/node/node_test.go

Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
/*
2+
Copyright 2025 The llm-d 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 node
18+
19+
import (
20+
"testing"
21+
22+
fmav1alpha1 "github.com/llm-d-incubation/llm-d-fast-model-actuation/api/v1alpha1"
23+
corev1 "k8s.io/api/core/v1"
24+
"k8s.io/apimachinery/pkg/api/resource"
25+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
26+
)
27+
28+
func TestFilterNodes(t *testing.T) {
29+
// 创建测试节点
30+
nodes := []*corev1.Node{
31+
{
32+
ObjectMeta: metav1.ObjectMeta{
33+
Name: "node1",
34+
Labels: map[string]string{
35+
"node.kubernetes.io/instance-type": "gx3-48x240x2l40s",
36+
"nvidia.com/gpu.family": "ada-lovelace",
37+
"nvidia.com/gpu.product": "NVIDIA-L40S",
38+
},
39+
},
40+
Status: corev1.NodeStatus{
41+
Allocatable: corev1.ResourceList{
42+
"cpu": resource.MustParse("47"),
43+
"memory": resource.MustParse("243293320Ki"),
44+
"nvidia.com/gpu": resource.MustParse("2"),
45+
},
46+
Capacity: corev1.ResourceList{
47+
"cpu": resource.MustParse("48"),
48+
"memory": resource.MustParse("245760Mi"),
49+
"nvidia.com/gpu": resource.MustParse("2"),
50+
},
51+
},
52+
},
53+
{
54+
ObjectMeta: metav1.ObjectMeta{
55+
Name: "node2",
56+
Labels: map[string]string{
57+
"node.kubernetes.io/instance-type": "other-type",
58+
"nvidia.com/gpu.family": "other-family",
59+
},
60+
},
61+
Status: corev1.NodeStatus{
62+
Allocatable: corev1.ResourceList{
63+
"cpu": resource.MustParse("16"),
64+
"memory": resource.MustParse("65536Mi"),
65+
"nvidia.com/gpu": resource.MustParse("0"),
66+
},
67+
},
68+
},
69+
}
70+
71+
tests := []struct {
72+
name string
73+
selector *fmav1alpha1.EnhancedNodeSelector
74+
expected []string
75+
}{
76+
{
77+
name: "label selector match",
78+
selector: &fmav1alpha1.EnhancedNodeSelector{
79+
LabelSelector: &metav1.LabelSelector{
80+
MatchLabels: map[string]string{
81+
"nvidia.com/gpu.family": "ada-lovelace",
82+
},
83+
},
84+
},
85+
expected: []string{"node1"},
86+
},
87+
{
88+
name: "no selector",
89+
selector: &fmav1alpha1.EnhancedNodeSelector{
90+
LabelSelector: nil,
91+
},
92+
expected: []string{"node1", "node2"},
93+
},
94+
}
95+
96+
for _, tt := range tests {
97+
t.Run(tt.name, func(t *testing.T) {
98+
result, err := FilterNodes(nodes, tt.selector)
99+
if err != nil {
100+
t.Errorf("FilterNodes() error = %v", err)
101+
return
102+
}
103+
104+
if len(result) != len(tt.expected) {
105+
t.Errorf("FilterNodes() got %d nodes, want %d", len(result), len(tt.expected))
106+
return
107+
}
108+
109+
for i, node := range result {
110+
if node.Name != tt.expected[i] {
111+
t.Errorf("FilterNodes() node[%d] = %v, want %v", i, node.Name, tt.expected[i])
112+
}
113+
}
114+
})
115+
}
116+
}
117+
118+
func TestMatchesResourceRequirements(t *testing.T) {
119+
node := &corev1.Node{
120+
Status: corev1.NodeStatus{
121+
Allocatable: corev1.ResourceList{
122+
"cpu": resource.MustParse("47"),
123+
"memory": resource.MustParse("243293320Ki"),
124+
"nvidia.com/gpu": resource.MustParse("2"),
125+
},
126+
Capacity: corev1.ResourceList{
127+
"cpu": resource.MustParse("48"),
128+
"memory": resource.MustParse("245760Mi"),
129+
"nvidia.com/gpu": resource.MustParse("2"),
130+
},
131+
},
132+
}
133+
134+
tests := []struct {
135+
name string
136+
req *fmav1alpha1.ResourceRequirementSpec
137+
expected bool
138+
}{
139+
{
140+
name: "nil requirements",
141+
req: nil,
142+
expected: true,
143+
},
144+
{
145+
name: "match allocatable GPU count",
146+
req: &fmav1alpha1.ResourceRequirementSpec{
147+
Allocatable: map[string]string{
148+
"nvidia.com/gpu": ">=1",
149+
},
150+
},
151+
expected: true,
152+
},
153+
{
154+
name: "no matching allocatable, fallback to capacity",
155+
req: &fmav1alpha1.ResourceRequirementSpec{
156+
Allocatable: map[string]string{},
157+
Capacity: map[string]string{
158+
"nvidia.com/gpu": "==2",
159+
},
160+
},
161+
expected: true,
162+
},
163+
{
164+
name: "allocatable memory requirement not met",
165+
req: &fmav1alpha1.ResourceRequirementSpec{
166+
Allocatable: map[string]string{
167+
"memory": ">300Gi",
168+
},
169+
},
170+
expected: false,
171+
},
172+
{
173+
name: "capacity CPU requirement met",
174+
req: &fmav1alpha1.ResourceRequirementSpec{
175+
Capacity: map[string]string{
176+
"cpu": "<50",
177+
},
178+
},
179+
expected: true,
180+
},
181+
}
182+
183+
for _, tt := range tests {
184+
t.Run(tt.name, func(t *testing.T) {
185+
result := matchesResourceRequirements(node, tt.req)
186+
if result != tt.expected {
187+
t.Errorf("matchesResourceRequirements() = %v, want %v", result, tt.expected)
188+
}
189+
})
190+
}
191+
}
192+
193+
func TestParseCondition(t *testing.T) {
194+
tests := []struct {
195+
name string
196+
expr string
197+
expectedOp string
198+
expectedVal string
199+
expectError bool
200+
}{
201+
{
202+
name: "greater than or equal",
203+
expr: ">=2",
204+
expectedOp: ">=",
205+
expectedVal: "2",
206+
expectError: false,
207+
},
208+
{
209+
name: "less than",
210+
expr: "<5Gi",
211+
expectedOp: "<",
212+
expectedVal: "5Gi",
213+
expectError: false,
214+
},
215+
{
216+
name: "equal with whitespace",
217+
expr: " == 4Mi ",
218+
expectedOp: "==",
219+
expectedVal: "4Mi",
220+
expectError: false,
221+
},
222+
{
223+
name: "default to equal",
224+
expr: "1",
225+
expectedOp: "==",
226+
expectedVal: "1",
227+
expectError: false,
228+
},
229+
{
230+
name: "invalid format",
231+
expr: "invalid",
232+
expectError: true,
233+
},
234+
}
235+
236+
for _, tt := range tests {
237+
t.Run(tt.name, func(t *testing.T) {
238+
result, err := parseCondition(tt.expr)
239+
if tt.expectError {
240+
if err == nil {
241+
t.Errorf("parseCondition() expected error but got none")
242+
}
243+
return
244+
}
245+
246+
if err != nil {
247+
t.Errorf("parseCondition() unexpected error: %v", err)
248+
return
249+
}
250+
251+
if result.Op != tt.expectedOp {
252+
t.Errorf("parseCondition() Op = %v, want %v", result.Op, tt.expectedOp)
253+
}
254+
255+
expectedQty := resource.MustParse(tt.expectedVal)
256+
if result.Val.Cmp(expectedQty) != 0 {
257+
t.Errorf("parseCondition() Val = %v, want %v", result.Val, expectedQty)
258+
}
259+
})
260+
}
261+
}
262+
263+
func TestCompareQuantities(t *testing.T) {
264+
tests := []struct {
265+
name string
266+
actual string
267+
expected string
268+
op string
269+
result bool
270+
}{
271+
{
272+
name: "greater than - true",
273+
actual: "5",
274+
expected: "3",
275+
op: ">",
276+
result: true,
277+
},
278+
{
279+
name: "greater than - false",
280+
actual: "2",
281+
expected: "3",
282+
op: ">",
283+
result: false,
284+
},
285+
{
286+
name: "equal - true",
287+
actual: "3Gi",
288+
expected: "3072Mi",
289+
op: "==",
290+
result: true,
291+
},
292+
{
293+
name: "equal - false",
294+
actual: "3Gi",
295+
expected: "3072",
296+
op: "==",
297+
result: false,
298+
},
299+
{
300+
name: "less than or equal - true",
301+
actual: "2",
302+
expected: "3",
303+
op: "<=",
304+
result: true,
305+
},
306+
{
307+
name: "less than or equal - also true when equal",
308+
actual: "3",
309+
expected: "3",
310+
op: "<=",
311+
result: true,
312+
},
313+
{
314+
name: "not equal - true",
315+
actual: "2",
316+
expected: "3",
317+
op: "!=",
318+
result: true,
319+
},
320+
{
321+
name: "not equal - false",
322+
actual: "3",
323+
expected: "3",
324+
op: "!=",
325+
result: false,
326+
},
327+
{
328+
name: "invalid operator",
329+
actual: "3",
330+
expected: "3",
331+
op: "invalid",
332+
result: false,
333+
},
334+
}
335+
336+
for _, tt := range tests {
337+
t.Run(tt.name, func(t *testing.T) {
338+
actualQty := resource.MustParse(tt.actual)
339+
expectedQty := resource.MustParse(tt.expected)
340+
result := compareQuantities(actualQty, expectedQty, tt.op)
341+
if result != tt.result {
342+
t.Errorf("compareQuantities() = %v, want %v", result, tt.result)
343+
}
344+
})
345+
}
346+
}

0 commit comments

Comments
 (0)