Skip to content

Commit 1275430

Browse files
authored
Add unit tests for session affinity scorer (#222)
* Add unit tests for session affinity scorer * fix CI lint issues and formatting * fix: corrected unit tests for session affinity scorer * fix: removed the redundant nil response testing from PostResponse part of unit test
1 parent 384916e commit 1275430

File tree

1 file changed

+145
-0
lines changed

1 file changed

+145
-0
lines changed
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package scorer_test
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"testing"
7+
8+
"github.com/google/go-cmp/cmp"
9+
k8stypes "k8s.io/apimachinery/pkg/types"
10+
11+
"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend"
12+
backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
13+
"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/requestcontrol"
14+
"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/scheduling/types"
15+
16+
"github.com/llm-d/llm-d-inference-scheduler/pkg/plugins/scorer"
17+
)
18+
19+
func TestSessionAffinity_Score(t *testing.T) {
20+
podA := &types.PodMetrics{
21+
Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod-a"}},
22+
MetricsState: &backendmetrics.MetricsState{},
23+
}
24+
podB := &types.PodMetrics{
25+
Pod: &backend.Pod{NamespacedName: k8stypes.NamespacedName{Name: "pod-b"}},
26+
MetricsState: &backendmetrics.MetricsState{},
27+
}
28+
29+
inputPods := []types.Pod{podA, podB}
30+
31+
// valid session token for podB
32+
validSessionTokenForPodB := base64.StdEncoding.EncodeToString([]byte(podB.GetPod().NamespacedName.String()))
33+
34+
sessionAffinityScorer := scorer.NewSessionAffinity()
35+
36+
tests := []struct {
37+
name string
38+
req *types.LLMRequest
39+
input []types.Pod
40+
wantScores map[types.Pod]float64
41+
}{
42+
{
43+
name: "selects correct pod : podB",
44+
req: &types.LLMRequest{
45+
Headers: map[string]string{"x-session-token": validSessionTokenForPodB},
46+
},
47+
input: inputPods,
48+
wantScores: map[types.Pod]float64{
49+
podA: 0.0,
50+
podB: 1.0,
51+
},
52+
},
53+
{
54+
name: "no session token",
55+
req: &types.LLMRequest{
56+
Headers: map[string]string{},
57+
},
58+
// both pods get score 0.0
59+
input: inputPods,
60+
wantScores: map[types.Pod]float64{
61+
podA: 0.0,
62+
podB: 0.0,
63+
},
64+
},
65+
{
66+
name: "invalid session token",
67+
req: &types.LLMRequest{
68+
Headers: map[string]string{"x-session-token": "garbage-token"},
69+
},
70+
// expect same behavior as no session token
71+
input: inputPods,
72+
wantScores: map[types.Pod]float64{
73+
podA: 0.0,
74+
podB: 0.0,
75+
},
76+
},
77+
{
78+
name: "no pods available",
79+
req: &types.LLMRequest{},
80+
input: []types.Pod{},
81+
// returns empty score map
82+
wantScores: map[types.Pod]float64{},
83+
},
84+
}
85+
86+
for _, test := range tests {
87+
t.Run(test.name, func(t *testing.T) {
88+
gotScores := sessionAffinityScorer.Score(context.Background(), nil, test.req, test.input)
89+
90+
if diff := cmp.Diff(test.wantScores, gotScores); diff != "" {
91+
t.Errorf("Unexpected output (-want +got): %v", diff)
92+
}
93+
})
94+
}
95+
}
96+
97+
func TestSessionAffinity_PostResponse(t *testing.T) {
98+
99+
targetPod := &backend.Pod{
100+
NamespacedName: k8stypes.NamespacedName{Name: "pod1"},
101+
Address: "1.2.3.4",
102+
}
103+
104+
// expected token to be set in response header
105+
wantToken := base64.StdEncoding.EncodeToString([]byte(targetPod.NamespacedName.String()))
106+
107+
tests := []struct {
108+
name string
109+
initialResponse *requestcontrol.Response
110+
targetPod *backend.Pod
111+
wantHeaders map[string]string
112+
}{
113+
{
114+
name: "standard case with existing headers map",
115+
initialResponse: &requestcontrol.Response{RequestId: "req-1", Headers: make(map[string]string)},
116+
targetPod: targetPod,
117+
wantHeaders: map[string]string{"x-session-token": wantToken},
118+
},
119+
{
120+
name: "response with nil headers map",
121+
initialResponse: &requestcontrol.Response{RequestId: "req-2", Headers: nil},
122+
targetPod: targetPod,
123+
wantHeaders: map[string]string{"x-session-token": wantToken},
124+
},
125+
{
126+
name: "nil targetPod should do nothing",
127+
initialResponse: &requestcontrol.Response{RequestId: "req-3", Headers: make(map[string]string)},
128+
targetPod: nil,
129+
wantHeaders: map[string]string{},
130+
},
131+
}
132+
133+
s := scorer.NewSessionAffinity()
134+
ctx := context.Background()
135+
136+
for _, test := range tests {
137+
t.Run(test.name, func(t *testing.T) {
138+
s.PostResponse(ctx, nil, test.initialResponse, test.targetPod)
139+
140+
if diff := cmp.Diff(test.wantHeaders, test.initialResponse.Headers); diff != "" {
141+
t.Errorf("Unexpected output (-want +got): %v", diff)
142+
}
143+
})
144+
}
145+
}

0 commit comments

Comments
 (0)