284 lines
7.0 KiB
Go
284 lines
7.0 KiB
Go
package process_test
|
|
|
|
import (
|
|
"context"
|
|
"cursor-api-proxy/internal/process"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// sh 是跨平台 shell 執行小 script 的輔助函式
|
|
func sh(t *testing.T, script string, opts process.RunOptions) (process.RunResult, error) {
|
|
t.Helper()
|
|
return process.Run("sh", []string{"-c", script}, opts)
|
|
}
|
|
|
|
func TestRun_StdoutAndStderr(t *testing.T) {
|
|
result, err := sh(t, "echo hello; echo world >&2", process.RunOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Code != 0 {
|
|
t.Errorf("Code = %d, want 0", result.Code)
|
|
}
|
|
if result.Stdout != "hello\n" {
|
|
t.Errorf("Stdout = %q, want %q", result.Stdout, "hello\n")
|
|
}
|
|
if result.Stderr != "world\n" {
|
|
t.Errorf("Stderr = %q, want %q", result.Stderr, "world\n")
|
|
}
|
|
}
|
|
|
|
func TestRun_BasicSpawn(t *testing.T) {
|
|
result, err := sh(t, "printf ok", process.RunOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Code != 0 {
|
|
t.Errorf("Code = %d, want 0", result.Code)
|
|
}
|
|
if result.Stdout != "ok" {
|
|
t.Errorf("Stdout = %q, want ok", result.Stdout)
|
|
}
|
|
}
|
|
|
|
func TestRun_ConfigDir_Propagated(t *testing.T) {
|
|
result, err := process.Run("sh", []string{"-c", `printf "$CURSOR_CONFIG_DIR"`},
|
|
process.RunOptions{ConfigDir: "/test/account/dir"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Stdout != "/test/account/dir" {
|
|
t.Errorf("Stdout = %q, want /test/account/dir", result.Stdout)
|
|
}
|
|
}
|
|
|
|
func TestRun_ConfigDir_Absent(t *testing.T) {
|
|
// 確保沒有殘留的環境變數
|
|
_ = os.Unsetenv("CURSOR_CONFIG_DIR")
|
|
result, err := process.Run("sh", []string{"-c", `printf "${CURSOR_CONFIG_DIR:-unset}"`},
|
|
process.RunOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Stdout != "unset" {
|
|
t.Errorf("Stdout = %q, want unset", result.Stdout)
|
|
}
|
|
}
|
|
|
|
func TestRun_NonZeroExit(t *testing.T) {
|
|
result, err := sh(t, "exit 42", process.RunOptions{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Code != 42 {
|
|
t.Errorf("Code = %d, want 42", result.Code)
|
|
}
|
|
}
|
|
|
|
func TestRun_Timeout(t *testing.T) {
|
|
start := time.Now()
|
|
result, err := sh(t, "sleep 30", process.RunOptions{TimeoutMs: 300})
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Code == 0 {
|
|
t.Error("expected non-zero exit code after timeout")
|
|
}
|
|
if elapsed > 2*time.Second {
|
|
t.Errorf("elapsed %v, want < 2s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestRunStreaming_OnLine(t *testing.T) {
|
|
var lines []string
|
|
result, err := process.RunStreaming("sh", []string{"-c", "printf 'a\nb\nc\n'"},
|
|
process.RunStreamingOptions{
|
|
OnLine: func(line string) { lines = append(lines, line) },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Code != 0 {
|
|
t.Errorf("Code = %d, want 0", result.Code)
|
|
}
|
|
if len(lines) != 3 {
|
|
t.Errorf("got %d lines, want 3: %v", len(lines), lines)
|
|
}
|
|
if lines[0] != "a" || lines[1] != "b" || lines[2] != "c" {
|
|
t.Errorf("lines = %v, want [a b c]", lines)
|
|
}
|
|
}
|
|
|
|
func TestRunStreaming_FlushFinalLine(t *testing.T) {
|
|
var lines []string
|
|
result, err := process.RunStreaming("sh", []string{"-c", "printf tail"},
|
|
process.RunStreamingOptions{
|
|
OnLine: func(line string) { lines = append(lines, line) },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Code != 0 {
|
|
t.Errorf("Code = %d, want 0", result.Code)
|
|
}
|
|
if len(lines) != 1 {
|
|
t.Errorf("got %d lines, want 1: %v", len(lines), lines)
|
|
}
|
|
if lines[0] != "tail" {
|
|
t.Errorf("lines[0] = %q, want tail", lines[0])
|
|
}
|
|
}
|
|
|
|
func TestRunStreaming_ConfigDir(t *testing.T) {
|
|
var lines []string
|
|
_, err := process.RunStreaming("sh", []string{"-c", `printf "$CURSOR_CONFIG_DIR"`},
|
|
process.RunStreamingOptions{
|
|
RunOptions: process.RunOptions{ConfigDir: "/my/config/dir"},
|
|
OnLine: func(line string) { lines = append(lines, line) },
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(lines) != 1 || lines[0] != "/my/config/dir" {
|
|
t.Errorf("lines = %v, want [/my/config/dir]", lines)
|
|
}
|
|
}
|
|
|
|
func TestRunStreaming_Stderr(t *testing.T) {
|
|
result, err := process.RunStreaming("sh", []string{"-c", "echo err-output >&2"},
|
|
process.RunStreamingOptions{OnLine: func(string) {}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Stderr == "" {
|
|
t.Error("expected stderr to contain output")
|
|
}
|
|
}
|
|
|
|
func TestRunStreaming_Timeout(t *testing.T) {
|
|
start := time.Now()
|
|
result, err := process.RunStreaming("sh", []string{"-c", "sleep 30"},
|
|
process.RunStreamingOptions{
|
|
RunOptions: process.RunOptions{TimeoutMs: 300},
|
|
OnLine: func(string) {},
|
|
})
|
|
elapsed := time.Since(start)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if result.Code == 0 {
|
|
t.Error("expected non-zero exit code after timeout")
|
|
}
|
|
if elapsed > 2*time.Second {
|
|
t.Errorf("elapsed %v, want < 2s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestRunStreaming_Concurrent(t *testing.T) {
|
|
var lines1, lines2 []string
|
|
done := make(chan struct{}, 2)
|
|
|
|
run := func(label string, target *[]string) {
|
|
process.RunStreaming("sh", []string{"-c", "printf '" + label + "'"},
|
|
process.RunStreamingOptions{
|
|
OnLine: func(line string) { *target = append(*target, line) },
|
|
})
|
|
done <- struct{}{}
|
|
}
|
|
|
|
go run("stream1", &lines1)
|
|
go run("stream2", &lines2)
|
|
|
|
<-done
|
|
<-done
|
|
|
|
if len(lines1) != 1 || lines1[0] != "stream1" {
|
|
t.Errorf("lines1 = %v, want [stream1]", lines1)
|
|
}
|
|
if len(lines2) != 1 || lines2[0] != "stream2" {
|
|
t.Errorf("lines2 = %v, want [stream2]", lines2)
|
|
}
|
|
}
|
|
|
|
func TestRunStreaming_ContextCancel(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
start := time.Now()
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
process.RunStreaming("sh", []string{"-c", "sleep 30"},
|
|
process.RunStreamingOptions{
|
|
RunOptions: process.RunOptions{Ctx: ctx},
|
|
OnLine: func(string) {},
|
|
})
|
|
close(done)
|
|
}()
|
|
|
|
time.AfterFunc(100*time.Millisecond, cancel)
|
|
<-done
|
|
elapsed := time.Since(start)
|
|
|
|
if elapsed > 2*time.Second {
|
|
t.Errorf("elapsed %v, want < 2s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestRun_ContextCancel(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
start := time.Now()
|
|
done := make(chan process.RunResult, 1)
|
|
|
|
go func() {
|
|
r, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{Ctx: ctx})
|
|
done <- r
|
|
}()
|
|
|
|
time.AfterFunc(100*time.Millisecond, cancel)
|
|
result := <-done
|
|
elapsed := time.Since(start)
|
|
|
|
if result.Code == 0 {
|
|
t.Error("expected non-zero exit code after cancel")
|
|
}
|
|
if elapsed > 2*time.Second {
|
|
t.Errorf("elapsed %v, want < 2s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestRun_AlreadyCancelledContext(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // 已取消
|
|
|
|
start := time.Now()
|
|
result, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{Ctx: ctx})
|
|
elapsed := time.Since(start)
|
|
|
|
if result.Code == 0 {
|
|
t.Error("expected non-zero exit code")
|
|
}
|
|
if elapsed > 2*time.Second {
|
|
t.Errorf("elapsed %v, want < 2s", elapsed)
|
|
}
|
|
}
|
|
|
|
func TestKillAllChildProcesses(t *testing.T) {
|
|
done := make(chan process.RunResult, 1)
|
|
go func() {
|
|
r, _ := process.Run("sh", []string{"-c", "sleep 30"}, process.RunOptions{})
|
|
done <- r
|
|
}()
|
|
|
|
time.Sleep(80 * time.Millisecond)
|
|
process.KillAllChildProcesses()
|
|
result := <-done
|
|
|
|
if result.Code == 0 {
|
|
t.Error("expected non-zero exit code after kill")
|
|
}
|
|
// 再次呼叫不應 panic
|
|
process.KillAllChildProcesses()
|
|
}
|