test: 引擎核心的测试与基准

除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:

  - vmstate_test  goja 的硬限制:非原始值不是 goroutine 安全的、对象跨不了
                  Runtime。其中一条是哨兵——goja 哪天允许对象跨 Runtime 了,
                  它会失败,提醒我们可以简化设计。
  - govalue_test  Go 的切片/map 进到脚本里长什么样。这不是本库的行为而是
                  goja 的,但脚本作者照着它写代码,变了会静默走错分支。
  - safety_test   并发下坏 VM 不会被别的 goroutine 捡到。
  - bench_gobind  脚本碰 Go 对象的单次开销,README 性能一节的数据来源。

missing_global_test 里有一条断言 typeof setTimeout === "undefined":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
This commit is contained in:
2026-09-05 22:13:57 +08:00
parent 6ce9b483fd
commit 7f484d7a71
20 changed files with 4660 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
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())
}