Skip to content

Commit a812eee

Browse files
authored
refactor(gen-ai api): consolidate envtest setup to package-level lifecycle (opendatahub-io#6054)
Signed-off-by: Matthew F Leader <mleader@redhat.com>
1 parent 2841c09 commit a812eee

13 files changed

+406
-243
lines changed

packages/gen-ai/bff/internal/api/aaa_mcps_handler_test.go

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,7 @@ func TestMCPListHandler(t *testing.T) {
2727
logger,
2828
)
2929

30-
ctx, cancel := context.WithCancel(context.Background())
31-
testEnvState, ctrlClient, err := k8smocks.SetupEnvTest(k8smocks.TestEnvInput{
32-
Users: k8smocks.DefaultTestUsers,
33-
Logger: logger,
34-
Ctx: ctx,
35-
Cancel: cancel,
36-
})
37-
require.NoError(t, err)
38-
defer k8smocks.TeardownEnvTest(t, testEnvState)
39-
40-
mockK8sFactory, err := k8smocks.NewMockedKubernetesClientFactory(ctrlClient, testEnvState, config.EnvConfig{
41-
AuthMethod: "user_token",
42-
}, logger)
30+
mockK8sFactory, err := k8smocks.NewTokenClientFactory(testK8sClient, testCfg, logger)
4331
require.NoError(t, err)
4432

4533
app := &App{

packages/gen-ai/bff/internal/api/aaa_models_handler_test.go

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,7 @@ import (
2121
)
2222

2323
func TestModelsAAHandler(t *testing.T) {
24-
ctx, cancel := context.WithCancel(context.Background())
25-
testEnvState, ctrlClient, err := k8smocks.SetupEnvTest(k8smocks.TestEnvInput{
26-
Users: k8smocks.DefaultTestUsers,
27-
Logger: slog.Default(),
28-
Ctx: ctx,
29-
Cancel: cancel,
30-
})
31-
require.NoError(t, err)
32-
defer k8smocks.TeardownEnvTest(t, testEnvState)
33-
34-
k8sFactory, err := k8smocks.NewTokenClientFactory(ctrlClient, testEnvState.Env.Config, slog.Default())
24+
k8sFactory, err := k8smocks.NewTokenClientFactory(testK8sClient, testCfg, slog.Default())
3525
require.NoError(t, err)
3626

3727
// Create test app with real mock infrastructure

packages/gen-ai/bff/internal/api/api_suite_test.go

Lines changed: 211 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,192 @@ import (
1414
"testing"
1515
"time"
1616

17+
kservev1alpha1 "github.com/kserve/kserve/pkg/apis/serving/v1alpha1"
18+
kservev1beta1 "github.com/kserve/kserve/pkg/apis/serving/v1beta1"
19+
lsdapi "github.com/llamastack/llama-stack-k8s-operator/api/v1alpha1"
1720
. "github.com/onsi/ginkgo/v2"
1821
. "github.com/onsi/gomega"
22+
gorchv1alpha1 "github.com/trustyai-explainability/trustyai-service-operator/api/gorch/v1alpha1"
23+
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
24+
"k8s.io/apimachinery/pkg/runtime"
25+
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
26+
"k8s.io/client-go/rest"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
"sigs.k8s.io/controller-runtime/pkg/envtest"
29+
logf "sigs.k8s.io/controller-runtime/pkg/log"
30+
"sigs.k8s.io/controller-runtime/pkg/log/zap"
31+
32+
"github.com/opendatahub-io/gen-ai/internal/cache"
1933
"github.com/opendatahub-io/gen-ai/internal/config"
34+
"github.com/opendatahub-io/gen-ai/internal/integrations/kubernetes/k8smocks"
35+
"github.com/opendatahub-io/gen-ai/internal/integrations/llamastack/lsmocks"
36+
"github.com/opendatahub-io/gen-ai/internal/integrations/maas/maasmocks"
37+
"github.com/opendatahub-io/gen-ai/internal/integrations/mcp/mcpmocks"
38+
"github.com/opendatahub-io/gen-ai/internal/repositories"
39+
"github.com/opendatahub-io/gen-ai/internal/services"
40+
)
41+
42+
// Package-level test infrastructure - initialized once, shared by all tests.
43+
// WARNING: Tests using these shared resources must NOT use t.Parallel()
44+
// as they share cluster state.
45+
var (
46+
ctx context.Context
47+
cancel context.CancelFunc
48+
testK8sClient client.Client
49+
testCfg *rest.Config
50+
testEnv *envtest.Environment
51+
testScheme *runtime.Scheme
2052
)
2153

22-
// TestAPIHandlers is the main test suite entry point
54+
// TestMain sets up envtest for ALL tests (both regular Go tests and Ginkgo tests).
55+
// This is called once before any tests run and handles cleanup after all tests complete.
56+
func TestMain(m *testing.M) {
57+
logf.SetLogger(zap.New(zap.UseDevMode(true)))
58+
59+
ctx, cancel = context.WithCancel(context.TODO())
60+
61+
testScheme = runtime.NewScheme()
62+
err := clientgoscheme.AddToScheme(testScheme)
63+
if err != nil {
64+
logf.Log.Error(err, "failed to add Kubernetes types to scheme")
65+
os.Exit(1)
66+
}
67+
68+
err = lsdapi.AddToScheme(testScheme)
69+
if err != nil {
70+
logf.Log.Error(err, "failed to add LlamaStackDistribution types to scheme")
71+
os.Exit(1)
72+
}
73+
74+
err = kservev1alpha1.AddToScheme(testScheme)
75+
if err != nil {
76+
logf.Log.Error(err, "failed to add KServe v1alpha1 types to scheme")
77+
os.Exit(1)
78+
}
79+
80+
err = kservev1beta1.AddToScheme(testScheme)
81+
if err != nil {
82+
logf.Log.Error(err, "failed to add KServe v1beta1 types to scheme")
83+
os.Exit(1)
84+
}
85+
86+
err = gorchv1alpha1.AddToScheme(testScheme)
87+
if err != nil {
88+
logf.Log.Error(err, "failed to add GuardrailsOrchestrator (gorch) types to scheme")
89+
os.Exit(1)
90+
}
91+
92+
logf.Log.Info("bootstrapping test environment")
93+
binaryDir, err := getFirstFoundEnvTestBinaryDir()
94+
if err != nil {
95+
logf.Log.Error(err, "failed to resolve envtest binary directory")
96+
os.Exit(1)
97+
}
98+
testEnv = &envtest.Environment{
99+
BinaryAssetsDirectory: binaryDir,
100+
ControlPlaneStartTimeout: 60 * time.Second,
101+
ControlPlaneStopTimeout: 60 * time.Second,
102+
CRDs: []*apiextensionsv1.CustomResourceDefinition{
103+
k8smocks.CreateLlamaStackDistributionCRD(),
104+
k8smocks.CreateGuardrailsOrchestratorCRD(),
105+
},
106+
}
107+
108+
testCfg, err = testEnv.Start()
109+
if err != nil {
110+
logf.Log.Error(err, "failed to start test environment")
111+
os.Exit(1)
112+
}
113+
if testCfg == nil {
114+
logf.Log.Error(nil, "testCfg is nil after starting test environment")
115+
os.Exit(1)
116+
}
117+
118+
testK8sClient, err = client.New(testCfg, client.Options{Scheme: testScheme})
119+
if err != nil {
120+
logf.Log.Error(err, "failed to create controller-runtime client")
121+
os.Exit(1)
122+
}
123+
if testK8sClient == nil {
124+
logf.Log.Error(nil, "testK8sClient is nil after creation")
125+
os.Exit(1)
126+
}
127+
128+
err = k8smocks.SetupMock(testK8sClient, ctx)
129+
if err != nil {
130+
logf.Log.Error(err, "failed to setup mock data")
131+
os.Exit(1)
132+
}
133+
134+
code := m.Run()
135+
136+
// Find PID before stopping (needs to happen while envtest is still running)
137+
apiServerPID := k8smocks.FindAPIServerPID()
138+
testEnvState := &k8smocks.TestEnvState{
139+
Env: testEnv,
140+
APIServerPID: apiServerPID,
141+
Ctx: ctx,
142+
Cancel: cancel,
143+
}
144+
145+
// Use proper cleanup instead of just testEnv.Stop()
146+
// This handles the Linux issue where envtest.Stop() fails to reap child processes
147+
k8smocks.CleanupTestEnvState(
148+
testEnvState,
149+
func(format string, args ...interface{}) { logf.Log.Error(nil, fmt.Sprintf(format, args...)) },
150+
func(format string, args ...interface{}) { logf.Log.Info(fmt.Sprintf(format, args...)) },
151+
)
152+
153+
os.Exit(code)
154+
}
155+
156+
// getFirstFoundEnvTestBinaryDir returns the envtest binary assets directory.
157+
// ENVTEST_ASSETS is set by "make test"; when unset (e.g. IDE or ad-hoc go test),
158+
// it locates the module root via go.mod and uses <moduleRoot>/bin/k8s/<version-dir>.
159+
// Callers must fail fast on error; run "make test" or "make envtest" to ensure assets exist.
160+
func getFirstFoundEnvTestBinaryDir() (string, error) {
161+
if envtestAssets := os.Getenv("ENVTEST_ASSETS"); envtestAssets != "" {
162+
return envtestAssets, nil
163+
}
164+
165+
cwd, err := os.Getwd()
166+
if err != nil {
167+
return "", fmt.Errorf("get working directory: %w", err)
168+
}
169+
projectRoot := cwd
170+
for {
171+
if _, err := os.Stat(filepath.Join(projectRoot, "go.mod")); err == nil {
172+
break
173+
}
174+
parent := filepath.Dir(projectRoot)
175+
if parent == projectRoot {
176+
return "", fmt.Errorf("project root (go.mod) not found when walking up from %s", cwd)
177+
}
178+
projectRoot = parent
179+
}
180+
181+
basePath := filepath.Join(projectRoot, "bin", "k8s")
182+
entries, err := os.ReadDir(basePath)
183+
if err != nil {
184+
logf.Log.Error(err, "failed to read envtest binary directory", "path", basePath)
185+
return "", fmt.Errorf("read envtest binary dir %s: %w", basePath, err)
186+
}
187+
for _, entry := range entries {
188+
if entry.IsDir() {
189+
return filepath.Join(basePath, entry.Name()), nil
190+
}
191+
}
192+
logf.Log.Error(nil, "no envtest version directory found", "path", basePath)
193+
return "", fmt.Errorf("no envtest version directory under %s (run make envtest)", basePath)
194+
}
195+
196+
// TestAPIHandlers is the main test suite entry point for Ginkgo-based HTTP tests
23197
func TestAPIHandlers(t *testing.T) {
24198
RegisterFailHandler(Fail)
25199
RunSpecs(t, "API Handlers Suite")
26200
}
27201

28-
// SharedTestContext holds common test infrastructure
202+
// SharedTestContext holds common test infrastructure for HTTP tests
29203
type SharedTestContext struct {
30204
App *App
31205
Server *httptest.Server
@@ -36,32 +210,59 @@ type SharedTestContext struct {
36210

37211
var testCtx *SharedTestContext
38212

213+
// BeforeSuite sets up HTTP test infrastructure for Ginkgo tests only.
214+
// It relies on TestMain() having already initialized envtest.
39215
var _ = BeforeSuite(func() {
40-
By("Setting up test environment")
216+
By("Setting up HTTP test infrastructure")
217+
218+
Expect(testK8sClient).NotTo(BeNil())
219+
Expect(testCfg).NotTo(BeNil())
41220

42221
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
43222

44-
// Save current working directory
45223
originalWd, err := os.Getwd()
46224
Expect(err).NotTo(HaveOccurred())
47225

48-
// Change to project root directory so OpenAPI handler can find the YAML file
226+
// OpenAPI handler needs to find YAML file in project root
49227
projectRoot := filepath.Join(originalWd, "..", "..")
50228
err = os.Chdir(projectRoot)
51229
Expect(err).NotTo(HaveOccurred())
52230

53-
// Create test app with mock configuration
54231
cfg := config.EnvConfig{
55232
Port: 8080,
56233
APIPathPrefix: "/api/v1",
57234
AuthMethod: config.AuthMethodDisabled,
58235
MockLSClient: true,
59236
}
60237

61-
app, err := NewApp(cfg, logger)
238+
k8sFactory, err := k8smocks.NewTokenClientFactory(testK8sClient, testCfg, logger)
239+
Expect(err).NotTo(HaveOccurred())
240+
241+
openAPIHandler, err := NewOpenAPIHandler(logger)
62242
Expect(err).NotTo(HaveOccurred())
63243

64-
// Restore original working directory
244+
memStore := cache.NewMemoryStore()
245+
fileUploadJobTracker := services.NewFileUploadJobTracker(memStore, logger)
246+
247+
mcpFactory := mcpmocks.NewMockedMCPClientFactory(cfg, logger)
248+
// Create app manually to avoid NewApp() starting a second envtest instance
249+
app := &App{
250+
config: cfg,
251+
logger: logger,
252+
repositories: repositories.NewRepositoriesWithMCP(mcpFactory, logger),
253+
openAPI: openAPIHandler,
254+
kubernetesClientFactory: k8sFactory,
255+
llamaStackClientFactory: lsmocks.NewMockClientFactory(),
256+
maasClientFactory: maasmocks.NewMockClientFactory(),
257+
mcpClientFactory: mcpFactory,
258+
dashboardNamespace: "opendatahub",
259+
memoryStore: memStore,
260+
rootCAs: nil,
261+
clusterDomain: "",
262+
fileUploadJobTracker: fileUploadJobTracker,
263+
testEnvState: nil,
264+
}
265+
65266
err = os.Chdir(originalWd)
66267
Expect(err).NotTo(HaveOccurred())
67268

@@ -79,15 +280,14 @@ var _ = BeforeSuite(func() {
79280
Logger: logger,
80281
}
81282

82-
By("Test environment setup complete")
283+
By("HTTP test environment setup complete")
83284
})
84285

85286
var _ = AfterSuite(func() {
86-
By("Cleaning up test environment")
287+
By("tearing down HTTP test environment")
87288
if testCtx != nil && testCtx.Server != nil {
88289
testCtx.Server.Close()
89290
}
90-
By("Test environment cleanup complete")
91291
})
92292

93293
// MakeRequest is a helper to make HTTP requests in tests

packages/gen-ai/bff/internal/api/guardrails_handler_test.go

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,10 @@ import (
2424
)
2525

2626
func TestGuardrailsStatusHandler(t *testing.T) {
27-
// Setup test environment
28-
ctx, cancel := context.WithCancel(context.Background())
29-
defer cancel()
30-
3127
logger := slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{Level: slog.LevelDebug}))
3228

33-
testEnvState, ctrlClient, err := k8smocks.SetupEnvTest(k8smocks.TestEnvInput{
34-
Users: k8smocks.DefaultTestUsers,
35-
Logger: logger,
36-
Ctx: ctx,
37-
Cancel: cancel,
38-
})
39-
require.NoError(t, err)
40-
defer k8smocks.TeardownEnvTest(t, testEnvState)
41-
4229
// Create mock factory
43-
k8sFactory, err := k8smocks.NewTokenClientFactory(ctrlClient, testEnvState.Env.Config, logger)
30+
k8sFactory, err := k8smocks.NewTokenClientFactory(testK8sClient, testCfg, logger)
4431
require.NoError(t, err)
4532

4633
// Create test app with real mock infrastructure

0 commit comments

Comments
 (0)