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":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
This commit is contained in:
2026-09-05 22:13:57 +08:00
parent 6ce9b483fd
commit 7f484d7a71
20 changed files with 4660 additions and 0 deletions
+192
View File
@@ -0,0 +1,192 @@
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)
}
}