Files
jscriptx/engine_test.go
what 7f484d7a71 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":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
2026-09-05 22:13:57 +08:00

193 lines
5.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"
"strings"
"testing"
"git.fsdpf.net/go/jscriptx"
)
func TestNew_全局名校验(t *testing.T) {
for _, name := range []string{"db.From", "2fast", "有中文", "", "a-b"} {
if _, err := jscriptx.New(jscriptx.WithGlobal(name, 1)); !errors.Is(err, jscriptx.ErrBadGlobal) {
t.Errorf("全局名 %q 应该被拒绝,got %v", name, err)
}
}
for _, name := range []string{"db", "req", "_x", "$", "a1"} {
if _, err := jscriptx.New(jscriptx.WithGlobal(name, 1)); err != nil {
t.Errorf("全局名 %q 应该被接受: %v", name, err)
}
}
}
// 白名单之外的东西脚本一律看不见。
func TestGlobals_白名单之外看不见(t *testing.T) {
e := newEngine(t, jscriptx.WithGlobals(map[string]any{
"allowed": func() string { return "ok" },
}))
s := mustCompile(t, e, "wl", `
export function probe(name) { return typeof globalThis[name] }
`)
for _, tc := range []struct{ name, want string }{
{"allowed", "function"},
{"require", "undefined"},
{"process", "undefined"},
{"setTimeout", "undefined"},
{"db", "undefined"},
} {
got, err := s.Call(context.Background(), "probe", tc.name)
if err != nil {
t.Fatal(err)
}
if got != tc.want {
t.Errorf("typeof %s = %v, want %v", tc.name, got, tc.want)
}
}
}
// 注入的命名空间对象必须是只读的:否则同一个 Go map 被所有 VM 共享,
// 脚本改一下既污染别的 VM 又是数据竞争。
func TestGlobals_命名空间只读(t *testing.T) {
ns := map[string]any{
"helper": map[string]any{"double": func(n int) int { return n * 2 }},
}
e := newEngine(t, jscriptx.WithGlobals(ns))
s := mustCompile(t, e, "readonly", `
export function tamper() {
try { helper.double = null } catch (err) { /* 严格模式才会抛,这里吞掉 */ }
try { helper = null } catch (err) {}
return typeof helper.double
}
`)
for i := 0; i < 3; i++ {
got, err := s.Call(context.Background(), "tamper")
if err != nil {
t.Fatal(err)
}
if got != "function" {
t.Fatalf("第 %d 次: 白名单被脚本改掉了: %v", i, got)
}
}
// Go 侧的原始 map 也不能被动过
if _, ok := ns["helper"].(map[string]any)["double"].(func(int) int); !ok {
t.Error("Go 侧的白名单 map 被脚本改掉了")
}
}
// 多返回值约定:这是脚本作者唯一需要额外理解的规则,用测试把它钉死。
func TestMultiReturn_转换约定(t *testing.T) {
e := newEngine(t, jscriptx.WithGlobals(map[string]any{
"one": func() string { return "hello" },
"okPair": func() (string, error) { return "hello", nil },
"errPair": func() (string, error) { return "", errors.New("boom") },
"twoVals": func() (string, int) { return "a", 1 },
"threeVal": func() (string, int, error) { return "a", 1, nil },
"threeErr": func() (string, int, error) { return "", 0, errors.New("boom") },
}))
s := mustCompile(t, e, "multi", `
export function run() {
var out = {}
out.one = one()
out.okPair = okPair() // error 为 nil:拿到裸值
out.twoVals = twoVals().join(",") // 无 error:拿到数组
out.threeVal = threeVal().join(",") // 末位 error 为 nil:只剩前两个
try { errPair(); out.errPair = "没抛" } catch (err) { out.errPair = "抛了: " + err.message }
try { threeErr(); out.threeErr = "没抛" } catch (err) { out.threeErr = "抛了" }
return out
}
`)
got, err := s.Call(context.Background(), "run")
if err != nil {
t.Fatal(err)
}
out, _ := got.(map[string]any)
want := map[string]any{
"one": "hello",
"okPair": "hello",
"twoVals": "a,1",
"threeVal": "a,1",
"threeErr": "抛了",
}
for k, v := range want {
if out[k] != v {
t.Errorf("%s = %#v, want %#v", k, out[k], v)
}
}
if s, _ := out["errPair"].(string); !strings.HasPrefix(s, "抛了") {
t.Errorf("errPair = %#verror 非 nil 时应该变成 JS 异常", out["errPair"])
}
t.Logf("多返回值转换结果: %v", out)
}
func TestEngine_Reload与Invalidate(t *testing.T) {
loader := newTestLoader()
loader.put("x", `export function hi() { return "v1" }`, "v1")
e := newEngine(t, jscriptx.WithLoader(loader))
s1, _ := e.Script("x")
loader.put("x", `export function hi() { return "v2" }`, "v2")
// 没开自动重载,普通取还是旧的
s2, _ := e.Script("x")
if s1 != s2 {
t.Error("没开自动重载不该自己更新")
}
s3, err := e.Reload("x")
if err != nil {
t.Fatal(err)
}
if got, _ := s3.Call(context.Background(), "hi"); got != "v2" {
t.Errorf("Reload 后 got %#v", got)
}
// 旧 Script 被顶替后会被关掉,正在进行的调用不受影响、之后的调用报 ErrClosed
if _, err := s1.Call(context.Background(), "hi"); !errors.Is(err, jscriptx.ErrClosed) {
t.Errorf("被顶替的旧脚本应该已关闭,got %v", err)
}
e.Invalidate("x")
if len(e.Names()) != 0 {
t.Errorf("Invalidate 之后缓存该空了: %v", e.Names())
}
}
func TestEngine_Close(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
s := mustCompile(t, e, "x", `export function f() { return 1 }`)
e.Close()
if _, err := e.Compile("y", `export function g() {}`); !errors.Is(err, jscriptx.ErrClosed) {
t.Errorf("want ErrClosed, got %v", err)
}
if _, err := s.Call(context.Background(), "f"); !errors.Is(err, jscriptx.ErrClosed) {
t.Errorf("want ErrClosed, got %v", err)
}
}
// 同名脚本重新 Compile 会顶替掉旧的。
func TestEngine_重复Compile顶替(t *testing.T) {
e := newEngine(t)
mustCompile(t, e, "x", `export function f() { return 1 }`)
s2 := mustCompile(t, e, "x", `export function f() { return 2 }`)
cached, err := e.Script("x")
if err != nil {
t.Fatal(err)
}
if cached != s2 {
t.Error("缓存里应该是最后一次编译的版本")
}
if got, _ := cached.Call(context.Background(), "f"); got.(int64) != 2 {
t.Errorf("got %#v", got)
}
}