改完之后「某个行为在哪测」就是查同名文件:
lazyglobal_test.go → engine_globals_test.go
static_test.go → script_static_test.go
scope_pool_test.go → script_vm_test.go 它测的是 VM 池,不是作用域
esmwrap_test.go → bundle_finalize_test.go
missing_global_test.go → errors_hints_test.go
entry_test.go → bundle_entry_test.go 并收下 bundle_test.go 里
那三个 TestEntry_*
删掉 5 组复制粘贴。其中 TestNew_同实例调用串行 和 TestInstance_调用串行 **函数体
完全一致**,只差 mustCompile 的文件名参数 counter.js / counter.ts。Instance 的
测试全部归 instance_test.go 并统一前缀成 TestInstance_,scope_test.go 只留作用域。
vmstate_test.go + govalue_test.go → goja_facts_test.go。这两个测的都是 **goja
本身**,不是这个库:Program 能不能给多个 Runtime 共用、顶层变量跟不跟 Runtime 走、
Go 的切片进到脚本里是不是真数组。它们是「为什么这个库要这么设计」的实验记录,
goja 行为变了会先红。原来那个名字看不出这层定位。
helper 集中到 helper_test.go:newEngine / mustCompile 被十几个文件依赖,原来住在
script_test.go 里,那个名字暗示「测 Script」,找不到。
caller_example_test.go 并进 caller_test.go——同一个类型没必要两个文件。
overlay_test.go 和 script_vm_test.go 保持内部测试,各写了一句为什么:前者要读
Engine.loader 和 isPrepared,后者要直接断言 vmHandle.scoped。我试过把 overlay
挪成外部测试,挪不动。
298 lines
7.8 KiB
Go
298 lines
7.8 KiB
Go
package jscriptx_test
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"log"
|
||
"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)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Caller 用来实现自定义的调用约定——本库不预设脚本回调该长什么样,
|
||
// 这个例子实现的是「回调 + next」这种中间件式的约定:脚本函数写成哪种形状,
|
||
// 运行时探测出来自动适配,脚本作者不用额外声明签名。
|
||
//
|
||
// 它同时也说明了为什么需要 Caller 而不是直接用 Call:next 是个 Go 闭包,
|
||
// 得先看脚本声明了几个形参,才知道要不要把它传进去——这些都必须在同一次
|
||
// VM 借出期间完成。
|
||
func ExampleCaller() {
|
||
e, err := jscriptx.New(jscriptx.WithLogger(nil))
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
defer e.Close()
|
||
|
||
// 三种回调写法,脚本作者挑顺手的用
|
||
s, err := e.Compile("ctrl.ts", `
|
||
export function twoArgs(v, next) { return next(v) + "|脚本自己调的 next" }
|
||
export function decorator(v) { return v + "|装饰过" }
|
||
export function sideEffect(v) { /* 只做事,不返回 */ }
|
||
`)
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
|
||
next := func(v string) string { return "[" + v + "]" }
|
||
|
||
// 回调签名的约定是**调用方**定的(这里定成"1 个或 2 个形参"),
|
||
// 所以哨兵错误也归调用方声明,本库不提供。
|
||
errBadSignature := errors.New("回调签名不受支持")
|
||
|
||
// dispatch 按脚本函数的形状分派
|
||
dispatch := func(fn, value string) (string, error) {
|
||
var out string
|
||
err := s.WithCall(context.Background(), fn, func(c jscriptx.Caller) error {
|
||
switch n := c.Arity(); n {
|
||
case 2:
|
||
// handle(v, next):调不调 next 由脚本决定,但得把结果返回出来
|
||
res, err := c.Call(value, next)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if res.IsEmpty() {
|
||
return fmt.Errorf("%w: 两个形参的回调必须返回结果",
|
||
errBadSignature)
|
||
}
|
||
return res.Into(&out)
|
||
|
||
case 1:
|
||
// handle(v):有返回值当装饰器,没返回值当纯副作用
|
||
res, err := c.Call(value)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if res.IsEmpty() {
|
||
out = next(value)
|
||
return nil
|
||
}
|
||
var decorated string
|
||
if err := res.Into(&decorated); err != nil {
|
||
return err
|
||
}
|
||
out = next(decorated)
|
||
return nil
|
||
|
||
default:
|
||
return fmt.Errorf("%w: 形参个数必须是 1 或 2,当前是 %d",
|
||
errBadSignature, n)
|
||
}
|
||
})
|
||
return out, err
|
||
}
|
||
|
||
for _, fn := range []string{"twoArgs", "decorator", "sideEffect"} {
|
||
got, err := dispatch(fn, "输入")
|
||
if err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
fmt.Printf("%-11s → %s\n", fn, got)
|
||
}
|
||
|
||
// Output:
|
||
// twoArgs → [输入]|脚本自己调的 next
|
||
// decorator → [输入|装饰过]
|
||
// sideEffect → [输入]
|
||
}
|