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