Files
jscriptx/lazyglobal_test.go
T
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

281 lines
7.4 KiB
Go
Raw 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"
"log/slog"
"strings"
"sync/atomic"
"testing"
"git.fsdpf.net/go/jscriptx"
)
// countExt 记录自己的 Bindings 被取用过几次——用来验证"没访问就不 freeze"。
type countExt struct {
name string
calls *atomic.Int64
}
func (e countExt) Name() string { return e.name }
func (countExt) Module() (string, string) { return "", "" }
func (e countExt) Bindings() map[string]any {
return map[string]any{
"hit": func() string {
e.calls.Add(1)
return e.name
},
}
}
// 全局是惰性的:装的是访问器属性,值要等第一次读才算出来。
//
// 直接查属性描述符——惰性注入是 {get, set},急切注入是 {value, writable}。
// 这是两种实现唯一可靠的观测点:数"方法被调了几次"不行,因为急切注入只是
// **包装**函数、并不调用它,两边都是 0。
func TestLazyGlobal_是惰性的访问器(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("s.ts", `
export default function () {
const d = Object.getOwnPropertyDescriptor(globalThis, "unused")
if (typeof d.get !== "function") return "急切: 装的是数据属性"
if (d.value !== undefined) return "急切: 有 value"
// 读一次,值要能正常算出来
return typeof unused.hit === "function" ? "惰性" : "惰性但值不对"
}`)
if err != nil {
t.Fatal(err)
}
var usedCalls, unusedCalls atomic.Int64
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(
countExt{"used", &usedCalls},
countExt{"unused", &unusedCalls},
))
got, err := s.Call(ctx, "")
if err != nil {
t.Fatal(err)
}
if got != "惰性" {
t.Errorf("全局该是惰性的访问器属性,结果 %#v", got)
}
}
// 用得上的扩展照常能用——惰性不能把功能弄丢。
func TestLazyGlobal_用得上的照常(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("s.ts", `export default function () { return used.hit() }`)
if err != nil {
t.Fatal(err)
}
var usedCalls, unusedCalls atomic.Int64
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(
countExt{"used", &usedCalls},
countExt{"unused", &unusedCalls},
))
got, err := s.Call(ctx, "")
if err != nil {
t.Fatal(err)
}
if got != "used" {
t.Errorf("拿到 %#v", got)
}
if usedCalls.Load() != 1 {
t.Errorf("used.hit 该被调一次,实际 %d 次", usedCalls.Load())
}
if unusedCalls.Load() != 0 {
t.Error("没用到的扩展,它的方法不该被调用")
}
}
// 读两次只 freeze 一次——getter 里的缓存要生效。
func TestLazyGlobal_只转换一次(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
// 每次 freeze 都会重新包装 Bindings 里的函数,所以两次读到的
// 如果是同一个对象,就说明只转换了一次
s, err := e.Compile("s.ts", `
export default function () { return globalThis.ext === globalThis.ext }`)
if err != nil {
t.Fatal(err)
}
var calls atomic.Int64
ctx := jscriptx.WithScope(context.Background(),
jscriptx.ScopeExtensions(countExt{"ext", &calls}))
got, err := s.Call(ctx, "")
if err != nil {
t.Fatal(err)
}
if got != true {
t.Error("两次读该拿到同一个对象,说明 freeze 只跑了一次")
}
}
// 只读性不能变:脚本赋值不生效。
//
// 注意断言的是"值没变"而不是"抛错"——打包产物**没有** "use strict"
// 脚本跑在非严格模式下,给只读属性赋值是静默失败的。
func TestLazyGlobal_仍然只读(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("s.ts", `
export default function () {
globalThis.ext = 1
return typeof ext === "object" ? "没被改掉" : "被改成了 " + typeof ext
}`)
if err != nil {
t.Fatal(err)
}
var calls atomic.Int64
ctx := jscriptx.WithScope(context.Background(),
jscriptx.ScopeExtensions(countExt{"ext", &calls}))
got, err := s.Call(ctx, "")
if err != nil {
t.Fatal(err)
}
if got != "没被改掉" {
t.Errorf("扩展全局该是只读的,结果 %#v", got)
}
}
// 删不掉、重定义不了(configurable=false)。
func TestLazyGlobal_不可删除(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("s.ts", `
export default function () {
delete globalThis.ext
return typeof ext === "object" ? "还在" : "被删了"
}`)
if err != nil {
t.Fatal(err)
}
var calls atomic.Int64
ctx := jscriptx.WithScope(context.Background(),
jscriptx.ScopeExtensions(countExt{"ext", &calls}))
got, err := s.Call(ctx, "")
if err != nil {
t.Fatal(err)
}
if got != "还在" {
t.Errorf("configurable 该是 false,删不掉,结果 %#v", got)
}
}
// 每个 VM 各转各的——goja.Value 跨不了 Runtime,缓存绝不能共享。
func TestLazyGlobal_每个VM各一份(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("s.ts", `export default function () { return ext.hit() }`)
if err != nil {
t.Fatal(err)
}
var calls atomic.Int64
// 每次都是新作用域 → 新 VM(带作用域的调用不走池)
for range 5 {
ctx := jscriptx.WithScope(context.Background(),
jscriptx.ScopeExtensions(countExt{"ext", &calls}))
if _, err := s.Call(ctx, ""); err != nil {
t.Fatal(err)
}
}
if got := calls.Load(); got != 5 {
t.Errorf("5 个 VM 各调一次,该是 5 次,实际 %d 次", got)
}
}
// 白名单全局(WithGlobals)同样是惰性的,行为跟以前一致。
func TestLazyGlobal_白名单也惰性(t *testing.T) {
e, err := jscriptx.New(
jscriptx.WithLogger(nil),
jscriptx.WithGlobals(map[string]any{
"cfg": map[string]any{"name": "线上", "port": 8080},
}),
)
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("s.ts", `export default function () { return cfg.name + ":" + cfg.port }`)
if err != nil {
t.Fatal(err)
}
got, err := s.Call(context.Background(), "")
if err != nil {
t.Fatal(err)
}
if got != "线上:8080" {
t.Errorf("拿到 %#v", got)
}
// 嵌套的对象也该是只读的
s2, _ := e.Compile("s2.ts", `
export default function () { cfg.name = "改了"; return cfg.name }`)
if got, _ := s2.Call(context.Background(), ""); got != "线上" {
t.Errorf("嵌套字段该只读、改不动,结果 %#v", got)
}
}
// newTestLogger 造一个把输出写进 buf 的 slog,用来验 console。
func newTestLogger(buf *strings.Builder) *slog.Logger {
return slog.New(slog.NewTextHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
}
// console 也走惰性注入,用法不变。
func TestLazyGlobal_console照常(t *testing.T) {
var buf strings.Builder
e, err := jscriptx.New(jscriptx.WithLogger(newTestLogger(&buf)))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("s.ts", `export default function () { console.log("来自脚本"); return 1 }`)
if err != nil {
t.Fatal(err)
}
if _, err := s.Call(context.Background(), ""); err != nil {
t.Fatal(err)
}
if !strings.Contains(buf.String(), "来自脚本") {
t.Errorf("console.log 该照常落到日志,拿到 %q", buf.String())
}
}