Skip to content

Commit 3b01d7f

Browse files
committed
test/bbr: add isolated integration test harness
Introduces the `BBRHarness` struct to manage isolated BBR server instances, laying the groundwork for parallel integration testing. Features: - **Isolation:** Spawns a dedicated server per test on a random loopback port. - **Helpers:** Provides BBR-specific assertion helpers (e.g., `ExpectBBRHeader`) to reduce protobuf boilerplate. - **Safety:** Ensures proper server shutdown and resource cleanup via `t.Cleanup`.
1 parent bf2882e commit 3b01d7f

File tree

2 files changed

+224
-0
lines changed

2 files changed

+224
-0
lines changed
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
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
18+
19+
import (
20+
"context"
21+
"fmt"
22+
"strings"
23+
"testing"
24+
25+
extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
26+
"github.com/stretchr/testify/require"
27+
"google.golang.org/grpc"
28+
"google.golang.org/grpc/credentials/insecure"
29+
30+
runserver "sigs.k8s.io/gateway-api-inference-extension/pkg/bbr/server"
31+
logutil "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/util/logging"
32+
"sigs.k8s.io/gateway-api-inference-extension/test/integration"
33+
)
34+
35+
var logger = logutil.NewTestLogger().V(logutil.VERBOSE)
36+
37+
// BBRHarness encapsulates the environment for a single isolated BBR test run.
38+
type BBRHarness struct {
39+
t *testing.T
40+
ctx context.Context
41+
Client extProcPb.ExternalProcessor_ProcessClient
42+
43+
// Internal handles for cleanup
44+
server *runserver.ExtProcServerRunner
45+
grpcConn *grpc.ClientConn
46+
}
47+
48+
// NewBBRHarness boots up an isolated BBR server on a random port.
49+
// streaming: determines if the BBR server runs in streaming mode or unary/buffered mode.
50+
func NewBBRHarness(t *testing.T, ctx context.Context, streaming bool) *BBRHarness {
51+
t.Helper()
52+
53+
// 1. Allocate Free Port
54+
tcpAddr, err := integration.GetFreePort()
55+
require.NoError(t, err, "failed to acquire free port for BBR server")
56+
port := tcpAddr.Port
57+
58+
// 2. Configure BBR Server
59+
// BBR is simpler than EPP; it doesn't need a K8s Manager.
60+
runner := runserver.NewDefaultExtProcServerRunner(port, false)
61+
runner.SecureServing = false
62+
runner.Streaming = streaming
63+
64+
// 3. Start Server in Background
65+
serverCtx, serverCancel := context.WithCancel(ctx)
66+
go func() {
67+
logger.Info("Starting BBR server", "port", port, "streaming", streaming)
68+
if err := runner.AsRunnable(logger.WithName("bbr-server")).Start(serverCtx); err != nil {
69+
// Context cancellation is expected during teardown.
70+
if !strings.Contains(err.Error(), "context canceled") {
71+
logger.Error(err, "BBR server stopped unexpectedly")
72+
}
73+
}
74+
}()
75+
76+
// 4. Connect Client
77+
// Blocking dial ensures the server is reachable before the test logic begins.
78+
addr := fmt.Sprintf("127.0.0.1:%d", port)
79+
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
80+
require.NoError(t, err, "failed to create grpc connection to BBR server")
81+
82+
extProcClient, err := extProcPb.NewExternalProcessorClient(conn).Process(ctx)
83+
require.NoError(t, err, "failed to initialize ext_proc stream client")
84+
85+
h := &BBRHarness{
86+
t: t,
87+
ctx: ctx,
88+
Client: extProcClient,
89+
server: runner,
90+
grpcConn: conn,
91+
}
92+
93+
// 5. Register Cleanup
94+
t.Cleanup(func() {
95+
logger.Info("Tearing down BBR server", "port", port)
96+
serverCancel()
97+
if err := h.grpcConn.Close(); err != nil {
98+
t.Logf("Warning: failed to close grpc connection: %v", err)
99+
}
100+
})
101+
102+
return h
103+
}

