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:
@@ -0,0 +1,237 @@
|
||||
package jscriptx_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.fsdpf.net/go/jscriptx"
|
||||
)
|
||||
|
||||
const objScript = `
|
||||
export default class DeviceHandler {
|
||||
constructor(deviceId, model) {
|
||||
this.deviceId = deviceId
|
||||
this.model = model
|
||||
this.count = 0
|
||||
}
|
||||
info() { return this.deviceId + "/" + this.model }
|
||||
onMessage(payload) { this.count++; return this.count }
|
||||
spin() { while (true) {} }
|
||||
}
|
||||
`
|
||||
|
||||
// 最基本的:new 出对象,当普通 Go 对象用。
|
||||
func TestNew_当对象用(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
|
||||
obj, err := s.New(context.Background(), "dev-1", "温控器")
|
||||
if err != nil {
|
||||
t.Fatalf("New 失败: %v", err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
got, err := obj.Call(ctx, "info")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "dev-1/温控器" {
|
||||
t.Errorf("info = %#v", got)
|
||||
}
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
got, err := obj.Call(ctx, "onMessage", "p")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.(int64) != int64(i) {
|
||||
t.Errorf("第 %d 次 = %#v", i, got)
|
||||
}
|
||||
}
|
||||
if obj.Calls() != 4 {
|
||||
t.Errorf("Calls = %d, want 4", obj.Calls())
|
||||
}
|
||||
if obj.Name() != "device.js" {
|
||||
t.Errorf("Name = %q", obj.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// 多个实例互相独立。
|
||||
func TestNew_实例互相独立(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
ctx := context.Background()
|
||||
|
||||
a, err := s.New(ctx, "dev-A", "型号A")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer a.Close()
|
||||
b, err := s.New(ctx, "dev-B", "型号B")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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.(int64) != 1 {
|
||||
t.Errorf("B 被 A 污染: %#v", got)
|
||||
}
|
||||
if got, _ := a.Call(ctx, "info"); got != "dev-A/型号A" {
|
||||
t.Errorf("A 的构造参数不对: %#v", got)
|
||||
}
|
||||
if got, _ := b.Call(ctx, "info"); got != "dev-B/型号B" {
|
||||
t.Errorf("B 的构造参数不对: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// constructor 抛异常要在 New 就报出来,不用等到第一次调用。
|
||||
func TestNew_构造失败当场报错(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "bad.js", `
|
||||
export default class H {
|
||||
constructor(cfg) {
|
||||
if (!cfg) { throw new Error("缺少配置") }
|
||||
}
|
||||
run() { return 1 }
|
||||
}
|
||||
`)
|
||||
|
||||
_, err := s.New(context.Background()) // 故意不传构造参数
|
||||
if err == nil {
|
||||
t.Fatal("New 就该报错")
|
||||
}
|
||||
var jsErr *jscriptx.Error
|
||||
if !errors.As(err, &jsErr) {
|
||||
t.Fatalf("错误类型不对: %T", err)
|
||||
}
|
||||
if jsErr.Func != "constructor" || !strings.Contains(jsErr.Msg, "缺少配置") {
|
||||
t.Errorf("错误信息不对: %+v", jsErr)
|
||||
}
|
||||
t.Logf("%v", err)
|
||||
}
|
||||
|
||||
func TestNew_Close后拒绝调用(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
ctx := context.Background()
|
||||
|
||||
obj, _ := s.New(ctx, "dev-1", "温控器")
|
||||
if _, err := obj.Call(ctx, "info"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
obj.Close()
|
||||
if _, err := obj.Call(ctx, "info"); !errors.Is(err, jscriptx.ErrClosed) {
|
||||
t.Errorf("want ErrClosed, got %v", err)
|
||||
}
|
||||
obj.Close() // 重复关闭不该 panic
|
||||
}
|
||||
|
||||
// 同一个实例被并发调用时必须串行。
|
||||
func TestNew_同实例调用串行(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "counter.js", `
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 脚本出错会重建实例,状态归零,Resets 能查到。
|
||||
func TestNew_出错后重建(t *testing.T) {
|
||||
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
ctx := context.Background()
|
||||
|
||||
obj, _ := s.New(ctx, "dev-1", "温控器")
|
||||
defer obj.Close()
|
||||
obj.Call(ctx, "onMessage", "x")
|
||||
obj.Call(ctx, "onMessage", "x")
|
||||
|
||||
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, "onMessage", "x")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.(int64) != 1 {
|
||||
t.Errorf("重建后状态应该归零, got %#v", got)
|
||||
}
|
||||
// 构造参数会重新传一遍,所以 info 仍然正确
|
||||
if got, _ := obj.Call(ctx, "info"); got != "dev-1/温控器" {
|
||||
t.Errorf("重建后构造参数丢了: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_Has(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
obj, _ := s.New(context.Background(), "d", "m")
|
||||
defer obj.Close()
|
||||
|
||||
if !obj.Has("onMessage") || !obj.Has("info") {
|
||||
t.Error("Has 认不出实例方法")
|
||||
}
|
||||
if obj.Has("nope") {
|
||||
t.Error("Has 不该认出不存在的方法")
|
||||
}
|
||||
}
|
||||
|
||||
// 脚本关闭后,已经 new 出来的实例也不能再用。
|
||||
func TestNew_脚本关闭后失效(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
obj, _ := s.New(context.Background(), "d", "m")
|
||||
|
||||
s.Close()
|
||||
if _, err := obj.Call(context.Background(), "info"); !errors.Is(err, jscriptx.ErrClosed) {
|
||||
t.Errorf("want ErrClosed, got %v", err)
|
||||
}
|
||||
if _, err := s.New(context.Background(), "d", "m"); !errors.Is(err, jscriptx.ErrClosed) {
|
||||
t.Errorf("关闭的脚本不该能 New, got %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user