test: 引擎核心的测试与基准
除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:
- vmstate_test goja 的硬限制:非原始值不是 goroutine 安全的、对象跨不了
Runtime。其中一条是哨兵——goja 哪天允许对象跨 Runtime 了,
它会失败,提醒我们可以简化设计。
- govalue_test Go 的切片/map 进到脚本里长什么样。这不是本库的行为而是
goja 的,但脚本作者照着它写代码,变了会静默走错分支。
- safety_test 并发下坏 VM 不会被别的 goroutine 捡到。
- bench_gobind 脚本碰 Go 对象的单次开销,README 性能一节的数据来源。
missing_global_test 里有一条断言 typeof setTimeout === "undefined":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
This commit is contained in:
+433
@@ -0,0 +1,433 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user