Skip to content

Commit fefed30

Browse files
committed
add initial integration test for body-based routing extension
1 parent b40de04 commit fefed30

File tree

1 file changed

+210
-0
lines changed

1 file changed

+210
-0
lines changed

test/integration/bbr/hermetic_test.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
/*
2+
Copyright 2025 The Kubernetes 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 bbr contains integration tests for the body-based routing extension.
18+
package bbr
19+
20+
import (
21+
"context"
22+
"encoding/json"
23+
"fmt"
24+
"testing"
25+
"time"
26+
27+
configPb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
28+
extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
29+
"github.com/go-logr/logr"
30+
"github.com/google/go-cmp/cmp"
31+
"google.golang.org/grpc"
32+
"google.golang.org/grpc/credentials/insecure"
33+
"google.golang.org/protobuf/testing/protocmp"
34+
"k8s.io/client-go/rest"
35+
ctrl "sigs.k8s.io/controller-runtime"
36+
runserver "sigs.k8s.io/gateway-api-inference-extension/pkg/body-based-routing/server"
37+
logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
38+
)
39+
40+
const port = runserver.DefaultGrpcPort
41+
42+
var (
43+
serverRunner *runserver.ExtProcServerRunner
44+
logger = logutil.NewTestLogger().V(logutil.VERBOSE)
45+
)
46+
47+
func TestBodyBasedRouting(t *testing.T) {
48+
tests := []struct {
49+
name string
50+
req *extProcPb.ProcessingRequest
51+
wantHeaders []*configPb.HeaderValueOption
52+
wantBody []byte
53+
wantErr bool
54+
immediateResponse *extProcPb.ImmediateResponse
55+
}{
56+
{
57+
name: "success adding model parameter to header",
58+
req: generateRequest(logger, "llama"),
59+
wantHeaders: []*configPb.HeaderValueOption{
60+
{
61+
Header: &configPb.HeaderValue{
62+
Key: "X-Gateway-Model-Name",
63+
RawValue: []byte("llama"),
64+
},
65+
},
66+
{
67+
Header: &configPb.HeaderValue{
68+
Key: "Content-Length",
69+
RawValue: []byte("76"),
70+
},
71+
},
72+
},
73+
wantBody: []byte("{\"max_tokens\":100,\"model\":\"llama\",\"prompt\":\"test1\",\"temperature\":0}"),
74+
wantErr: false,
75+
},
76+
}
77+
78+
// Set up extproc server runner with test environment config
79+
cleanup := BeforeSuit(t)
80+
defer cleanup()
81+
82+
for _, test := range tests {
83+
t.Run(test.name, func(t *testing.T) {
84+
client, cleanup := setUpHermeticServer()
85+
t.Cleanup(cleanup)
86+
87+
want := &extProcPb.ProcessingResponse{
88+
Response: &extProcPb.ProcessingResponse_RequestBody{
89+
RequestBody: &extProcPb.BodyResponse{
90+
Response: &extProcPb.CommonResponse{
91+
HeaderMutation: &extProcPb.HeaderMutation{
92+
SetHeaders: test.wantHeaders,
93+
},
94+
BodyMutation: &extProcPb.BodyMutation{
95+
Mutation: &extProcPb.BodyMutation_Body{
96+
Body: test.wantBody,
97+
},
98+
},
99+
},
100+
},
101+
},
102+
}
103+
104+
res, err := sendRequest(t, client, test.req)
105+
if err != nil && !test.wantErr {
106+
t.Errorf("Unexpected error, got: %v, want error: %v", err, test.wantErr)
107+
}
108+
if test.immediateResponse != nil {
109+
want = &extProcPb.ProcessingResponse{
110+
Response: &extProcPb.ProcessingResponse_ImmediateResponse{
111+
ImmediateResponse: test.immediateResponse,
112+
},
113+
}
114+
}
115+
if diff := cmp.Diff(want, res, protocmp.Transform()); diff != "" {
116+
t.Errorf("Unexpected response, (-want +got): %v", diff)
117+
}
118+
})
119+
}
120+
}
121+
122+
func generateRequest(logger logr.Logger, model string) *extProcPb.ProcessingRequest {
123+
j := map[string]interface{}{
124+
"prompt": "test1",
125+
"max_tokens": 100,
126+
"temperature": 0,
127+
}
128+
if model != "" {
129+
j["model"] = model
130+
}
131+
132+
llmReq, err := json.Marshal(j)
133+
if err != nil {
134+
logutil.Fatal(logger, err, "Failed to unmarshal LLM request")
135+
}
136+
req := &extProcPb.ProcessingRequest{
137+
Request: &extProcPb.ProcessingRequest_RequestBody{
138+
RequestBody: &extProcPb.HttpBody{Body: llmReq},
139+
},
140+
}
141+
return req
142+
}
143+
144+
func setUpHermeticServer() (client extProcPb.ExternalProcessor_ProcessClient, cleanup func()) {
145+
serverCtx, stopServer := context.WithCancel(context.Background())
146+
147+
go func() {
148+
if err := serverRunner.AsRunnable(logger.WithName("ext-proc")).Start(serverCtx); err != nil {
149+
logutil.Fatal(logger, err, "Failed to start ext-proc server")
150+
}
151+
}()
152+
153+
address := fmt.Sprintf("localhost:%v", port)
154+
// Create a grpc connection
155+
conn, err := grpc.NewClient(address, grpc.WithTransportCredentials(insecure.NewCredentials()))
156+
if err != nil {
157+
logutil.Fatal(logger, err, "Failed to connect", "address", address)
158+
}
159+
160+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
161+
client, err = extProcPb.NewExternalProcessorClient(conn).Process(ctx)
162+
if err != nil {
163+
logutil.Fatal(logger, err, "Failed to create client")
164+
}
165+
return client, func() {
166+
cancel()
167+
conn.Close()
168+
stopServer()
169+
170+
// wait a little until the goroutines actually exit
171+
time.Sleep(5 * time.Second)
172+
}
173+
}
174+
175+
// Sets up a test environment and returns the runner struct
176+
func BeforeSuit(t *testing.T) func() {
177+
// Init runtime.
178+
ctrl.SetLogger(logger)
179+
180+
mgr, err := ctrl.NewManager(&rest.Config{}, ctrl.Options{})
181+
if err != nil {
182+
logutil.Fatal(logger, err, "Failed to create manager")
183+
}
184+
185+
serverRunner = runserver.NewDefaultExtProcServerRunner()
186+
go func() {
187+
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
188+
logutil.Fatal(logger, err, "Failed to start manager")
189+
}
190+
}()
191+
logger.Info("Setting up hermetic ExtProc server")
192+
193+
return func() {}
194+
}
195+
196+
func sendRequest(t *testing.T, client extProcPb.ExternalProcessor_ProcessClient, req *extProcPb.ProcessingRequest) (*extProcPb.ProcessingResponse, error) {
197+
t.Logf("Sending request: %v", req)
198+
if err := client.Send(req); err != nil {
199+
t.Logf("Failed to send request %+v: %v", req, err)
200+
return nil, err
201+
}
202+
203+
res, err := client.Recv()
204+
if err != nil {
205+
t.Logf("Failed to receive: %v", err)
206+
return nil, err
207+
}
208+
t.Logf("Received request %+v", res)
209+
return res, err
210+
}

0 commit comments

Comments
 (0)