Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
SDK Tests / Python SDK Tests (sandbox) (push) Waiting to run
SDK Tests / CLI Quality (push) Waiting to run
SDK Tests / CLI Tests (push) Waiting to run
SDK Tests / Python SDK Quality (code-interpreter) (push) Waiting to run
SDK Tests / Python SDK Quality (sandbox) (push) Waiting to run
SDK Tests / Python SDK Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Waiting to run
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Waiting to run
SDK Tests / Go SDK Quality And Tests (push) Waiting to run
Deploy Docs Pages / build (push) Has been cancelled
Deploy Docs Pages / deploy (push) Has been cancelled
Real E2E Tests / JavaScript E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Python E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Java E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / C# E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Go E2E (docker bridge) (push) Has been cancelled
Real E2E Tests / Real E2E CI (push) Has been cancelled
SDK Tests / SDK CI (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

430 lines
11 KiB
Go

// Copyright 2025 Alibaba Group Holding Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/config"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/types"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/utils"
api "github.com/alibaba/OpenSandbox/sandbox-k8s/pkg/task-executor"
)
// MockTaskManager implements manager.TaskManager for testing
type MockTaskManager struct {
tasks map[string]*types.Task
err error
}
func NewMockTaskManager() *MockTaskManager {
return &MockTaskManager{
tasks: make(map[string]*types.Task),
}
}
func (m *MockTaskManager) Create(ctx context.Context, task *types.Task) (*types.Task, error) {
if m.err != nil {
return nil, m.err
}
m.tasks[task.Name] = task
return task, nil
}
func (m *MockTaskManager) Sync(ctx context.Context, desired []*types.Task) ([]*types.Task, error) {
if m.err != nil {
return nil, m.err
}
m.tasks = make(map[string]*types.Task)
var result []*types.Task
for _, t := range desired {
m.tasks[t.Name] = t
result = append(result, t)
}
return result, nil
}
func (m *MockTaskManager) Get(ctx context.Context, id string) (*types.Task, error) {
if m.err != nil {
return nil, m.err
}
if t, ok := m.tasks[id]; ok {
return t, nil
}
return nil, fmt.Errorf("not found")
}
func (m *MockTaskManager) List(ctx context.Context) ([]*types.Task, error) {
if m.err != nil {
return nil, m.err
}
var list []*types.Task
for _, t := range m.tasks {
list = append(list, t)
}
return list, nil
}
func (m *MockTaskManager) Delete(ctx context.Context, id string) error {
if m.err != nil {
return m.err
}
delete(m.tasks, id)
return nil
}
func (m *MockTaskManager) Start(ctx context.Context) {}
func (m *MockTaskManager) Stop() {}
func TestHandler_Health(t *testing.T) {
cfg := &config.Config{}
h := NewHandler(NewMockTaskManager(), cfg)
req := httptest.NewRequest("GET", "/health", nil)
w := httptest.NewRecorder()
h.Health(w, req)
if w.Code != http.StatusOK {
t.Errorf("Health returned status %d", w.Code)
}
}
func TestHandler_CreateTask(t *testing.T) {
mgr := NewMockTaskManager()
cfg := &config.Config{}
h := NewHandler(mgr, cfg)
task := api.Task{
Name: "test-task",
Process: &api.Process{
Command: []string{"echo"},
},
}
body, _ := json.Marshal(task)
req := httptest.NewRequest("POST", "/tasks", bytes.NewReader(body))
w := httptest.NewRecorder()
h.CreateTask(w, req)
if w.Code != http.StatusCreated {
t.Errorf("CreateTask returned status %d", w.Code)
}
if _, ok := mgr.tasks["test-task"]; !ok {
t.Error("Task was not created in manager")
}
}
func TestHandler_GetTask(t *testing.T) {
mgr := NewMockTaskManager()
mgr.tasks["test-task"] = &types.Task{Name: "test-task"}
cfg := &config.Config{}
h := NewHandler(mgr, cfg)
router := NewRouter(h)
req := httptest.NewRequest("GET", "/tasks/test-task", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("GetTask returned status %d", w.Code)
}
var resp api.Task
json.NewDecoder(w.Body).Decode(&resp)
if resp.Name != "test-task" {
t.Errorf("GetTask returned name %s", resp.Name)
}
}
func TestHandler_DeleteTask(t *testing.T) {
mgr := NewMockTaskManager()
mgr.tasks["test-task"] = &types.Task{Name: "test-task"}
cfg := &config.Config{}
h := NewHandler(mgr, cfg)
router := NewRouter(h)
req := httptest.NewRequest("DELETE", "/tasks/test-task", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != http.StatusNoContent {
t.Errorf("DeleteTask returned status %d", w.Code)
}
if _, ok := mgr.tasks["test-task"]; ok {
t.Error("Task was not deleted from manager")
}
}
func TestHandler_ListTasks(t *testing.T) {
mgr := NewMockTaskManager()
mgr.tasks["task-1"] = &types.Task{Name: "task-1"}
mgr.tasks["task-2"] = &types.Task{Name: "task-2"}
cfg := &config.Config{}
h := NewHandler(mgr, cfg)
req := httptest.NewRequest("GET", "/getTasks", nil)
w := httptest.NewRecorder()
h.ListTasks(w, req)
if w.Code != http.StatusOK {
t.Errorf("ListTasks returned status %d", w.Code)
}
var resp []api.Task
json.NewDecoder(w.Body).Decode(&resp)
if len(resp) != 2 {
t.Errorf("ListTasks returned %d tasks, want 2", len(resp))
}
}
func TestHandler_SyncTasks(t *testing.T) {
mgr := NewMockTaskManager()
cfg := &config.Config{}
h := NewHandler(mgr, cfg)
tasks := []api.Task{
{Name: "task-1", Process: &api.Process{}},
}
body, _ := json.Marshal(tasks)
req := httptest.NewRequest("POST", "/setTasks", bytes.NewReader(body))
w := httptest.NewRecorder()
h.SyncTasks(w, req)
if w.Code != http.StatusOK {
t.Errorf("SyncTasks returned status %d", w.Code)
}
if _, ok := mgr.tasks["task-1"]; !ok {
t.Error("Task was not synced to manager")
}
}
func TestHandler_Errors(t *testing.T) {
mgr := NewMockTaskManager()
mgr.err = errors.New("mock error")
cfg := &config.Config{}
h := NewHandler(mgr, cfg)
// Create fail
task := api.Task{Name: "fail"}
body, _ := json.Marshal(task)
req := httptest.NewRequest("POST", "/tasks", bytes.NewReader(body))
w := httptest.NewRecorder()
h.CreateTask(w, req)
if w.Code != http.StatusInternalServerError {
t.Errorf("CreateTask should fail with 500, got %d", w.Code)
}
}
func TestConvertInternalToAPITask(t *testing.T) {
now := time.Now()
t.Run("Process Task", func(t *testing.T) {
task := &types.Task{
Name: "proc-task",
Process: &api.Process{Command: []string{"ls"}},
Status: types.Status{
State: types.TaskStateSucceeded,
SubStatuses: []types.SubStatus{
{
ExitCode: 0,
Reason: "Completed",
FinishedAt: &now,
},
},
},
}
apiTask := convertInternalToAPITask(task)
assert.NotNil(t, apiTask.ProcessStatus)
assert.NotNil(t, apiTask.ProcessStatus.Terminated)
assert.Equal(t, int32(0), apiTask.ProcessStatus.Terminated.ExitCode)
assert.Nil(t, apiTask.PodStatus)
})
t.Run("Pod Task - Partially Ready", func(t *testing.T) {
task := &types.Task{
Name: "pod-task-partial",
PodTemplateSpec: &corev1.PodTemplateSpec{},
Status: types.Status{
State: types.TaskStateRunning,
SubStatuses: []types.SubStatus{
{
Name: "c1",
StartedAt: &now,
},
{
Name: "c2",
Reason: "Pending",
},
},
},
}
apiTask := convertInternalToAPITask(task)
assert.NotNil(t, apiTask.PodStatus)
assert.Equal(t, corev1.PodRunning, apiTask.PodStatus.Phase)
assert.Len(t, apiTask.PodStatus.ContainerStatuses, 2)
assert.True(t, apiTask.PodStatus.ContainerStatuses[0].Ready)
assert.False(t, apiTask.PodStatus.ContainerStatuses[1].Ready)
assert.False(t, utils.IsPodReadyConditionTrue(*apiTask.PodStatus))
// Conditions check
var podReady, containersReady *corev1.PodCondition
for i := range apiTask.PodStatus.Conditions {
c := &apiTask.PodStatus.Conditions[i]
if c.Type == corev1.PodReady {
podReady = c
} else if c.Type == corev1.ContainersReady {
containersReady = c
}
}
assert.NotNil(t, podReady)
assert.Equal(t, corev1.ConditionFalse, podReady.Status)
assert.NotNil(t, containersReady)
assert.Equal(t, corev1.ConditionFalse, containersReady.Status)
assert.Equal(t, now.Unix(), podReady.LastTransitionTime.Unix())
})
t.Run("Pod Task - Fully Ready", func(t *testing.T) {
later := now.Add(time.Minute)
task := &types.Task{
Name: "pod-task-ready",
PodTemplateSpec: &corev1.PodTemplateSpec{},
Status: types.Status{
State: types.TaskStateRunning,
SubStatuses: []types.SubStatus{
{
Name: "c1",
StartedAt: &now,
},
{
Name: "c2",
StartedAt: &later,
},
},
},
}
apiTask := convertInternalToAPITask(task)
assert.NotNil(t, apiTask.PodStatus)
// Conditions check
var podReady, containersReady *corev1.PodCondition
for i := range apiTask.PodStatus.Conditions {
c := &apiTask.PodStatus.Conditions[i]
if c.Type == corev1.PodReady {
podReady = c
} else if c.Type == corev1.ContainersReady {
containersReady = c
}
}
assert.NotNil(t, podReady)
assert.Equal(t, corev1.ConditionTrue, podReady.Status)
assert.NotNil(t, containersReady)
assert.Equal(t, corev1.ConditionTrue, containersReady.Status)
// Should use the latest timestamp (later)
assert.Equal(t, later.Unix(), podReady.LastTransitionTime.Unix())
assert.True(t, utils.IsPodReadyConditionTrue(*apiTask.PodStatus))
})
}
func TestConvertInternalToAPITask_Timeout(t *testing.T) {
now := time.Now()
timeoutSec := int64(60)
t.Run("Process Task Timeout", func(t *testing.T) {
task := &types.Task{
Name: "timeout-task",
Process: &api.Process{
Command: []string{"sleep", "100"},
TimeoutSeconds: &timeoutSec,
},
Status: types.Status{
State: types.TaskStateTimeout,
SubStatuses: []types.SubStatus{
{
Reason: "TaskTimeout",
Message: "Task exceeded timeout of 60 seconds",
StartedAt: &now,
FinishedAt: nil, // Not finished yet
},
},
},
}
apiTask := convertInternalToAPITask(task)
// Should map to Terminated with exit code 137
assert.NotNil(t, apiTask.ProcessStatus)
assert.NotNil(t, apiTask.ProcessStatus.Terminated)
assert.Nil(t, apiTask.ProcessStatus.Running)
assert.Nil(t, apiTask.ProcessStatus.Waiting)
assert.Equal(t, int32(137), apiTask.ProcessStatus.Terminated.ExitCode)
assert.Equal(t, "TaskTimeout", apiTask.ProcessStatus.Terminated.Reason)
assert.Equal(t, "Task exceeded timeout of 60 seconds", apiTask.ProcessStatus.Terminated.Message)
assert.Equal(t, now.Unix(), apiTask.ProcessStatus.Terminated.StartedAt.Unix())
// FinishedAt should be set to "now" for timeout
assert.False(t, apiTask.ProcessStatus.Terminated.FinishedAt.IsZero())
assert.Nil(t, apiTask.PodStatus)
})
t.Run("Timeout After Completion", func(t *testing.T) {
later := now.Add(2 * time.Minute)
task := &types.Task{
Name: "completed-task",
Process: &api.Process{Command: []string{"ls"}},
Status: types.Status{
State: types.TaskStateFailed, // After stop, it becomes Failed
SubStatuses: []types.SubStatus{
{
ExitCode: 137,
Reason: "Killed",
StartedAt: &now,
FinishedAt: &later,
},
},
},
}
apiTask := convertInternalToAPITask(task)
// Should be Terminated with actual exit code
assert.NotNil(t, apiTask.ProcessStatus.Terminated)
assert.Equal(t, int32(137), apiTask.ProcessStatus.Terminated.ExitCode)
assert.Equal(t, now.Unix(), apiTask.ProcessStatus.Terminated.StartedAt.Unix())
assert.Equal(t, later.Unix(), apiTask.ProcessStatus.Terminated.FinishedAt.Unix())
})
}