diff --git a/gen/rust/Cargo.lock b/gen/rust/Cargo.lock index 830924724b..f69def2c93 100644 --- a/gen/rust/Cargo.lock +++ b/gen/rust/Cargo.lock @@ -135,9 +135,9 @@ checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" [[package]] name = "cc" -version = "1.2.50" +version = "1.2.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f50d563227a1c37cc0a263f64eca3334388c01c5e4c4861a9def205c614383c" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" dependencies = [ "find-msvc-tools", "shlex", @@ -188,9 +188,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] name = "find-msvc-tools" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" [[package]] name = "fixedbitset" @@ -542,9 +542,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.16" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" [[package]] name = "lazy_static" @@ -763,9 +763,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "portable-atomic" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f59e70c4aef1e55797c2e8fd94a4f2a973fc972cfde0e0b05f683667b0cd39dd" +checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" [[package]] name = "ppv-lite86" @@ -788,9 +788,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" dependencies = [ "unicode-ident", ] @@ -1172,10 +1172,11 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.7" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] diff --git a/runs/Makefile b/runs/Makefile index 2dd9ff854f..5cbe39db4b 100644 --- a/runs/Makefile +++ b/runs/Makefile @@ -50,8 +50,21 @@ docker-postgres-stop: ## Stop and remove PostgreSQL container @docker stop flyte-runs-postgres || true @docker rm flyte-runs-postgres || true +##@ Testing + +# Override test to exclude API tests +.PHONY: test +test: ## Run unit tests only (excludes test/) + @echo "Running unit tests..." + @go test -v -race -cover $(shell go list ./... | grep -v '/test') + ##@ Integration Tests +.PHONY: api-test +api-test: ## Run API integration tests + @echo "Running API integration tests..." + @go test -v -race ./test/api/... + .PHONY: integration-test integration-test: build-fast build-client ## Run integration test with SQLite @echo "Running service with SQLite..." diff --git a/runs/README.md b/runs/README.md index 72a7aa28cf..0fac2440ac 100644 --- a/runs/README.md +++ b/runs/README.md @@ -121,6 +121,15 @@ go test ./... make test ``` +### Run API tests + +Pleaes ensure the service is started by following [Quick Start with SQLite](#quick-start-with-sqlite) section +before running API tests. + +```sh +make api-test +``` + ### Run integration test with client **Using SQLite:** diff --git a/runs/test/api/setup_test.go b/runs/test/api/setup_test.go new file mode 100644 index 0000000000..0bc272e284 --- /dev/null +++ b/runs/test/api/setup_test.go @@ -0,0 +1,158 @@ +package api + +import ( + "context" + "fmt" + "log" + "net/http" + "os" + "testing" + "time" + + "golang.org/x/net/http2" + "golang.org/x/net/http2/h2c" + "gorm.io/gorm" + + "github.com/flyteorg/flyte/v2/flytestdlib/database" + "github.com/flyteorg/flyte/v2/flytestdlib/logger" + "github.com/flyteorg/flyte/v2/gen/go/flyteidl2/task/taskconnect" + "github.com/flyteorg/flyte/v2/runs/migrations" + "github.com/flyteorg/flyte/v2/runs/repository" + "github.com/flyteorg/flyte/v2/runs/service" +) + +const ( + testPort = 8091 // Different port to avoid conflicts with main service + testDBFile = "/tmp/flyte-runs-test.db" // Temporary database file for tests +) + +var ( + endpoint string + testServer *http.Server + testDB *gorm.DB // Expose DB for cleanup +) + + +// TestMain sets up the test environment with SQLite database and runs service +func TestMain(m *testing.M) { + ctx := context.Background() + var exitCode int + defer func() { + os.Exit(exitCode) + }() + + // Setup: Create SQLite database + // NOTE: Using a temp file instead of :memory: to avoid GORM AutoMigrate issues + // with index creation on fresh in-memory databases + dbConfig := &database.DbConfig{ + SQLite: database.SQLiteConfig{ + File: testDBFile, + }, + } + logCfg := logger.GetConfig() + var err error + testDB, err = database.GetDB(ctx, dbConfig, logCfg) + if err != nil { + log.Fatalf("Failed to initialize database: %v", err) + } + log.Println("Database initialized") + + // Run migrations + if err := migrations.RunMigrations(testDB); err != nil { + log.Fatalf("Failed to run migrations: %v", err) + } + log.Println("Database migrations completed") + + // Create repository and services + repo := repository.NewRepository(testDB) + taskSvc := service.NewTaskService(repo) + + // Setup HTTP server + mux := http.NewServeMux() + taskPath, taskHandler := taskconnect.NewTaskServiceHandler(taskSvc) + mux.Handle(taskPath, taskHandler) + + // Add health check + mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("OK")) + }) + + endpoint = fmt.Sprintf("http://localhost:%d", testPort) + testServer = &http.Server{ + Addr: fmt.Sprintf(":%d", testPort), + Handler: h2c.NewHandler(mux, &http2.Server{}), + } + + // Start server in background + go func() { + log.Printf("Test server starting on %s", endpoint) + if err := testServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Printf("Server error: %v", err) + } + }() + + // Wait for server to be ready + if !waitForServer(endpoint, 10*time.Second) { + log.Fatal("Test server failed to start") + } + log.Println("Test server is ready") + + // Run tests + exitCode = m.Run() + + // Teardown: Stop server + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := testServer.Shutdown(shutdownCtx); err != nil { + log.Printf("Test server shutdown error: %v", err) + } + log.Println("Test server stopped") + + // Cleanup: Remove test database file + if err := os.Remove(testDBFile); err != nil && !os.IsNotExist(err) { + log.Printf("Warning: Failed to remove test database: %v", err) + } +} + +// waitForServer waits for the server to be ready +func waitForServer(url string, timeout time.Duration) bool { + client := &http.Client{Timeout: 1 * time.Second} + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + resp, err := client.Get(url + "/healthz") + if err == nil && resp.StatusCode == http.StatusOK { + resp.Body.Close() + return true + } + if resp != nil { + resp.Body.Close() + } + time.Sleep(100 * time.Millisecond) + } + return false +} + +// cleanupTestDB clears all tables in the test database +// This ensures each test starts with a clean state +func cleanupTestDB(t *testing.T) { + t.Helper() + + if testDB == nil { + t.Log("Warning: testDB is nil, skipping cleanup") + return + } + + // Delete all records from all tables + // Order matters due to foreign key constraints (if any) + tables := []string{"tasks", "task_specs", "actions"} + + for _, table := range tables { + if err := testDB.Exec(fmt.Sprintf("DELETE FROM %s", table)).Error; err != nil { + t.Logf("Warning: Failed to cleanup table %s: %v", table, err) + } + } + + t.Log("Test database cleaned up") +} diff --git a/runs/test/api/task_service_test.go b/runs/test/api/task_service_test.go new file mode 100644 index 0000000000..cdfb437a39 --- /dev/null +++ b/runs/test/api/task_service_test.go @@ -0,0 +1,75 @@ +package api + +import ( + "context" + "testing" + + "connectrpc.com/connect" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/flyteorg/flyte/v2/gen/go/flyteidl2/core" + "github.com/flyteorg/flyte/v2/gen/go/flyteidl2/task" + "github.com/flyteorg/flyte/v2/gen/go/flyteidl2/task/taskconnect" +) + +const ( + testOrg = "test-org" + testProject = "test-project" + testDomain = "test-domain" +) + +func TestDeployTask(t *testing.T) { + t.Cleanup(func() { + cleanupTestDB(t) + }) + + ctx := context.Background() + httpClient := newClient() + opts := []connect.ClientOption{} + + taskClient := taskconnect.NewTaskServiceClient(httpClient, endpoint, opts...) + + // Deploy a task + taskID := &task.TaskIdentifier{ + Org: testOrg, + Project: testProject, + Domain: testDomain, + Name: "test-task", + Version: uniqueString(), + } + + deployResp, err := taskClient.DeployTask(ctx, connect.NewRequest(&task.DeployTaskRequest{ + TaskId: taskID, + Spec: &task.TaskSpec{ + TaskTemplate: &core.TaskTemplate{ + Type: "container", + Target: &core.TaskTemplate_Container{ + Container: &core.Container{ + Image: "alpine:latest", + Args: []string{"echo", "hello"}, + }, + }, + }, + }, + })) + + require.NoError(t, err) + require.NotNil(t, deployResp) + t.Logf("Task deployed successfully: %v", taskID) + + // Get task details + getTaskDetailsResp, err := taskClient.GetTaskDetails(ctx, connect.NewRequest(&task.GetTaskDetailsRequest{ + TaskId: taskID, + })) + + require.NoError(t, err) + require.NotNil(t, getTaskDetailsResp) + details := getTaskDetailsResp.Msg.GetDetails() + assert.Equal(t, taskID.GetOrg(), details.GetTaskId().GetOrg()) + assert.Equal(t, taskID.GetProject(), details.GetTaskId().GetProject()) + assert.Equal(t, taskID.GetDomain(), details.GetTaskId().GetDomain()) + assert.Equal(t, taskID.GetName(), details.GetTaskId().GetName()) + assert.Equal(t, taskID.GetVersion(), details.GetTaskId().GetVersion()) + t.Logf("Task details retrieved successfully: %v", details) +} diff --git a/runs/test/api/utils.go b/runs/test/api/utils.go new file mode 100644 index 0000000000..c0208db93c --- /dev/null +++ b/runs/test/api/utils.go @@ -0,0 +1,25 @@ +package api + +import ( + "fmt" + "net/http" + "os" + "time" +) + +func getEnvOrDefault(key, defaultValue string) string { + if value, exists := os.LookupEnv(key); exists { + return value + } + return defaultValue +} + +func uniqueString() string { + return fmt.Sprintf("%d", time.Now().UnixNano()) +} + +func newClient() *http.Client { + return &http.Client{ + Timeout: 30 * time.Second, + } +} diff --git a/runs/tests/scripts/create_task.sh b/runs/test/scripts/create_task.sh similarity index 100% rename from runs/tests/scripts/create_task.sh rename to runs/test/scripts/create_task.sh diff --git a/runs/tests/scripts/list_tasks.sh b/runs/test/scripts/list_tasks.sh similarity index 100% rename from runs/tests/scripts/list_tasks.sh rename to runs/test/scripts/list_tasks.sh