Skip to content

Commit ceb8f26

Browse files
authored
fix: Improved test coverage for scheduler_profile.go (kubernetes-sigs/gateway-api-inference-extension#2610)
1 parent 7e61ff3 commit ceb8f26

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

pkg/epp/scheduling/scheduler_profile_test.go

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package scheduling
1818

1919
import (
2020
"context"
21+
"strings"
2122
"testing"
2223

2324
"github.com/google/go-cmp/cmp"
@@ -242,6 +243,188 @@ func (tp *testPlugin) reset() {
242243
tp.NumOfPickerCandidates = 0
243244
}
244245

246+
func TestAddPlugins(t *testing.T) {
247+
tests := []struct {
248+
name string
249+
plugins []fwkplugin.Plugin
250+
wantFilters int
251+
wantScorers int
252+
wantPicker bool
253+
wantErr bool
254+
errContains string
255+
}{
256+
{
257+
name: "add WeightedScorer that also implements Filter and Picker",
258+
plugins: []fwkplugin.Plugin{
259+
NewWeightedScorer(&testPlugin{TypeRes: "multi"}, 1.0),
260+
},
261+
wantFilters: 1,
262+
wantScorers: 1,
263+
wantPicker: true,
264+
},
265+
{
266+
name: "add plugin that only implements Filter",
267+
plugins: []fwkplugin.Plugin{
268+
&filterOnlyPlugin{typedName: fwkplugin.TypedName{Name: "filter-only"}},
269+
},
270+
wantFilters: 1,
271+
wantScorers: 0,
272+
wantPicker: false,
273+
},
274+
{
275+
name: "error when adding Scorer without weight",
276+
plugins: []fwkplugin.Plugin{
277+
&testPlugin{TypeRes: "bare-scorer"},
278+
},
279+
wantErr: true,
280+
errContains: "without a weight",
281+
},
282+
{
283+
name: "error when adding duplicate picker",
284+
plugins: []fwkplugin.Plugin{
285+
NewWeightedScorer(&testPlugin{TypeRes: "picker1"}, 1.0),
286+
NewWeightedScorer(&testPlugin{TypeRes: "picker2"}, 1.0),
287+
},
288+
wantErr: true,
289+
errContains: "already have a registered picker",
290+
},
291+
}
292+
293+
for _, test := range tests {
294+
t.Run(test.name, func(t *testing.T) {
295+
profile := NewSchedulerProfile()
296+
err := profile.AddPlugins(test.plugins...)
297+
298+
if test.wantErr {
299+
if err == nil {
300+
t.Fatalf("expected error but got nil")
301+
}
302+
if test.errContains != "" && !strings.Contains(err.Error(), test.errContains) {
303+
t.Errorf("error %q does not contain %q", err.Error(), test.errContains)
304+
}
305+
return
306+
}
307+
if err != nil {
308+
t.Fatalf("unexpected error: %v", err)
309+
}
310+
if len(profile.filters) != test.wantFilters {
311+
t.Errorf("got %d filters, want %d", len(profile.filters), test.wantFilters)
312+
}
313+
if len(profile.scorers) != test.wantScorers {
314+
t.Errorf("got %d scorers, want %d", len(profile.scorers), test.wantScorers)
315+
}
316+
if test.wantPicker && profile.picker == nil {
317+
t.Errorf("expected picker to be set")
318+
}
319+
if !test.wantPicker && profile.picker != nil {
320+
t.Errorf("expected picker to be nil")
321+
}
322+
})
323+
}
324+
}
325+
326+
func TestSchedulerProfileString(t *testing.T) {
327+
tp1 := &testPlugin{TypeRes: "test1"}
328+
tp2 := &testPlugin{TypeRes: "test2"}
329+
pickerPlugin := &testPlugin{TypeRes: "picker"}
330+
331+
profile := NewSchedulerProfile().
332+
WithFilters(tp1, tp2).
333+
WithScorers(NewWeightedScorer(tp1, 1.5), NewWeightedScorer(tp2, 2.0)).
334+
WithPicker(pickerPlugin)
335+
336+
result := profile.String()
337+
338+
// Verify the string contains filter, scorer, and picker info
339+
if !strings.Contains(result, "Filters:") {
340+
t.Errorf("String() missing Filters section: %s", result)
341+
}
342+
if !strings.Contains(result, "Scorers:") {
343+
t.Errorf("String() missing Scorers section: %s", result)
344+
}
345+
if !strings.Contains(result, "Picker:") {
346+
t.Errorf("String() missing Picker section: %s", result)
347+
}
348+
}
349+
350+
func TestEnforceScoreRange(t *testing.T) {
351+
tests := []struct {
352+
name string
353+
score float64
354+
want float64
355+
}{
356+
{name: "negative score clamped to 0", score: -0.5, want: 0},
357+
{name: "score above 1 clamped to 1", score: 1.5, want: 1},
358+
{name: "score at 0 stays 0", score: 0, want: 0},
359+
{name: "score at 1 stays 1", score: 1, want: 1},
360+
{name: "score in range stays unchanged", score: 0.5, want: 0.5},
361+
}
362+
for _, test := range tests {
363+
t.Run(test.name, func(t *testing.T) {
364+
got := enforceScoreRange(test.score)
365+
if got != test.want {
366+
t.Errorf("enforceScoreRange(%v) = %v, want %v", test.score, got, test.want)
367+
}
368+
})
369+
}
370+
}
371+
372+
func TestRunWithOutOfRangeScores(t *testing.T) {
373+
// Scorer that returns negative score
374+
negativeScorer := &testPlugin{
375+
TypeRes: "negative",
376+
ScoreRes: -0.5,
377+
FilterRes: []k8stypes.NamespacedName{{Name: "pod1"}},
378+
}
379+
// Scorer that returns score > 1
380+
overScorer := &testPlugin{
381+
TypeRes: "over",
382+
ScoreRes: 1.5,
383+
FilterRes: []k8stypes.NamespacedName{{Name: "pod1"}},
384+
}
385+
pickerPlugin := &testPlugin{
386+
TypeRes: "picker",
387+
PickRes: k8stypes.NamespacedName{Name: "pod1"},
388+
}
389+
390+
profile := NewSchedulerProfile().
391+
WithFilters().
392+
WithScorers(NewWeightedScorer(negativeScorer, 1), NewWeightedScorer(overScorer, 1)).
393+
WithPicker(pickerPlugin)
394+
395+
input := []fwksched.Endpoint{
396+
fwksched.NewEndpoint(&fwkdl.EndpointMetadata{NamespacedName: k8stypes.NamespacedName{Name: "pod1"}}, nil, nil),
397+
}
398+
399+
request := &fwksched.LLMRequest{
400+
TargetModel: "test-model",
401+
RequestId: uuid.NewString(),
402+
}
403+
404+
_, err := profile.Run(context.Background(), request, fwksched.NewCycleState(), input)
405+
if err != nil {
406+
t.Fatalf("unexpected error: %v", err)
407+
}
408+
409+
// negative score clamped to 0, over score clamped to 1: total = 0*1 + 1*1 = 1.0
410+
if pickerPlugin.WinnerEndpointScore != 1.0 {
411+
t.Errorf("expected winner score 1.0, got %v", pickerPlugin.WinnerEndpointScore)
412+
}
413+
}
414+
415+
// filterOnlyPlugin implements only the Filter interface (not Scorer or Picker).
416+
type filterOnlyPlugin struct {
417+
typedName fwkplugin.TypedName
418+
}
419+
420+
func (p *filterOnlyPlugin) TypedName() fwkplugin.TypedName {
421+
return p.typedName
422+
}
423+
424+
func (p *filterOnlyPlugin) Filter(_ context.Context, _ *fwksched.CycleState, _ *fwksched.LLMRequest, endpoints []fwksched.Endpoint) []fwksched.Endpoint {
425+
return endpoints
426+
}
427+
245428
func findEndpoints(endpoints []fwksched.Endpoint, names ...k8stypes.NamespacedName) []fwksched.Endpoint {
246429
res := []fwksched.Endpoint{}
247430
for _, endpoint := range endpoints {

0 commit comments

Comments
 (0)