Files
jscriptx/engine_globals_test.go
what 7c9a70712e test: 测试文件跟源文件同名,删掉复制粘贴出来的重复
改完之后「某个行为在哪测」就是查同名文件:

  lazyglobal_test.go      → engine_globals_test.go
  static_test.go          → script_static_test.go
  scope_pool_test.go      → script_vm_test.go     它测的是 VM 池,不是作用域
  esmwrap_test.go         → bundle_finalize_test.go
  missing_global_test.go  → errors_hints_test.go
  entry_test.go           → bundle_entry_test.go  并收下 bundle_test.go 里
                                                  那三个 TestEntry_*

删掉 5 组复制粘贴。其中 TestNew_同实例调用串行 和 TestInstance_调用串行 **函数体
完全一致**,只差 mustCompile 的文件名参数 counter.js / counter.ts。Instance 的
测试全部归 instance_test.go 并统一前缀成 TestInstance_,scope_test.go 只留作用域。

vmstate_test.go + govalue_test.go → goja_facts_test.go。这两个测的都是 **goja
本身**,不是这个库:Program 能不能给多个 Runtime 共用、顶层变量跟不跟 Runtime 走、
Go 的切片进到脚本里是不是真数组。它们是「为什么这个库要这么设计」的实验记录,
goja 行为变了会先红。原来那个名字看不出这层定位。

helper 集中到 helper_test.go:newEngine / mustCompile 被十几个文件依赖,原来住在
script_test.go 里,那个名字暗示「测 Script」,找不到。

caller_example_test.go 并进 caller_test.go——同一个类型没必要两个文件。

overlay_test.go 和 script_vm_test.go 保持内部测试,各写了一句为什么:前者要读
Engine.loader 和 isPrepared,后者要直接断言 vmHandle.scoped。我试过把 overlay
挪成外部测试,挪不动。
2026-09-10 15:39:25 +08:00

281 lines
7.4 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"
"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())
}
}