package jscriptx_test import ( "context" "errors" "strings" "testing" "git.fsdpf.net/go/jscriptx" ) // newEngine 造一个测试用引擎,默认不打日志(避免测试输出被 console 刷屏)。 func newEngine(t *testing.T, opts ...jscriptx.Option) *jscriptx.Engine { t.Helper() opts = append([]jscriptx.Option{jscriptx.WithLogger(nil)}, opts...) e, err := jscriptx.New(opts...) if err != nil { t.Fatalf("New 失败: %v", err) } t.Cleanup(e.Close) return e } func mustCompile(t *testing.T, e *jscriptx.Engine, name, src string) *jscriptx.Script { t.Helper() s, err := e.Compile(name, src) if err != nil { t.Fatalf("编译 %s 失败: %v", name, err) } return s } func TestCall_基本调用(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "calc", ` export function add(a, b) { return a + b } export function greet(name) { return "你好, " + name } export function nothing() {} `) got, err := s.Call(context.Background(), "add", 2, 3) if err != nil { t.Fatalf("调用失败: %v", err) } if n, ok := got.(int64); !ok || n != 5 { t.Errorf("add(2,3) = %#v, want int64(5)", got) } got, err = s.Call(context.Background(), "greet", "张三") if err != nil { t.Fatalf("调用失败: %v", err) } if got != "你好, 张三" { t.Errorf("greet = %#v", got) } got, err = s.Call(context.Background(), "nothing") if err != nil { t.Fatalf("调用失败: %v", err) } if got != nil { t.Errorf("无返回值的函数应该拿到 nil,实际 %#v", got) } } func TestCall_脚本自身求值出的函数(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "default_fn", ` export default function handler(x) { return x * 2 } `) got, err := s.Call(context.Background(), jscriptx.DefaultFunc, 21) if err != nil { t.Fatalf("调用失败: %v", err) } if n, _ := got.(int64); n != 42 { t.Errorf("got = %#v, want 42", got) } } func TestCallInto_按类型转换(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "typed", ` export function count() { return 7 } export function profile() { return {Name: "张三", Age: 30} } `) var n int if err := s.CallInto(context.Background(), "count", &n); err != nil { t.Fatalf("调用失败: %v", err) } if n != 7 { t.Errorf("n = %d, want 7", n) } // goja 默认按 Go 字段名映射(不看 json tag),脚本里就得写 Name/Age var p struct { Name string Age int } if err := s.CallInto(context.Background(), "profile", &p); err != nil { t.Fatalf("调用失败: %v", err) } if p.Name != "张三" || p.Age != 30 { t.Errorf("p = %+v", p) } } func TestCallInto_拒绝非指针目标(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "x", `export function f() { return 1 }`) var n int err := s.CallInto(context.Background(), "f", n) if err == nil { t.Fatal("传值不传指针应该报错") } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindType { t.Errorf("错误分类不对: %v", err) } } // 脚本函数是绑在 VM 上的,VM 一还回池子就失效了,不能让它跨出边界。 func TestValueEscape_拒绝把脚本函数带出来(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "escape", `export function make() { return function(x) { return x } }`) if _, err := s.Call(context.Background(), "make"); !errors.Is(err, jscriptx.ErrValueEscape) { t.Errorf("Call 返回函数应该被拒绝,实际: %v", err) } var fn func(int) int err := s.CallInto(context.Background(), "make", &fn) if !errors.Is(err, jscriptx.ErrValueEscape) { t.Errorf("CallInto 导出成 Go 函数应该被拒绝,实际: %v", err) } } func TestCall_函数不存在(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "mod.ts", ` export const x = 1 export function other() { return "有这个" } `) _, err := s.Call(context.Background(), "nope") if !errors.Is(err, jscriptx.ErrFuncNotFound) { t.Fatalf("want ErrFuncNotFound, got %v", err) } if s.Has("nope") { t.Error("Has 不该认出不存在的名字") } if !s.Has("other") { t.Error("Has 认不出导出的函数") } // 导出的名字存在但不是函数,同样算找不到 if s.Has("x") { t.Error("非函数的导出不该被当成函数") } } func TestCompile_语法错误(t *testing.T) { e := newEngine(t) _, err := e.Compile("bad", `export function f( { syntax error`) if err == nil { t.Fatal("语法错误应该在编译期报出来") } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindCompile { t.Errorf("错误分类不对: %v", err) } if jsErr.Script != "bad" { t.Errorf("错误里没带上脚本名: %+v", jsErr) } } func TestError_带定位信息(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "boom.js", ` export function explode(arg) { throw new Error("业务校验失败: " + arg) } `) _, err := s.Call(context.Background(), "explode", "订单号-123") if err == nil { t.Fatal("应该报错") } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) { t.Fatalf("错误类型不对: %T", err) } if jsErr.Kind != jscriptx.KindRuntime { t.Errorf("Kind = %q", jsErr.Kind) } if jsErr.Script != "boom.js" || jsErr.Func != "explode" { t.Errorf("脚本/函数名不对: %+v", jsErr) } if !strings.Contains(jsErr.Msg, "业务校验失败") { t.Errorf("没带上脚本抛出的信息: %q", jsErr.Msg) } if len(jsErr.Stack) == 0 || jsErr.Stack[0].Line != 3 { t.Errorf("栈信息定位不到出错行: %+v", jsErr.Stack) } if len(jsErr.Args) != 1 || !strings.Contains(jsErr.Args[0], "订单号-123") { t.Errorf("没记录调用参数: %v", jsErr.Args) } t.Logf("错误文本: %v", err) } // Go 函数返回的 error 透到脚本里没被 catch 时,调用方的 errors.Is 还应该匹配得上。 func TestError_透传Go侧哨兵错误(t *testing.T) { sentinel := errors.New("库存不足") e := newEngine(t, jscriptx.WithGlobal("checkStock", func() error { return sentinel })) s := mustCompile(t, e, "stock", `export function run() { checkStock(); return "ok" }`) _, err := s.Call(context.Background(), "run") if !errors.Is(err, sentinel) { t.Fatalf("原始 Go error 丢了: %v", err) } } func TestClose_关闭后拒绝调用(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "x", `export function f() { return 1 }`) if _, err := s.Call(context.Background(), "f"); err != nil { t.Fatal(err) } s.Close() if _, err := s.Call(context.Background(), "f"); !errors.Is(err, jscriptx.ErrClosed) { t.Errorf("want ErrClosed, got %v", err) } s.Close() // 重复关闭不应 panic }