Files
jscriptx/caller_test.go
what 7c9a70712e test: 测试文件跟源文件同名,删掉复制粘贴出来的重复
改完之后「某个行为在哪测」就是查同名文件:

  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
挪成外部测试,挪不动。
2026-09-10 15:39:25 +08:00

298 lines
7.8 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 而不是直接用 Callnext 是个 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 → [输入]
}