Files
jscriptx/scope_test.go
T
what 1f25425762 refactor: 取 VM 的三条路径合一,newVM 成为唯一入口
borrow(池化)、ensure(实例)、borrowStatic(静态)各自写了一遍「取作用域 →
拼 extra → 建 Runtime → load」。borrowStatic 甚至把 newVM 逐行抄了一遍,只为把
noInstance 传成 true。

结果就是规则会漏:上一个 commit 修的「borrowStatic 没标 scoped」正是这么来的。
现在三条都走 newVM,作用域注入和 scoped 标记只有一份。

顺带删掉两处无意义的间接:

  loadInto     s.load(ctx, rt, ctorArgs, false) 的单行包装,只被 newVM 调一次
  newRuntime   它的 scope 参数两个调用点都传 s.name,而注释描述的「会话 VM 用
               会话 key」这条路径根本没实现(作用域 key 走的是 vmGlobals)。
               参数名还跟同包的 type scope struct 撞名

**scope 是显式参数,不是从 ctx 嗅的**——这一点差点被我改坏:Instance 重建 VM
时要回到它**创建时**那个作用域,而不是当次调用 ctx 里的。对一个作用域实例调
Call(context.Background()) 不该把它的扩展弄丢。加了
TestScope_实例重建VM后仍在原作用域 守这条,用超时逼出 VM 重建(第一版用抛异常,
那不会丢 VM,两种写法都通过,等于没测)。
2026-09-10 15:25:12 +08:00

480 lines
12 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"
"fmt"
"sync"
"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) {} }
}
`
// 实例独占 VM,this.xxx 在它活着期间跨调用保持。
func TestInstance_状态跨调用保持(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "device.ts", deviceScript)
ctx := context.Background()
obj, err := s.New(ctx, "device-A", "温控器")
if err != nil {
t.Fatal(err)
}
defer obj.Close()
for i, want := range []string{"device-A#1:m1", "device-A#2:m2", "device-A#3:m3"} {
got, err := obj.Call(ctx, "onMessage", fmt.Sprintf("m%d", i+1))
if err != nil {
t.Fatalf("第 %d 条失败: %v", i+1, err)
}
if got != want {
t.Errorf("第 %d 条 = %#v, want %q", i+1, got, want)
}
}
}
// 不同实例互不相干,这是并发的基础。
func TestInstance_互相隔离(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "device.ts", deviceScript)
ctx := context.Background()
a, _ := s.New(ctx, "device-A", "型号A")
defer a.Close()
b, _ := s.New(ctx, "device-B", "型号B")
defer b.Close()
a.Call(ctx, "onMessage", "x")
a.Call(ctx, "onMessage", "x")
got, err := b.Call(ctx, "onMessage", "x")
if err != nil {
t.Fatal(err)
}
if got != "device-B#1:x" {
t.Errorf("B 被 A 污染: %#v", got)
}
}
// 作用域的核心用途:同一个 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")
}
}
// 同一个实例的调用串行,保护 this.xxx 不错乱。
func TestInstance_调用串行(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "counter.ts", `
export default class C {
constructor() { this.n = 0 }
bump() { this.n++; return this.n }
total() { return this.n }
}
`)
obj, err := s.New(context.Background())
if err != nil {
t.Fatal(err)
}
defer obj.Close()
const goroutines, per = 8, 100
var wg sync.WaitGroup
for g := 0; g < goroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < per; i++ {
if _, err := obj.Call(context.Background(), "bump"); err != nil {
t.Errorf("%v", err)
return
}
}
}()
}
wg.Wait()
got, _ := obj.Call(context.Background(), "total")
if got.(int64) != goroutines*per {
t.Errorf("串行保证失效, total = %#v, want %d", got, goroutines*per)
}
}
// 多个实例并发跑,各自的状态正确。
func TestInstance_多实例并发(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "device.ts", deviceScript)
const devices, messages = 16, 30
var wg sync.WaitGroup
for d := 0; d < devices; d++ {
id := fmt.Sprintf("device-%d", d)
wg.Add(1)
go func(id string) {
defer wg.Done()
obj, err := s.New(context.Background(), id, "型号")
if err != nil {
t.Errorf("%v", err)
return
}
defer obj.Close()
for m := 0; m < messages; m++ {
got, err := obj.Call(context.Background(), "onMessage", "m")
if err != nil {
t.Errorf("%s: %v", id, err)
return
}
want := fmt.Sprintf("%s#%d:m", id, m+1)
if got != want {
t.Errorf("%s 第 %d 条 = %#v, want %q", id, m+1, got, want)
return
}
}
}(id)
}
wg.Wait()
}
// 脚本出错会丢 this 上的状态,但扩展里的东西还在。
func TestInstance_出错后状态归零(t *testing.T) {
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
s := mustCompile(t, e, "mix.ts", `
export default class M {
constructor() { this.n = 0 }
bump() { store.Incr("kept", 1); return ++this.n }
spin() { while (true) {} }
}
`)
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()
obj.Call(ctx, "bump")
obj.Call(ctx, "bump")
if _, err := obj.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) {
t.Fatalf("want ErrTimeout, got %v", err)
}
if obj.Resets() != 1 {
t.Errorf("Resets = %d, want 1", obj.Resets())
}
got, err := obj.Call(ctx, "bump")
if err != nil {
t.Fatal(err)
}
if got.(int64) != 1 {
t.Errorf("this 上的状态应该归零, got %#v", got)
}
// 扩展在 Go 侧,不受 VM 重建影响
if n := toNumber(t, st.Get("kept")); n != 3 {
t.Errorf("扩展里的状态不该丢, got %v want 3", n)
}
}
// 脚本关闭后实例失效。
func TestInstance_脚本关闭后失效(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "device.ts", deviceScript)
obj, _ := s.New(context.Background(), "d", "m")
s.Close()
if _, err := obj.Call(context.Background(), "onMessage", "x"); !errors.Is(err, jscriptx.ErrClosed) {
t.Errorf("want ErrClosed, got %v", err)
}
}
// toNumber 把脚本返回的数字统一成 float64:goja 会按数值是否为整数
// 导出成 int64 或 float64,测试里不必纠结这个差异。
func toNumber(t *testing.T, v any) float64 {
t.Helper()
switch n := v.(type) {
case int64:
return float64(n)
case float64:
return n
default:
t.Fatalf("不是数字: %#v", v)
return 0
}
}
// 作用域实例重建 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)
}
}