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
+209
View File
@@ -0,0 +1,209 @@
package jscriptx_test
import (
"context"
"errors"
"strings"
"testing"
"time"
"git.fsdpf.net/go/jscriptx"
)
// WithCall 把 VM 借出期间的控制权交给调用方,用来实现自定义的调用约定。
func TestWithCall_探测签名并调用(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "cb.ts", `
export function zero() { return "无参" }
export function one(a) { return "一参:" + a }
export function two(a, b) { return "二参:" + a + "," + b }
`)
ctx := context.Background()
for _, c := range []struct {
fn string
wantArity int
args []any
want string
}{
{"zero", 0, nil, "无参"},
{"one", 1, []any{"x"}, "一参:x"},
{"two", 2, []any{"x", "y"}, "二参:x,y"},
} {
var got string
err := s.WithCall(ctx, c.fn, func(cl jscriptx.Caller) error {
if cl.Arity() != c.wantArity {
t.Errorf("%s: Arity = %d, want %d", c.fn, cl.Arity(), c.wantArity)
}
if cl.Script() != "cb.ts" {
t.Errorf("Script = %q", cl.Script())
}
res, err := cl.Call(c.args...)
if err != nil {
return err
}
return res.Into(&got)
})
if err != nil {
t.Fatalf("%s: %v", c.fn, err)
}
if got != c.want {
t.Errorf("%s = %q, want %q", c.fn, got, c.want)
}
}
}
// 把 Go 闭包当参数传给脚本,脚本能直接调它——「next」就是靠这个实现的。
func TestWithCall_传Go闭包(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "cb.ts", `
export function handle(v, next) { return next(v) + "|脚本加的" }
`)
calledWith := ""
next := func(v string) string {
calledWith = v
return "Go 返回的:" + v
}
var got string
err := s.WithCall(context.Background(), "handle", func(c jscriptx.Caller) error {
res, err := c.Call("输入", next)
if err != nil {
return err
}
return res.Into(&got)
})
if err != nil {
t.Fatal(err)
}
if calledWith != "输入" {
t.Errorf("Go 闭包没被脚本调到,calledWith = %q", calledWith)
}
if got != "Go 返回的:输入|脚本加的" {
t.Errorf("got = %q", got)
}
}
// 脚本没返回东西时 IsEmpty 为真——「纯副作用」的写法靠它识别。
func TestWithCall_空返回值(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "cb.ts", `
export function nothing() {}
export function something() { return 1 }
`)
ctx := context.Background()
check := func(fn string, wantEmpty bool) {
t.Helper()
err := s.WithCall(ctx, fn, func(c jscriptx.Caller) error {
res, err := c.Call()
if err != nil {
return err
}
if res.IsEmpty() != wantEmpty {
t.Errorf("%s: IsEmpty = %v, want %v", fn, res.IsEmpty(), wantEmpty)
}
if wantEmpty && res.Value() != nil {
t.Errorf("%s: 空返回值的 Value 应该是 nil, got %#v", fn, res.Value())
}
return nil
})
if err != nil {
t.Fatalf("%s: %v", fn, err)
}
}
check("nothing", true)
check("something", false)
}
// do 里返回的错误会被包成 *Error;wrap 哨兵错误后调用方 errors.Is 能匹配到。
func TestWithCall_自定义错误(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "cb.ts", `export function f(a, b, c) { return 1 }`)
sentinel := errors.New("我的约定不接受三个形参")
err := s.WithCall(context.Background(), "f", func(c jscriptx.Caller) error {
if c.Arity() != 2 {
return sentinel
}
return nil
})
if !errors.Is(err, sentinel) {
t.Fatalf("哨兵错误丢了: %v", err)
}
var jsErr *jscriptx.Error
if !errors.As(err, &jsErr) {
t.Fatalf("没被包成 *Error: %T", err)
}
if jsErr.Script != "cb.ts" || jsErr.Func != "f" {
t.Errorf("上下文不对: %+v", jsErr)
}
}
// 超时、panic 这些保护跟普通调用一样生效。
func TestWithCall_同样受保护(t *testing.T) {
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
s := mustCompile(t, e, "cb.ts", `
export function spin() { while (true) {} }
export function boom() { throw new Error("脚本炸了") }
`)
ctx := context.Background()
err := s.WithCall(ctx, "spin", func(c jscriptx.Caller) error {
_, err := c.Call()
return err
})
if !errors.Is(err, jscriptx.ErrTimeout) {
t.Errorf("want ErrTimeout, got %v", err)
}
err = s.WithCall(ctx, "boom", func(c jscriptx.Caller) error {
_, err := c.Call()
return err
})
var jsErr *jscriptx.Error
if !errors.As(err, &jsErr) || !strings.Contains(jsErr.Msg, "脚本炸了") {
t.Errorf("脚本异常没带出来: %v", err)
}
// 函数不存在
err = s.WithCall(ctx, "nope", func(c jscriptx.Caller) error { return nil })
if !errors.Is(err, jscriptx.ErrFuncNotFound) {
t.Errorf("want ErrFuncNotFound, got %v", err)
}
}
// 实例上也能用,跑在它独占的 VM 里。
func TestWithCall_在实例上(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "c.ts", `
export default class C {
constructor() { this.n = 0 }
bump() { return ++this.n }
}
`)
ctx := context.Background()
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
for i := 1; i <= 3; i++ {
var got int
err := obj.WithCall(ctx, "bump", func(c jscriptx.Caller) error {
res, err := c.Call()
if err != nil {
return err
}
return res.Into(&got)
})
if err != nil {
t.Fatal(err)
}
if got != i {
t.Errorf("第 %d 次 = %d,实例状态没保持", i, got)
}
}
}