package jscriptx_test import ( "context" "errors" "strings" "sync" "testing" "time" "git.fsdpf.net/go/jscriptx" "git.fsdpf.net/go/jscriptx/internal/testext" ) // 死循环脚本必须能被超时打断,而不是把调用方的 goroutine 卡死。 func TestTimeout_打断死循环(t *testing.T) { e := newEngine(t, jscriptx.WithTimeout(100*time.Millisecond)) s := mustCompile(t, e, "loop", `export function spin() { while (true) {} }`) start := time.Now() _, err := s.Call(context.Background(), "spin") elapsed := time.Since(start) if !errors.Is(err, jscriptx.ErrTimeout) { t.Fatalf("want ErrTimeout, got %v", err) } var jsErr *jscriptx.Error if errors.As(err, &jsErr) && jsErr.Kind != jscriptx.KindTimeout { t.Errorf("Kind = %q, want timeout", jsErr.Kind) } if elapsed > 2*time.Second { t.Errorf("中断来得太慢: %v", elapsed) } t.Logf("%v 后被中断: %v", elapsed, err) } // 调用方主动取消 context 时同样要能立刻停下来。 func TestTimeout_context取消(t *testing.T) { e := newEngine(t, jscriptx.WithTimeout(0)) // 关掉默认超时,只靠 context s := mustCompile(t, e, "loop", `export function spin() { while (true) {} }`) ctx, cancel := context.WithCancel(context.Background()) go func() { time.Sleep(50 * time.Millisecond) cancel() }() _, err := s.Call(ctx, "spin") if !errors.Is(err, context.Canceled) { t.Fatalf("want context.Canceled, got %v", err) } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindCanceled { t.Errorf("Kind 不对: %v", err) } } // 脚本顶层就死循环时,建 VM 这一步也要受超时保护。 func TestTimeout_顶层死循环(t *testing.T) { e := newEngine(t, jscriptx.WithTimeout(100*time.Millisecond)) s := mustCompile(t, e, "toploop", `while (true) {}; function f() {}`) start := time.Now() _, err := s.Call(context.Background(), "f") if !errors.Is(err, jscriptx.ErrTimeout) { t.Fatalf("want ErrTimeout, got %v", err) } t.Logf("%v 后被中断", time.Since(start)) } // 一次超时不能污染 VM 池:紧接着的正常调用必须照常工作。 func TestTimeout_不污染VM池(t *testing.T) { e := newEngine(t, jscriptx.WithTimeout(100*time.Millisecond)) s := mustCompile(t, e, "mix", ` export function spin() { while (true) {} } export function ok() { return "fine" } `) for i := 0; i < 3; i++ { if _, err := s.Call(context.Background(), "spin"); !errors.Is(err, jscriptx.ErrTimeout) { t.Fatalf("第 %d 次: want ErrTimeout, got %v", i, err) } got, err := s.Call(context.Background(), "ok") if err != nil { t.Fatalf("第 %d 次超时之后的正常调用失败了: %v", i, err) } if got != "fine" { t.Fatalf("第 %d 次: got %#v", i, got) } } t.Logf("VM 池统计: %+v", s.Stats()) } // 注入到脚本里的 Go 函数 panic,必须兜成 error,不能掀翻调用方的 goroutine。 func TestPanic_Go侧回调panic(t *testing.T) { e := newEngine(t, jscriptx.WithGlobal("explode", func() { panic("Go 侧炸了") })) s := mustCompile(t, e, "gopanic", `export function run() { explode() }`) _, err := s.Call(context.Background(), "run") if err == nil { t.Fatal("应该报错而不是 panic 出去") } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) { t.Fatalf("错误类型不对: %T", err) } if jsErr.Kind != jscriptx.KindPanic { t.Errorf("Kind = %q, want panic", jsErr.Kind) } if !strings.Contains(jsErr.Msg, "Go 侧炸了") { t.Errorf("panic 信息丢了: %q", jsErr.Msg) } if jsErr.GoStack == "" { t.Error("没记录 Go 侧调用栈") } // panic 过的 VM 会被丢弃,下一次调用要能正常工作 if _, err := s.Call(context.Background(), "run"); err == nil { t.Error("第二次同样应该报错") } } // 脚本里的空指针/类型错误是普通 JS 异常,走 KindRuntime。 func TestPanic_脚本类型错误(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "typeerr", `export function run() { var x = null; return x.foo.bar }`) _, err := s.Call(context.Background(), "run") var jsErr *jscriptx.Error if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindRuntime { t.Fatalf("want KindRuntime, got %v", err) } t.Logf("%v", err) } // 无限递归会打爆调用栈,goja 有栈深上限,这里确认它变成普通 error。 func TestPanic_无限递归(t *testing.T) { e := newEngine(t, jscriptx.WithMaxCallStackSize(200)) s := mustCompile(t, e, "recur", `export function run() { return run() }`) _, err := s.Call(context.Background(), "run") if err == nil { t.Fatal("应该报错") } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindRuntime { t.Fatalf("错误分类不对: %v", err) } t.Logf("%v", jsErr.Msg) } // 并发调用同一个脚本:VM 池要保证每个 goroutine 拿到独占的 VM(配合 -race 跑)。 func TestConcurrent_并发调用(t *testing.T) { e := newEngine(t, jscriptx.WithMaxVMs(4)) s := mustCompile(t, e, "concurrent", ` export function work(state, key, n) { state.Incr(key, n) return state.Get(key) } `) const goroutines = 16 const perGoroutine = 50 // 一份共享状态,所有 goroutine 通过参数传给脚本,验证并发下不丢更新 shared := testext.New() var wg sync.WaitGroup for g := 0; g < goroutines; g++ { wg.Add(1) go func(g int) { defer wg.Done() state := shared for i := 0; i < perGoroutine; i++ { if _, err := s.Call(context.Background(), "work", state, "total", 1); err != nil { t.Errorf("goroutine %d: %v", g, err) return } } }(g) } wg.Wait() got := shared.Get("total") if n, _ := got.(float64); n != goroutines*perGoroutine { t.Errorf("累加结果 = %#v, want %d", got, goroutines*perGoroutine) } t.Logf("VM 池统计: %+v", s.Stats()) } // 并发场景下混着超时调用,确认坏 VM 不会被别的 goroutine 捡到。 func TestConcurrent_混合超时(t *testing.T) { e := newEngine(t, jscriptx.WithTimeout(50*time.Millisecond), jscriptx.WithMaxVMs(4)) s := mustCompile(t, e, "mixed", ` export function spin() { while (true) {} } export function ok(n) { return n + 1 } `) var wg sync.WaitGroup for g := 0; g < 8; g++ { wg.Add(1) go func(g int) { defer wg.Done() for i := 0; i < 10; i++ { if g%2 == 0 { if _, err := s.Call(context.Background(), "spin"); !errors.Is(err, jscriptx.ErrTimeout) { t.Errorf("want ErrTimeout, got %v", err) return } continue } got, err := s.Call(context.Background(), "ok", i) if err != nil { t.Errorf("正常调用被超时污染了: %v", err) return } if n, _ := got.(int64); n != int64(i+1) { t.Errorf("got %#v, want %d", got, i+1) return } } }(g) } wg.Wait() t.Logf("VM 池统计: %+v", s.Stats()) }