Files
jscriptx/entry_test.go
T
what 7f484d7a71 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":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
2026-09-05 22:13:57 +08:00

214 lines
5.4 KiB
Go

package jscriptx_test
import (
"context"
"errors"
"strings"
"testing"
"time"
"git.fsdpf.net/go/jscriptx"
)
// 所有脚本都要经 esbuild 打包,入口只有一条路:模块导出。
// 这里把各种导出形态过一遍。
func TestEntry_各种导出形态(t *testing.T) {
cases := []struct {
name string
src string
fn string
args []any
want any
newObj bool // 需要先 New 出实例(导出的是 class)
}{
{
name: "export default class",
src: `export default class DeviceHandler {
constructor(id) { this.id = id }
info() { return "class:" + this.id }
}`,
fn: "info", newObj: true, args: []any{"d1"}, want: "class:d1",
},
{
name: "export default 匿名 class",
src: `export default class {
constructor(id) { this.id = id }
info() { return "匿名:" + this.id }
}`,
fn: "info", newObj: true, args: []any{"d2"}, want: "匿名:d2",
},
{
name: "export default 实例",
src: `class H { info() { return "实例" } }
export default new H()`,
fn: "info", want: "实例",
},
{
name: "export default 对象字面量",
src: `export default {
info: function () { return "对象" }
}`,
fn: "info", want: "对象",
},
{
name: "export default 具名函数",
src: `export default function handler(x) { return x * 2 }`,
fn: jscriptx.DefaultFunc, args: []any{21}, want: int64(42),
},
{
name: "export default 箭头函数",
src: `export default (x) => x + 1`,
fn: jscriptx.DefaultFunc, args: []any{41}, want: int64(42),
},
{
name: "只有命名导出",
src: `export function Options() { return "opts" }
export function Version() { return "v1" }`,
fn: "Options", want: "opts",
},
{
name: "继承来的方法也找得到",
src: `class Base { base() { return "base" } }
export default class Derived extends Base {
own() { return "own" }
}`,
fn: "base", newObj: true, want: "base",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "mod.ts", c.src)
ctx := context.Background()
var got any
var err error
if c.newObj {
obj, e2 := s.New(ctx, c.args...)
if e2 != nil {
t.Fatalf("New 失败: %v", e2)
}
defer obj.Close()
got, err = obj.Call(ctx, c.fn)
} else {
got, err = s.Call(ctx, c.fn, c.args...)
}
if err != nil {
t.Fatalf("调用失败: %v", err)
}
if got != c.want {
t.Errorf("got = %#v, want %#v", got, c.want)
}
})
}
}
// TypeScript 直接能写。
func TestEntry_TypeScript(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "ts.ts", `
interface Cfg { name: string }
export default class C {
private count: number = 0
constructor(private cfg: Cfg) {}
Bump(): number { return ++this.count }
Name(): string { return this.cfg.name }
}
`)
ctx := context.Background()
obj, err := s.New(ctx, map[string]any{"name": "配置A"})
if err != nil {
t.Fatal(err)
}
defer obj.Close()
if got, _ := obj.Call(ctx, "Name"); got != "配置A" {
t.Errorf("Name = %#v", got)
}
if got, _ := obj.Call(ctx, "Bump"); got.(int64) != 1 {
t.Errorf("Bump = %#v", got)
}
}
// 不写 export 的脚本会被打包器摇空,报错要说清楚这一点。
func TestEntry_没有导出时报错有指引(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "noexport.js", `function handle(x) { return x * 2 }`)
_, err := s.Call(context.Background(), "handle", 1)
if !errors.Is(err, jscriptx.ErrFuncNotFound) {
t.Fatalf("want ErrFuncNotFound, got %v", err)
}
var jsErr *jscriptx.Error
errors.As(err, &jsErr)
for _, want := range []string{"没有任何导出", "export"} {
if !strings.Contains(jsErr.Msg, want) {
t.Errorf("报错里没有 %q: %s", want, jsErr.Msg)
}
}
t.Logf("%s", jsErr.Msg)
}
// constructor 抛异常要报清楚是构造阶段失败。
func TestEntry_构造失败(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "bad.ts", `
export default class H {
constructor(cfg) {
if (!cfg) { throw new Error("缺少配置") }
this.cfg = cfg
}
run() { return this.cfg }
}
`)
_, err := s.New(context.Background()) // 故意不传构造参数
if err == nil {
t.Fatal("构造失败应该报错")
}
var jsErr *jscriptx.Error
if !errors.As(err, &jsErr) {
t.Fatalf("错误类型不对: %T", err)
}
if jsErr.Func != "constructor" {
t.Errorf("应该指明是 constructor 出错: %+v", jsErr)
}
if !strings.Contains(jsErr.Msg, "缺少配置") {
t.Errorf("没带上脚本的错误信息: %q", jsErr.Msg)
}
}
// 池化调用下,实例状态 = VM 状态,换了 VM 就归零,只能当缓存。
func TestEntry_池化下实例状态会丢(t *testing.T) {
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
s := mustCompile(t, e, "counter.ts", `
export default class Counter {
constructor() { this.count = 0 }
bump() { return ++this.count }
spin() { while (true) {} }
}
`)
ctx := context.Background()
if got, _ := s.Call(ctx, "bump"); got.(int64) != 1 {
t.Fatalf("got %#v", got)
}
if got, _ := s.Call(ctx, "bump"); got.(int64) != 2 {
t.Fatalf("同一个 VM 内应该累加, got %#v", got)
}
if _, err := s.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) {
t.Fatalf("want ErrTimeout, got %v", err)
}
got, err := s.Call(ctx, "bump")
if err != nil {
t.Fatal(err)
}
if got.(int64) != 1 {
t.Errorf("池化模式换了 VM,实例状态应该归零,got %#v", got)
}
}