Skip to content

Commit b941a91

Browse files
authored
test: refactor integration test harness to support data layer testing (#2664)
* Create integration tests for datalayer metrics Signed-off-by: mohamedmahameed <mohamed.mahameed@ibm.com> * formatting Signed-off-by: mohamedmahameed <mohamed.mahameed@ibm.com> * review comments and code enhancements Signed-off-by: mohamedmahameed <mohamed.mahameed@ibm.com> * use go:embed, some code lint fixes Signed-off-by: mohamedmahameed <mohamed.mahameed@ibm.com> * use CommonTest and code cleanup Signed-off-by: mohamedmahameed <mohamed.mahameed@ibm.com> * remove unnecessary comment Signed-off-by: mohamedmahameed <mohamed.mahameed@ibm.com> --------- Signed-off-by: mohamedmahameed <mohamed.mahameed@ibm.com>
1 parent 013689e commit b941a91

9 files changed

Lines changed: 328 additions & 149 deletions

cmd/epp/runner/test_runner.go

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,32 @@ package runner
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122

2223
"k8s.io/client-go/rest"
2324
ctrl "sigs.k8s.io/controller-runtime"
2425
backendmetrics "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/backend/metrics"
2526
"sigs.k8s.io/gateway-api-inference-extension/pkg/epp/datastore"
27+
fwkdl "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/framework/interface/datalayer"
28+
fwkplugin "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/framework/interface/plugin"
2629
runserver "sigs.k8s.io/gateway-api-inference-extension/pkg/epp/server"
2730
)
2831