test/integration/bbr/util_test.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
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
18+
19+
import (
20+
"encoding/json"
21+
22+
envoyCorev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
23+
extProcPb "github.com/envoyproxy/go-control-plane/envoy/service/ext_proc/v3"
24+
)
25+
26+
// --- Response Expectations (Streaming) ---
27+
28+
// ExpectBBRHeader asserts that BBR set the specific model header and cleared the route cache.
29+
func ExpectBBRHeader(modelName string) *extProcPb.ProcessingResponse {
30+
return &extProcPb.ProcessingResponse{
31+
Response: &extProcPb.ProcessingResponse_RequestHeaders{
32+
RequestHeaders: &extProcPb.HeadersResponse{
33+
Response: &extProcPb.CommonResponse{
34+
ClearRouteCache: true,
35+
HeaderMutation: &extProcPb.HeaderMutation{
36+
SetHeaders: []*envoyCorev3.HeaderValueOption{
37+
{
38+
Header: &envoyCorev3.HeaderValue{
39+
Key: "X-Gateway-Model-Name",
40+
RawValue: []byte(modelName),
41+
},
42+
},
43+
},
44+
},
45+
},
46+
},
47+
},
48+
}
49+
}
50+
51+
// ExpectBBRBodyPassThrough asserts that BBR reconstructs and passes the body through.
52+
// BBR buffers the body to inspect it, then sends it downstream as a single chunk (usually).
53+
func ExpectBBRBodyPassThrough(prompt, model string) *extProcPb.ProcessingResponse {
54+
j := map[string]any{
55+
"max_tokens": 100, "prompt": prompt, "temperature": 0,
56+
}
57+
if model != "" {
58+
j["model"] = model
59+
}
60+
b, _ := json.Marshal(j)
61+
62+
return &extProcPb.ProcessingResponse{
63+
Response: &extProcPb.ProcessingResponse_RequestBody{
64+
RequestBody: &extProcPb.BodyResponse{
65+
Response: &extProcPb.CommonResponse{
66+
BodyMutation: &extProcPb.BodyMutation{
67+
Mutation: &extProcPb.BodyMutation_StreamedResponse{
68+
StreamedResponse: &extProcPb.StreamedBodyResponse{
69+
Body: b,
70+
EndOfStream: true,
71+
},
72+
},
73+
},
74+
},
75+
},
76+
},
77+
}
78+
}
79+
80+
// ExpectBBRNoOpHeader asserts that BBR did nothing to the headers (e.g., when no model is found).
81+
func ExpectBBRNoOpHeader() *extProcPb.ProcessingResponse {
82+
return &extProcPb.ProcessingResponse{
83+
Response: &extProcPb.ProcessingResponse_RequestHeaders{
84+
RequestHeaders: &extProcPb.HeadersResponse{},
85+
},
86+
}
87+
}
88+
89+
// --- Response Expectations (Unary) ---
90+
91+
// ExpectBBRUnaryResponse creates expected response for unary tests where the body is mutated directly.
92+
func ExpectBBRUnaryResponse(modelName string) *extProcPb.ProcessingResponse {
93+
resp := &extProcPb.ProcessingResponse{}
94+
95+
// If modelName is present, we expect header mutations.
96+
if modelName != "" {
97+
resp.Response = &extProcPb.ProcessingResponse_RequestBody{
98+
RequestBody: &extProcPb.BodyResponse{
99+
Response: &extProcPb.CommonResponse{
100+
ClearRouteCache: true,
101+
HeaderMutation: &extProcPb.HeaderMutation{
102+
SetHeaders: []*envoyCorev3.HeaderValueOption{
103+
{
104+
Header: &envoyCorev3.HeaderValue{
105+
Key: "X-Gateway-Model-Name",
106+
RawValue: []byte(modelName),
107+
},
108+
},
109+
},
110+
},
111+
},
112+
},
113+
}
114+
} else {
115+
// Otherwise, expect a No-Op on the body.
116+
resp.Response = &extProcPb.ProcessingResponse_RequestBody{
117+
RequestBody: &extProcPb.BodyResponse{},
118+
}
119+
}
120+
return resp
121+
}

0 commit comments

Comments
 (0)