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

290 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"
"errors"
"testing"
"time"
"git.fsdpf.net/go/jscriptx"
"git.fsdpf.net/go/jscriptx/internal/testext"
)
const deviceScript = `
export default class DeviceHandler {
constructor(deviceId, model) {
this.deviceId = deviceId
this.model = model
this.count = 0
}
onMessage(payload) {
this.count++
return this.deviceId + "#" + this.count + ":" + payload
}
spin() { while (true) {} }
}
`
// 作用域的核心用途:同一个 ctx 下的多个脚本共享扩展。
func TestScope_多脚本共享扩展(t *testing.T) {
e := newEngine(t)
writer := mustCompile(t, e, "writer.ts", `
export default class Writer {
put(k, v) { store.Set(k, v) }
}
`)
reader := mustCompile(t, e, "reader.ts", `
export default class Reader {
take(k) { return store.Get(k) }
count() { return store.Len() }
}
`)
st := testext.New()
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st))
w, err := writer.New(ctx)
if err != nil {
t.Fatal(err)
}
defer w.Close()
r, err := reader.New(ctx)
if err != nil {
t.Fatal(err)
}
defer r.Close()
if _, err := w.Call(ctx, "put", "k", "写进去的"); err != nil {
t.Fatal(err)
}
got, err := r.Call(ctx, "take", "k")
if err != nil {
t.Fatal(err)
}
if got != "写进去的" {
t.Errorf("另一个脚本读不到: %#v", got)
}
// Go 侧拿的是同一份
if st.Get("k") != "写进去的" {
t.Errorf("Go 侧读不到: %#v", st.Get("k"))
}
st.Set("fromGo", 1)
if got, _ := r.Call(ctx, "count"); toNumber(t, got) != 2 {
t.Errorf("脚本看不到 Go 侧写的: %#v", got)
}
}
// 不同作用域之间互不干扰。
func TestScope_作用域之间隔离(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "counter.ts", `
export default class C {
bump() { return store.Incr("n", 1) }
}
`)
st1, st2 := testext.New(), testext.New()
ctx1 := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st1))
ctx2 := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st2))
o1, _ := s.New(ctx1)
defer o1.Close()
o2, _ := s.New(ctx2)
defer o2.Close()
o1.Call(ctx1, "bump")
o1.Call(ctx1, "bump")
got, err := o2.Call(ctx2, "bump")
if err != nil {
t.Fatal(err)
}
if toNumber(t, got) != 1 {
t.Errorf("第二个作用域被污染: %#v", got)
}
}
// 用已有的 SharedState 建 store,把 Go 侧正在用的状态直接交给脚本。
func TestScope_复用已有状态(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "s.ts", `
export default class S {
read(k) { return store.Get(k) }
}
`)
state := testext.New()
state.Set("preset", "Go 侧预置的")
ctx := jscriptx.WithScope(context.Background(),
jscriptx.ScopeExtensions(state)) // Store 本身就是状态,直接当扩展用
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
if got, _ := obj.Call(ctx, "read", "preset"); got != "Go 侧预置的" {
t.Errorf("got = %#v", got)
}
}
// 改扩展的全局名。
func TestScope_扩展改名(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "s.ts", `
export default class S {
put() { $state.Set("k", "v"); return typeof globalThis.store }
}
`)
st := testext.Named("$state")
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st))
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
got, err := obj.Call(ctx, "put")
if err != nil {
t.Fatalf("改名后没生效: %v", err)
}
if got != "undefined" {
t.Errorf("改名后默认名不该还在: %#v", got)
}
if st.Get("k") != "v" {
t.Error("写入没落到同一份")
}
}
// 没有作用域时,扩展的全局不存在。
func TestScope_没有作用域就没有扩展(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "s.ts", `
export default class S {
probe() { return typeof globalThis.store }
}
`)
obj, err := s.New(context.Background())
if err != nil {
t.Fatal(err)
}
defer obj.Close()
if got, _ := obj.Call(context.Background(), "probe"); got != "undefined" {
t.Errorf("没有作用域不该有 store: %#v", got)
}
}
// ScopeGlobals 注入作用域专属的全局,脚本能读到会话标识。
func TestScope_全局与标识(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "s.ts", `
export default class S {
who() { return currentUser.name + "@" + scope.key }
}
`)
ctx := jscriptx.WithScope(context.Background(),
jscriptx.ScopeKey("req-7"),
jscriptx.ScopeGlobals(map[string]any{
"currentUser": map[string]any{"name": "张三"},
}))
if key, ok := jscriptx.ScopeKeyOf(ctx); !ok || key != "req-7" {
t.Errorf("ScopeKeyOf = %q, %v", key, ok)
}
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
if got, _ := obj.Call(ctx, "who"); got != "张三@req-7" {
t.Errorf("got = %#v", got)
}
}
// 不给 key 时自动生成一个,两次 WithScope 拿到的不一样。
func TestScope_自动生成key(t *testing.T) {
c1 := jscriptx.WithScope(context.Background())
c2 := jscriptx.WithScope(context.Background())
k1, ok1 := jscriptx.ScopeKeyOf(c1)
k2, ok2 := jscriptx.ScopeKeyOf(c2)
if !ok1 || !ok2 || k1 == "" || k2 == "" {
t.Fatalf("没生成 key: %q %q", k1, k2)
}
if k1 == k2 {
t.Error("两次应该生成不同的 key")
}
if _, ok := jscriptx.ScopeKeyOf(context.Background()); ok {
t.Error("没有作用域时不该返回 key")
}
}
// ScopeExtensionOf 在手上没有原对象时把扩展取回来。
func TestScope_取回扩展(t *testing.T) {
st := testext.New()
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st))
got := jscriptx.ScopeExtensionOf(ctx, "store")
if got != jscriptx.Extension(st) {
t.Errorf("取回来的不是同一个: %#v", got)
}
if jscriptx.ScopeExtensionOf(ctx, "nope") != nil {
t.Error("不存在的扩展该返回 nil")
}
if jscriptx.ScopeExtensionOf(context.Background(), "store") != nil {
t.Error("没有作用域时该返回 nil")
}
}
// 作用域实例重建 VM 时,要回到它**创建时**那个作用域。
//
// Instance 的 VM 超时后会被丢弃,下次调用重建(见 Resets)。重建走的是 i.scope
// 不是当次调用 ctx 里的——对一个作用域实例调 Call(context.Background()) 不该把
// 它的扩展弄丢。
//
// 这条守的是 newVM 的 scope 参数为什么是显式传的:改成从 ctx 嗅探就会破坏它。
func TestScope_实例重建VM后仍在原作用域(t *testing.T) {
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
s := mustCompile(t, e, "s.ts", `
export default class S {
spin() { while (true) {} }
seeStore() { return typeof store }
}
`)
st := testext.New()
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st))
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
if got, _ := obj.Call(ctx, "seeStore"); got != "object" {
t.Fatalf("一开始就没看见扩展: %#v", got)
}
// 超时会让实例丢掉 VM,下次调用重建
if _, err := obj.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) {
t.Fatalf("该超时: %v", err)
}
if obj.Resets() != 1 {
t.Fatalf("VM 没被丢弃,这条测不到重建(Resets=%d", obj.Resets())
}
// 用**不带作用域**的 ctx 触发重建:实例仍属于原来那个作用域
got, err := obj.Call(context.Background(), "seeStore")
if err != nil {
t.Fatal(err)
}
if got != "object" {
t.Errorf("重建之后扩展丢了(%#v)——实例被当成了无作用域的", got)
}
}