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
+117
View File
@@ -0,0 +1,117 @@
package jscriptx_test
import (
"context"
"strings"
"testing"
"git.fsdpf.net/go/jscriptx"
)
// 脚本碰到 goja 没有的全局时,光一句 ReferenceError 会把人带偏——
// 外部作者看到 "setTimeout is not defined" 会以为是打包漏了依赖,跑去装 polyfill。
// 这组用例守着那句补充说明。
func TestMissingGlobal_报错带上人话(t *testing.T) {
cases := []struct {
代码 string
要点 []string
}{
{`setTimeout(() => {}, 10)`, []string{"没有事件循环", "Job / Cron"}},
{`clearInterval(1)`, []string{"定时器用不了"}},
{`structuredClone({})`, []string{"JSON.parse", "深拷贝"}},
{`const 名 = "lodash"; require(名)`, []string{"ES 模块", "import"}}, // 动态名字,esbuild 打包期解析不了,才会漏到运行期
{`process.env.X`, []string{"不跑在 Node"}},
{`document.body`, []string{"没有 DOM"}},
{`fetch("http://x")`, []string{"同步执行", "Go 侧"}},
}
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
for _, c := range cases {
s, err := e.Compile("m.ts", "export default class C { Run() { "+c.代码+" } }")
if err != nil {
t.Fatalf("%s 编译失败: %v", c.代码, err)
}
ctx := context.Background()
obj, err := s.New(ctx)
if err != nil {
t.Fatalf("%s 实例化失败: %v", c.代码, err)
}
_, err = obj.Call(ctx, "Run")
obj.Close()
if err == nil {
t.Errorf("%s 该报错", c.代码)
continue
}
for _, want := range c.要点 {
if !strings.Contains(err.Error(), want) {
t.Errorf("%s 的错误里该提到 %q\n实际: %v", c.代码, want, err)
}
}
}
}
// 只认 goja 原样的那句,别把脚本自己抛的同名字符串也当成缺全局。
func TestMissingGlobal_不误伤(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("m.ts", `
export default class C {
Run() { throw new Error("ReferenceError: setTimeout is not defined") }
}`)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
obj, _ := s.New(ctx)
defer obj.Close()
_, err = obj.Call(ctx, "Run")
if err == nil {
t.Fatal("该报错")
}
// 脚本自己抛的,goja 会加上 "Error: " 前缀,前缀对不上就不该补提示
if strings.Contains(err.Error(), "没有事件循环") {
t.Errorf("不该给脚本自己抛的异常补提示: %v", err)
}
}
// 定义成"一调就报错"的桩会破坏特性探测,所以这些全局必须保持 undefined。
func TestMissingGlobal_特性探测仍然可用(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("m.ts", `
export default class C {
Run() {
// 库里常见的降级写法
return typeof setTimeout === "undefined" ? "走降级分支" : "走定时器分支"
}
}`)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
obj, _ := s.New(ctx)
defer obj.Close()
got, err := obj.Call(ctx, "Run")
if err != nil {
t.Fatal(err)
}
if got != "走降级分支" {
t.Errorf("定时器该是 undefined,好让库能降级,拿到 %#v", got)
}
}