改完之后「某个行为在哪测」就是查同名文件:
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
挪成外部测试,挪不动。
290 lines
7.4 KiB
Go
290 lines
7.4 KiB
Go
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)
|
||
}
|
||
}
|