29-
// NewTestRunnerSetup creates a setup runner dedicated for integration test and its corresponding dataStore.
30-
func NewTestRunnerSetup(ctx context.Context, cfg *rest.Config, opts *runserver.Options, pmc backendmetrics.PodMetricsClient) (ctrl.Manager, datastore.Datastore, error) {
32+
// NewTestRunnerSetup creates a setup runner dedicated for integration tests. When mockDataSource is
33+
// non-nil, its plugin type is registered as a factory that returns the provided instance, so the
34+
// YAML config can reference it by type name and the runner wires it into the endpoint factory
35+
// automatically. Pass nil to fall back to the legacy metrics system with pmc.
36+
func NewTestRunnerSetup(ctx context.Context, cfg *rest.Config, opts *runserver.Options, pmc backendmetrics.PodMetricsClient, mockDataSource fwkdl.DataSource) (ctrl.Manager, datastore.Datastore, error) {
3137
runner := NewRunner()
3238
runner.testOverrideSkipNameValidation = true
39+
40+
if mockDataSource != nil {
41+
mockType := mockDataSource.TypedName().Type
42+
fwkplugin.Register(mockType, func(name string, _ json.RawMessage, _ fwkplugin.Handle) (fwkplugin.Plugin, error) {
43+
return mockDataSource, nil
44+
})
45+
defer delete(fwkplugin.Registry, mockType)
46+
}
47+
3348
return runner.setup(ctx, cfg, opts, pmc)
3449
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ import (
3030
"sigs.k8s.io/gateway-api-inference-extension/test/integration"
3131
)
3232

33+
// Model name constants shared across test suites.
34+
const (
35+
modelMyModel = "my-model"
36+
modelMyModelTarget = "my-model-12345"
37+
modelSQLLora = "sql-lora"
38+
modelSQLLoraTarget = "sql-lora-1fdg2"
39+
)
40+
3341
// --- Domain Request Builders ---
3442

3543
// ReqSubset creates a request sequence with Envoy Endpoint Metadata.
@@ -251,6 +259,74 @@ func ExpectGRPCStreamResp(chunks ...string) []*extProcPb.ProcessingResponse {
251259
return res
252260
}
253261

262+
// --- Shared Test Cases ---
263+
264+
// testCase defines a single integration test scenario that can be shared across test suites.
265+
type testCase struct {
266+
name string
267+
requests []*extProcPb.ProcessingRequest
268+
pods []podState
269+
wantResponses []*extProcPb.ProcessingResponse
270+
wantMetrics map[string]string
271+
waitForModel string
272+
// requiresCRDs indicates that this test case relies on specific Gateway API CRD features (like
273+
// InferenceModelRewrite) which are not available in Standalone runMode without CRD.
274+
requiresCRDs bool
275+
// wantSpans lists the span names expected to be recorded (hermetic tests only).
276+
wantSpans []string
277+
}
278+
279+
// commonTestCases returns the test cases shared between the standard and data layer test suites.
280+
// prio adjusts expected priority label values based on execution context (e.g. 0 in NoCRD mode).
281+
func commonTestCases(prio func(int) int) []testCase {
282+
return []testCase{
283+
{
284+
name: "select lower queue and kv cache",
285+
requests: integration.ReqLLM(logger, "test1", modelMyModel, modelMyModelTarget),
286+
pods: []podState{
287+
P(0, 3, 0.2),
288+
P(1, 0, 0.1), // Winner (Low Queue, Low KV)
289+
P(2, 10, 0.2),
290+
},
291+
wantResponses: ExpectRouteTo("192.168.1.2:8000", modelMyModelTarget, "test1"),
292+
wantMetrics: map[string]string{
293+
"inference_objective_request_total": cleanMetric(metricReqTotal(modelMyModel, modelMyModelTarget, prio(2))),
294+
"inference_pool_ready_pods": cleanMetric(metricReadyPods(3)),
295+
},
296+
wantSpans: []string{"gateway.request", "gateway.request_orchestration"},
297+
},
298+
{
299+
name: "select active lora, low queue",
300+
requests: integration.ReqLLM(logger, "test2", modelSQLLora, modelSQLLoraTarget),
301+
pods: []podState{
302+
P(0, 0, 0.2, "foo", "bar"),
303+
P(1, 0, 0.1, "foo", modelSQLLoraTarget), // Winner (Has LoRA)
304+
P(2, 10, 0.2, "foo", "bar"),
305+
},
306+
wantResponses: ExpectRouteTo("192.168.1.2:8000", modelSQLLoraTarget, "test2"),
307+
wantMetrics: map[string]string{
308+
"inference_objective_request_total": cleanMetric(metricReqTotal(modelSQLLora, modelSQLLoraTarget, prio(2))),
309+
},
310+
},
311+
{
312+
name: "no backend pods available",
313+
requests: integration.ReqHeaderOnly(map[string]string{"content-type": "application/json"}),
314+
pods: nil,
315+
wantResponses: ExpectReject(envoyTypePb.StatusCode_InternalServerError,
316+
"inference error: Internal - no pods available in datastore"),
317+
},
318+
{
319+
name: "request missing model field",
320+
requests: integration.ReqRaw(
321+
map[string]string{"content-type": "application/json"},
322+
`{"prompt":"hello world"}`,
323+
),
324+
wantResponses: ExpectReject(envoyTypePb.StatusCode_BadRequest,
325+
"inference error: BadRequest - model not found in request body"),
326+
},
327+
}
328+
}
329+
254330
// --- Data Structures & Metrics Helpers ---
255331

256332
type podState struct {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
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 epp
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
configPb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
24+
"github.com/google/go-cmp/cmp"
25+
"github.com/stretchr/testify/require"
26+
"google.golang.org/protobuf/testing/protocmp"
27+
28+
"sigs.k8s.io/gateway-api-inference-extension/test/integration"
29+
)
30+
31+
// TestFullDuplexStreamed_DataLayer runs integration tests through the datalayer metrics pipeline.
32+
// It mirrors a representative subset of hermetic_test.go test cases but uses the data layer
33+
// (mock DataSource) instead of the standard FakePodMetricsClient.
34+
func TestFullDuplexStreamed_DataLayer(t *testing.T) {
35+
// Datalayer tests always run in standard mode with base resources (priority=2 in InferenceObjectives).
36+
tests := commonTestCases(func(p int) int { return p })
37+
38+
for _, tc := range tests {
39+
t.Run(tc.name, func(t *testing.T) {
40+
ctx, cancel := context.WithCancel(context.Background())
41+
defer cancel()
42+
43+
h := NewTestHarness(t, ctx, WithStandardMode(), WithDataLayer())
44+
h.WithBaseResources().WithPods(tc.pods).WaitForSync(len(tc.pods), modelMyModel)
45+
if len(tc.pods) > 0 {
46+
h.WaitForReadyPodsMetric(len(tc.pods))
47+
}
48+
49+
responses, err := integration.StreamedRequest(t, h.Client, tc.requests, len(tc.wantResponses))
50+
require.NoError(t, err)
51+
52+
if diff := cmp.Diff(tc.wantResponses, responses,
53+
protocmp.Transform(),
54+
protocmp.SortRepeated(func(a, b *configPb.HeaderValueOption) bool {
55+
return a.GetHeader().GetKey() < b.GetHeader().GetKey()
56+
}),
57+
); diff != "" {
58+
t.Errorf("Response mismatch (-want +got): %v", diff)
59+
}
60+
61+
if len(tc.wantMetrics) > 0 {
62+
h.ExpectMetrics(tc.wantMetrics)
63+
}
64+
})
65+
}
66+
}

0 commit comments

Comments
 (0)