Files
2026-06-09 22:43:29 +08:00

164 lines
4.5 KiB
Go

package run
import (
"context"
"runtime"
"sync"
"testing"
"time"
"github.com/google/uuid"
)
// fakeRepo 仅实现 supervisor 实际用到的 UpdateRunProfileExit;其余满足接口即可。
type fakeRepo struct {
mu sync.Mutex
exitID uuid.UUID
exitCode *int32
lastErr string
called bool
}
func (f *fakeRepo) CreateRunProfile(context.Context, *RunProfile) (*RunProfile, error) {
return nil, nil
}
func (f *fakeRepo) GetRunProfileByID(context.Context, uuid.UUID) (*RunProfile, error) {
return nil, nil
}
func (f *fakeRepo) ListRunProfilesByWorkspace(context.Context, uuid.UUID) ([]*RunProfile, error) {
return nil, nil
}
func (f *fakeRepo) UpdateRunProfile(context.Context, *RunProfile) (*RunProfile, error) {
return nil, nil
}
func (f *fakeRepo) DeleteRunProfile(context.Context, uuid.UUID) error { return nil }
func (f *fakeRepo) MarkRunProfileStarted(context.Context, uuid.UUID) (*RunProfile, error) {
return nil, nil
}
func (f *fakeRepo) UpdateRunProfileExit(_ context.Context, id uuid.UUID, exitCode *int32, lastErr string) error {
f.mu.Lock()
defer f.mu.Unlock()
f.called = true
f.exitID = id
f.exitCode = exitCode
f.lastErr = lastErr
return nil
}
func testCfg() SupervisorConfig {
return SupervisorConfig{
KillGrace: 2 * time.Second,
OutBufferLines: 100,
OutTailBytes: 2000,
WSSendBuffer: 16,
ShutdownGrace: 5 * time.Second,
}
}
func quickExit() SpawnParams {
p := SpawnParams{ProfileID: uuid.New()}
if runtime.GOOS == "windows" {
p.Command, p.Args = "cmd", []string{"/c", "exit", "0"}
} else {
p.Command, p.Args = "sh", []string{"-c", "exit 0"}
}
return p
}
func sleeper() SpawnParams {
p := SpawnParams{ProfileID: uuid.New()}
if runtime.GOOS == "windows" {
p.Command, p.Args = "cmd", []string{"/c", "timeout", "/t", "30", "/nobreak"}
} else {
p.Command, p.Args = "sh", []string{"-c", "sleep 30"}
}
return p
}
// TestSpawn_SelfExit_RecordsCrash 验证:未被我方终止而自行退出 → crashed,
// 且 onExit 把退出码落库(monitor 在 close(done) 前已完成 onExit)。
func TestSpawn_SelfExit_RecordsCrash(t *testing.T) {
repo := &fakeRepo{}
sup := NewSupervisor(repo, testCfg(), nil)
params := quickExit()
proc, err := sup.Spawn(params)
if err != nil {
t.Fatalf("spawn: %v", err)
}
select {
case <-proc.done:
case <-time.After(10 * time.Second):
t.Fatal("process did not exit")
}
if got := sup.StatusOf(params.ProfileID); got != StatusCrashed {
t.Fatalf("status = %q, want crashed (self-exit not initiated by us)", got)
}
repo.mu.Lock()
defer repo.mu.Unlock()
if !repo.called {
t.Fatal("UpdateRunProfileExit not called on exit")
}
if repo.exitID != params.ProfileID {
t.Fatalf("exit recorded for wrong profile: %v", repo.exitID)
}
if repo.exitCode == nil || *repo.exitCode != 0 {
t.Fatalf("exit code = %v, want 0", repo.exitCode)
}
}
// TestStop_TerminatesAndMarksStopped 验证 Stop 整树终止并判定为 stopped。
func TestStop_TerminatesAndMarksStopped(t *testing.T) {
repo := &fakeRepo{}
sup := NewSupervisor(repo, testCfg(), nil)
params := sleeper()
proc, err := sup.Spawn(params)
if err != nil {
t.Fatalf("spawn: %v", err)
}
if got := sup.StatusOf(params.ProfileID); got != StatusRunning {
t.Fatalf("status = %q, want running", got)
}
if err := sup.Stop(context.Background(), params.ProfileID, testCfg().KillGrace); err != nil {
t.Fatalf("stop: %v", err)
}
// Stop 已等 done;onExit 在 close(done) 前完成。
select {
case <-proc.done:
default:
t.Fatal("done not closed after Stop")
}
if got := sup.StatusOf(params.ProfileID); got != StatusStopped {
t.Fatalf("status = %q, want stopped", got)
}
if _, ok := sup.Get(params.ProfileID); ok {
t.Fatal("proc still registered after stop")
}
}
// TestSpawn_AlreadyRunning 验证同一 profile 不能并发拉起两个进程。
func TestSpawn_AlreadyRunning(t *testing.T) {
sup := NewSupervisor(&fakeRepo{}, testCfg(), nil)
params := sleeper()
if _, err := sup.Spawn(params); err != nil {
t.Fatalf("first spawn: %v", err)
}
defer func() { _ = sup.Stop(context.Background(), params.ProfileID, testCfg().KillGrace) }()
if _, err := sup.Spawn(params); err != ErrAlreadyRunning {
t.Fatalf("second spawn err = %v, want ErrAlreadyRunning", err)
}
}
// TestStop_NotRunning 验证停止未运行的 profile 返回 ErrNotRunning。
func TestStop_NotRunning(t *testing.T) {
sup := NewSupervisor(&fakeRepo{}, testCfg(), nil)
if err := sup.Stop(context.Background(), uuid.New(), testCfg().KillGrace); err != ErrNotRunning {
t.Fatalf("err = %v, want ErrNotRunning", err)
}
}