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) } } }