Files
wehub-resource-sync e0e362d700
SDK Tests / changes (push) Successful in 2m29s
Real E2E Tests / changes (push) Successful in 2m29s
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
SDK Tests / CLI Tests (push) Has been cancelled
SDK Tests / Python SDK Quality (code-interpreter) (push) Has been cancelled
SDK Tests / Python SDK Quality (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / JavaScript SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Python SDK Tests (sandbox) (push) Has been cancelled
SDK Tests / CLI Quality (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Kotlin SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (code-interpreter) (push) Has been cancelled
SDK Tests / C# SDK Quality And Tests (sandbox) (push) Has been cancelled
SDK Tests / Go SDK Quality And Tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:39:33 +08:00

823 lines
19 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 manager
import (
"context"
"os/exec"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/config"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/runtime"
store "github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/storage"
"github.com/alibaba/OpenSandbox/sandbox-k8s/internal/task-executor/types"
api "github.com/alibaba/OpenSandbox/sandbox-k8s/pkg/task-executor"
)
type fakeExecutor struct {
inspect map[string]*types.Status
starts int
}
func newFakeExecutor() *fakeExecutor {
return &fakeExecutor{inspect: make(map[string]*types.Status)}
}
func (f *fakeExecutor) Start(_ context.Context, task *types.Task) error {
f.starts++
f.inspect[task.Name] = &types.Status{
State: types.TaskStateRunning,
SubStatuses: []types.SubStatus{{
Reason: "Running",
}},
}
return nil
}
func (f *fakeExecutor) Inspect(_ context.Context, task *types.Task) (*types.Status, error) {
if status, ok := f.inspect[task.Name]; ok {
return status, nil
}
return &types.Status{
State: types.TaskStateFailed,
SubStatuses: []types.SubStatus{{
Reason: "ProcessCrashed",
Message: "Process exited without writing exit code",
}},
}, nil
}
func (f *fakeExecutor) Stop(_ context.Context, _ *types.Task) error {
return nil
}
func setupTestManager(t *testing.T) (TaskManager, *config.Config) {
cfg := &config.Config{
DataDir: t.TempDir(),
EnableSidecarMode: false,
ReconcileInterval: 100 * time.Millisecond,
}
taskStore, err := store.NewFileStore(cfg.DataDir)
if err != nil {
t.Fatalf("failed to create store: %v", err)
}
exec, err := runtime.NewProcessExecutor(cfg)
if err != nil {
t.Fatalf("failed to create executor: %v", err)
}
mgr, err := NewTaskManager(cfg, taskStore, exec)
if err != nil {
t.Fatalf("failed to create manager: %v", err)
}
return mgr, cfg
}
func cleanupTask(t *testing.T, mgr TaskManager, name string) {
ctx := context.Background()
mgr.Delete(ctx, name)
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
_, err := mgr.Get(ctx, name)
if err != nil {
return
}
time.Sleep(100 * time.Millisecond)
}
t.Logf("Task %s not deleted within timeout during cleanup", name)
}
func TestNewTaskManager(t *testing.T) {
cfg := &config.Config{
DataDir: t.TempDir(),
}
taskStore, _ := store.NewFileStore(cfg.DataDir)
exec, _ := runtime.NewProcessExecutor(cfg)
tests := []struct {
name string
cfg *config.Config
store store.TaskStore
executor runtime.Executor
wantErr bool
}{
{
name: "nil config",
cfg: nil,
store: taskStore,
executor: exec,
wantErr: true,
},
{
name: "nil store",
cfg: cfg,
store: nil,
executor: exec,
wantErr: true,
},
{
name: "nil executor",
cfg: cfg,
store: taskStore,
executor: nil,
wantErr: true,
},
{
name: "valid parameters",
cfg: cfg,
store: taskStore,
executor: exec,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mgr, err := NewTaskManager(tt.cfg, tt.store, tt.executor)
if (err != nil) != tt.wantErr {
t.Errorf("NewTaskManager() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && mgr == nil {
t.Error("NewTaskManager() returned nil manager")
}
})
}
}
func TestTaskManager_Create(t *testing.T) {
mgr, _ := setupTestManager(t)
ctx := context.Background()
tests := []struct {
name string
task *types.Task
wantErr bool
}{
{
name: "nil task",
task: nil,
wantErr: true,
},
{
name: "empty task name",
task: &types.Task{
Name: "",
Process: &api.Process{
Command: []string{"echo", "test"},
},
},
wantErr: true,
},
{
name: "valid task",
task: &types.Task{
Name: "test-task",
Process: &api.Process{
Command: []string{"sh", "-c", "echo hello && exit 0"},
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
created, err := mgr.Create(ctx, tt.task)
if (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr {
if created == nil {
t.Error("Create() returned nil task")
}
if created != nil && created.Name != tt.task.Name {
t.Errorf("Create() task name = %v, want %v", created.Name, tt.task.Name)
}
// Wait for task to complete naturally
time.Sleep(200 * time.Millisecond)
// Then clean up
if tt.task != nil {
mgr.Delete(ctx, tt.task.Name)
}
}
})
}
}
func TestTaskManager_CreateDuplicate(t *testing.T) {
mgr, _ := setupTestManager(t)
mgr.Start(context.Background())
defer mgr.Stop()
ctx := context.Background()
task := &types.Task{
Name: "duplicate-task",
Process: &api.Process{
Command: []string{"echo", "test"},
},
}
// First create should succeed
_, err := mgr.Create(ctx, task)
if err != nil {
t.Fatalf("First Create() failed: %v", err)
}
defer cleanupTask(t, mgr, task.Name)
// Second create should fail
_, err = mgr.Create(ctx, task)
if err == nil {
t.Error("Create() should fail for duplicate task")
}
}
func TestTaskManager_CreateMaxConcurrentTasks(t *testing.T) {
mgr, _ := setupTestManager(t)
mgr.Start(context.Background())
defer mgr.Stop()
ctx := context.Background()
task1 := &types.Task{
Name: "task-1",
Process: &api.Process{
Command: []string{"sleep", "10"},
},
}
// Create first task
_, err := mgr.Create(ctx, task1)
if err != nil {
t.Fatalf("First Create() failed: %v", err)
}
defer cleanupTask(t, mgr, task1.Name)
// Try to create second task - should fail due to max concurrent limit
task2 := &types.Task{
Name: "task-2",
Process: &api.Process{
Command: []string{"echo", "test"},
},
}
_, err = mgr.Create(ctx, task2)
if err == nil {
t.Error("Create() should fail when max concurrent tasks reached")
cleanupTask(t, mgr, task2.Name)
}
}
func TestTaskManager_Get(t *testing.T) {
mgr, _ := setupTestManager(t)
mgr.Start(context.Background())
defer mgr.Stop()
ctx := context.Background()
task := &types.Task{
Name: "get-task",
Process: &api.Process{
Command: []string{"echo", "get"},
},
}
// Create task
_, err := mgr.Create(ctx, task)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
defer cleanupTask(t, mgr, task.Name)
// Get task
got, err := mgr.Get(ctx, task.Name)
if err != nil {
t.Fatalf("Get() failed: %v", err)
}
if got.Name != task.Name {
t.Errorf("Get() name = %v, want %v", got.Name, task.Name)
}
}
func TestTaskManager_GetNotFound(t *testing.T) {
mgr, _ := setupTestManager(t)
ctx := context.Background()
_, err := mgr.Get(ctx, "non-existent")
if err == nil {
t.Error("Get() should fail for non-existent task")
}
}
func TestTaskManager_GetEmptyName(t *testing.T) {
mgr, _ := setupTestManager(t)
ctx := context.Background()
_, err := mgr.Get(ctx, "")
if err == nil {
t.Error("Get() should fail for empty name")
}
}
func TestTaskManager_List(t *testing.T) {
mgr, _ := setupTestManager(t)
ctx := context.Background()
// Initially empty
tasks, err := mgr.List(ctx)
if err != nil {
t.Fatalf("List() failed: %v", err)
}
if len(tasks) != 0 {
t.Errorf("List() initial count = %d, want 0", len(tasks))
}
// Create a task
task := &types.Task{
Name: "list-task",
Process: &api.Process{
Command: []string{"echo", "list"},
},
}
_, err = mgr.Create(ctx, task)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
defer mgr.Delete(ctx, task.Name)
// List should return 1 task
tasks, err = mgr.List(ctx)
if err != nil {
t.Fatalf("List() failed: %v", err)
}
if len(tasks) != 1 {
t.Errorf("List() count = %d, want 1", len(tasks))
}
if tasks[0].Name != task.Name {
t.Errorf("List() task name = %v, want %v", tasks[0].Name, task.Name)
}
}
func TestTaskManager_Delete(t *testing.T) {
mgr, _ := setupTestManager(t)
// Start the manager to enable the reconcile loop
mgr.Start(context.Background())
defer mgr.Stop()
ctx := context.Background()
task := &types.Task{
Name: "delete-task",
Process: &api.Process{
Command: []string{"echo", "delete"},
},
}
// Create task
_, err := mgr.Create(ctx, task)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
// Delete task (soft delete)
err = mgr.Delete(ctx, task.Name)
if err != nil {
t.Errorf("Delete() failed: %v", err)
}
// Verify task is marked for deletion but still exists
got, err := mgr.Get(ctx, task.Name)
if err != nil {
t.Fatalf("Get() should succeed after Delete() (soft delete): %v", err)
}
if got.DeletionTimestamp == nil {
t.Error("DeletionTimestamp should be set after Delete()")
}
// Wait for task to be finalized
timeout := 5 * time.Second
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
_, err := mgr.Get(ctx, task.Name)
if err != nil {
// Task is gone, success
return
}
time.Sleep(100 * time.Millisecond)
}
t.Error("Task was not finalized (deleted) within timeout")
}
func TestTaskManager_DeleteNonExistent(t *testing.T) {
mgr, _ := setupTestManager(t)
ctx := context.Background()
// Delete non-existent task should not error
err := mgr.Delete(ctx, "non-existent")
if err != nil {
t.Errorf("Delete() should not fail for non-existent task: %v", err)
}
}
func TestTaskManager_SyncRestartsRecoveredActiveTaskWhenRuntimeStateIsLost(t *testing.T) {
ctx := context.Background()
cfg := &config.Config{
DataDir: t.TempDir(),
EnableSidecarMode: false,
ReconcileInterval: time.Hour,
}
taskStore, err := store.NewFileStore(cfg.DataDir)
require.NoError(t, err)
persisted := &types.Task{
Name: "resume-task",
Process: &api.Process{
Command: []string{"sleep", "3600"},
},
Status: types.Status{
State: types.TaskStateRunning,
},
}
require.NoError(t, taskStore.Create(ctx, persisted))
exec := newFakeExecutor()
mgrIface, err := NewTaskManager(cfg, taskStore, exec)
require.NoError(t, err)
mgr := mgrIface.(*taskManager)
require.NoError(t, mgr.recoverTasks(ctx))
tasks, err := mgr.Sync(ctx, []*types.Task{{
Name: "resume-task",
Process: &api.Process{
Command: []string{"sleep", "3600"},
},
}})
require.NoError(t, err)
require.Len(t, tasks, 1)
assert.Equal(t, 1, exec.starts, "sync should recreate an active task whose recovered runtime state was lost")
assert.Equal(t, types.TaskStateRunning, tasks[0].Status.State)
}
func TestTaskManager_SyncKeepsRecoveredSucceededTask(t *testing.T) {
ctx := context.Background()
cfg := &config.Config{
DataDir: t.TempDir(),
EnableSidecarMode: false,
ReconcileInterval: time.Hour,
}
taskStore, err := store.NewFileStore(cfg.DataDir)
require.NoError(t, err)
persisted := &types.Task{
Name: "completed-task",
Process: &api.Process{
Command: []string{"echo", "done"},
},
Status: types.Status{
State: types.TaskStateSucceeded,
},
}
require.NoError(t, taskStore.Create(ctx, persisted))
exec := newFakeExecutor()
exec.inspect["completed-task"] = &types.Status{
State: types.TaskStateSucceeded,
SubStatuses: []types.SubStatus{{
Reason: "Succeeded",
}},
}
mgrIface, err := NewTaskManager(cfg, taskStore, exec)
require.NoError(t, err)
mgr := mgrIface.(*taskManager)
require.NoError(t, mgr.recoverTasks(ctx))
tasks, err := mgr.Sync(ctx, []*types.Task{{
Name: "completed-task",
Process: &api.Process{
Command: []string{"echo", "done"},
},
}})
require.NoError(t, err)
require.Len(t, tasks, 1)
assert.Equal(t, 0, exec.starts, "sync should not recreate tasks that were already completed before recovery")
assert.Equal(t, types.TaskStateSucceeded, tasks[0].Status.State)
}
func TestTaskManager_Sync(t *testing.T) {
mgr, _ := setupTestManager(t)
// Start the manager to enable the reconcile loop
mgr.Start(context.Background())
defer mgr.Stop()
ctx := context.Background()
// Create initial task
task1 := &types.Task{
Name: "sync-task-1",
Process: &api.Process{
Command: []string{"echo", "1"},
},
}
_, err := mgr.Create(ctx, task1)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
// Sync with new desired state (task1 removed, task2 added)
task2 := &types.Task{
Name: "sync-task-2",
Process: &api.Process{
Command: []string{"echo", "2"},
},
}
// Sync triggers soft delete for task1 and creation of task2
current, err := mgr.Sync(ctx, []*types.Task{task2})
if err != nil {
t.Fatalf("Sync() failed: %v", err)
}
defer mgr.Delete(ctx, task2.Name)
// Verify task1 is marked for deletion in the returned list
var task1Found bool
for _, t1 := range current {
if t1.Name == task1.Name {
task1Found = true
if t1.DeletionTimestamp == nil {
t.Error("task1 should be marked for deletion after Sync()")
}
}
}
if !task1Found {
// It's possible it was deleted super fast, but unlikely
t.Log("task1 not found in Sync result (maybe already deleted?)")
}
// Verify task2 is created
var task2Found bool
for _, t2 := range current {
if t2.Name == task2.Name {
task2Found = true
}
}
if !task2Found {
t.Error("task2 should be present after Sync()")
}
// Wait for task1 to be finalized
timeout := 5 * time.Second
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
_, err := mgr.Get(ctx, task1.Name)
if err != nil {
// Task is gone, success
return
}
time.Sleep(100 * time.Millisecond)
}
t.Error("task1 should be deleted after Sync()")
}
func TestTaskManager_SyncNil(t *testing.T) {
mgr, _ := setupTestManager(t)
ctx := context.Background()
_, err := mgr.Sync(ctx, nil)
if err == nil {
t.Error("Sync() should fail for nil desired list")
}
}
func TestTaskManager_AsyncStopOnDelete(t *testing.T) {
mgr, _ := setupTestManager(t)
mgr.Start(context.Background())
defer mgr.Stop()
ctx := context.Background()
timeoutSec := int64(30)
task := &types.Task{
Name: "long-running-task",
Process: &api.Process{
Command: []string{"sleep", "30"},
TimeoutSeconds: &timeoutSec,
},
}
// Create task
created, err := mgr.Create(ctx, task)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
defer cleanupTask(t, mgr, task.Name)
// Verify task is running
assert.Equal(t, types.TaskStateRunning, created.Status.State)
// Record the time before delete
beforeDelete := time.Now()
// Delete task (should trigger async stop)
err = mgr.Delete(ctx, task.Name)
if err != nil {
t.Fatalf("Delete() failed: %v", err)
}
// Verify DeletionTimestamp is set immediately (soft delete)
got, err := mgr.Get(ctx, task.Name)
if err != nil {
t.Fatalf("Get() after Delete failed: %v", err)
}
if got.DeletionTimestamp == nil {
t.Error("DeletionTimestamp should be set immediately after Delete()")
}
// Verify Delete returned quickly (not blocked by Stop)
deleteDuration := time.Since(beforeDelete)
if deleteDuration > 500*time.Millisecond {
t.Errorf("Delete() took too long (%v), should be fast (async stop)", deleteDuration)
}
// Wait for task to be finalized
deadline := time.Now().Add(15 * time.Second)
for time.Now().Before(deadline) {
_, err := mgr.Get(ctx, task.Name)
if err != nil {
// Task is gone, success
return
}
time.Sleep(100 * time.Millisecond)
}
t.Error("Task was not finalized within timeout after async stop")
}
func TestTaskManager_TimeoutHandling(t *testing.T) {
if _, err := exec.LookPath("sh"); err != nil {
t.Skip("sh not found, skipping timeout test")
}
mgr, _ := setupTestManager(t)
mgr.Start(context.Background())
defer mgr.Stop()
ctx := context.Background()
// Create task with short timeout
timeoutSec := int64(2)
task := &types.Task{
Name: "timeout-task",
Process: &api.Process{
Command: []string{"sleep", "30"},
TimeoutSeconds: &timeoutSec,
},
}
_, err := mgr.Create(ctx, task)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
defer cleanupTask(t, mgr, task.Name)
// Wait for timeout to be detected and async stop triggered
time.Sleep(3 * time.Second)
// Check task status - should be Timeout or Failed (after stop)
got, err := mgr.Get(ctx, task.Name)
if err != nil {
t.Fatalf("Get() failed: %v", err)
}
// State should be Timeout (during stop) or Failed (after stop completes)
if got.Status.State != types.TaskStateTimeout && got.Status.State != types.TaskStateFailed {
t.Errorf("Expected Timeout or Failed state, got: %s", got.Status.State)
}
// If in Timeout state, verify reason
if got.Status.State == types.TaskStateTimeout {
assert.NotEmpty(t, got.Status.SubStatuses)
assert.Equal(t, "TaskTimeout", got.Status.SubStatuses[0].Reason)
}
// Wait for final state
deadline := time.Now().Add(15 * time.Second)
for time.Now().Before(deadline) {
got, err := mgr.Get(ctx, task.Name)
if err != nil {
// Task was deleted, that's also acceptable
return
}
if got.Status.State == types.TaskStateFailed {
// Stop completed
return
}
time.Sleep(200 * time.Millisecond)
}
}
func TestTaskManager_CountActiveTasks(t *testing.T) {
mgr, _ := setupTestManager(t)
mgr.Start(context.Background())
defer mgr.Stop()
ctx := context.Background()
// Initially empty
activeCount := mgr.(*taskManager).countActiveTasks()
if activeCount != 0 {
t.Errorf("Initial active count = %d, want 0", activeCount)
}
// Create a short-lived task that will complete quickly
task1 := &types.Task{
Name: "quick-task-1",
Process: &api.Process{
Command: []string{"echo", "done"},
},
}
_, err := mgr.Create(ctx, task1)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
defer mgr.Delete(ctx, task1.Name)
// Wait for task1 to complete
time.Sleep(500 * time.Millisecond)
// Should have 0 active tasks after task1 completes
activeCount = mgr.(*taskManager).countActiveTasks()
if activeCount != 0 {
t.Errorf("Active count after task1 completion = %d, want 0", activeCount)
}
// Create a running task
task2 := &types.Task{
Name: "active-task-2",
Process: &api.Process{
Command: []string{"sleep", "5"},
},
}
_, err = mgr.Create(ctx, task2)
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
defer mgr.Delete(ctx, task2.Name)
// Should have 1 active task
activeCount = mgr.(*taskManager).countActiveTasks()
if activeCount != 1 {
t.Errorf("Active count after create = %d, want 1", activeCount)
}
}
func TestIsTerminalState(t *testing.T) {
tests := []struct {
name string
state types.TaskState
expected bool
}{
{"Succeeded is terminal", types.TaskStateSucceeded, true},
{"Failed is terminal", types.TaskStateFailed, true},
{"NotFound is terminal", types.TaskStateNotFound, true},
{"Pending is not terminal", types.TaskStatePending, false},
{"Running is not terminal", types.TaskStateRunning, false},
{"Unknown is not terminal", types.TaskStateUnknown, false},
{"Timeout is not terminal", types.TaskStateTimeout, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isTerminalState(tt.state)
if got != tt.expected {
t.Errorf("isTerminalState(%v) = %v, want %v", tt.state, got, tt.expected)
}
})
}
}