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:
+225
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user