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,两种写法都通过,等于没测)。
480 lines
12 KiB
Go
480 lines
12 KiB
Go
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)
|
||
}
|
||
}
|