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:
2026-09-05 22:13:57 +08:00
parent 6ce9b483fd
commit 7f484d7a71
20 changed files with 4660 additions and 0 deletions
+228
View File
@@ -0,0 +1,228 @@
package jscriptx_test
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"testing"
"git.fsdpf.net/go/jscriptx"
)
// testLoader 是测试用的 Loader 实现:本库只定义接口,实现由调用方提供。
type testLoader struct {
mu sync.Mutex
sources map[string]string
version map[string]string
calls int
}
func newTestLoader() *testLoader {
return &testLoader{sources: map[string]string{}, version: map[string]string{}}
}
func (l *testLoader) put(name, src, version string) {
l.mu.Lock()
defer l.mu.Unlock()
l.sources[name] = src
l.version[name] = version
}
func (l *testLoader) Load(name string) (string, string, error) {
l.mu.Lock()
defer l.mu.Unlock()
l.calls++
src, ok := l.sources[name]
if !ok {
return "", "", fmt.Errorf("%w: %s", jscriptx.ErrScriptNotFound, name)
}
return src, l.version[name], nil
}
func (l *testLoader) callCount() int {
l.mu.Lock()
defer l.mu.Unlock()
return l.calls
}
func TestLoader_按名字加载并缓存(t *testing.T) {
loader := newTestLoader()
loader.put("greet", `export function hi() { return "v1" }`, "v1")
e := newEngine(t, jscriptx.WithLoader(loader))
for i := 0; i < 3; i++ {
s, err := e.Script("greet")
if err != nil {
t.Fatal(err)
}
got, err := s.Call(context.Background(), "hi")
if err != nil {
t.Fatal(err)
}
if got != "v1" {
t.Fatalf("got %#v", got)
}
}
if loader.callCount() != 1 {
t.Errorf("没开自动重载时应该只加载一次,实际 %d 次", loader.callCount())
}
}
func TestLoader_脚本不存在(t *testing.T) {
e := newEngine(t, jscriptx.WithLoader(newTestLoader()))
_, err := e.Script("nope")
if !errors.Is(err, jscriptx.ErrScriptNotFound) {
t.Fatalf("want ErrScriptNotFound, got %v", err)
}
}
func TestLoader_没配Loader也没注册(t *testing.T) {
e := newEngine(t)
if _, err := e.Script("nope"); !errors.Is(err, jscriptx.ErrScriptNotFound) {
t.Fatalf("want ErrScriptNotFound, got %v", err)
}
}
// 热更新:版本号变了才重新编译,没变就复用编译产物和 VM 池。
func TestLoader_自动热更新(t *testing.T) {
loader := newTestLoader()
loader.put("greet", `export function hi() { return "v1" }`, "v1")
e := newEngine(t, jscriptx.WithLoader(loader), jscriptx.WithAutoReload(true))
s1, err := e.Script("greet")
if err != nil {
t.Fatal(err)
}
got, _ := s1.Call(context.Background(), "hi")
if got != "v1" {
t.Fatalf("got %#v", got)
}
// 版本号没变:应该拿到同一个 Script
s2, err := e.Script("greet")
if err != nil {
t.Fatal(err)
}
if s1 != s2 {
t.Error("版本号没变时不该重新编译")
}
// 版本号变了:重新编译,新调用走新逻辑
loader.put("greet", `export function hi() { return "v2" }`, "v2")
s3, err := e.Script("greet")
if err != nil {
t.Fatal(err)
}
if s3 == s1 {
t.Fatal("版本号变了应该重新编译")
}
if got, _ := s3.Call(context.Background(), "hi"); got != "v2" {
t.Errorf("热更新后 got %#v, want v2", got)
}
if s3.Version() != "v2" {
t.Errorf("Version() = %q", s3.Version())
}
}
// Loader 不提供版本号时,退化成按源码哈希判断有没有变。
func TestLoader_不提供版本号时按源码哈希(t *testing.T) {
loader := newTestLoader()
loader.put("x", `export function hi() { return 1 }`, "")
e := newEngine(t, jscriptx.WithLoader(loader), jscriptx.WithAutoReload(true))
s1, err := e.Script("x")
if err != nil {
t.Fatal(err)
}
s2, err := e.Script("x")
if err != nil {
t.Fatal(err)
}
if s1 != s2 {
t.Error("源码没变不该重新编译")
}
loader.put("x", `export function hi() { return 2 }`, "")
s3, err := e.Script("x")
if err != nil {
t.Fatal(err)
}
if s3 == s1 {
t.Error("源码变了应该重新编译")
}
}
// LoaderFunc:让普通函数直接当 Loader 用。
func TestLoaderFunc(t *testing.T) {
scripts := map[string]string{
"greet": `export function hi() { return "来自 LoaderFunc" }`,
}
loader := jscriptx.LoaderFunc(func(name string) (string, string, error) {
src, ok := scripts[name]
if !ok {
return "", "", errors.New("没有这个脚本: " + jscriptx.ErrScriptNotFound.Error() + " " + name)
}
return src, "v1", nil
})
e := newEngine(t, jscriptx.WithLoader(loader))
s, err := e.Script("greet")
if err != nil {
t.Fatalf("LoaderFunc 没生效: %v", err)
}
got, err := s.Call(context.Background(), "hi")
if err != nil {
t.Fatal(err)
}
if got != "来自 LoaderFunc" {
t.Errorf("got = %#v", got)
}
if _, err := e.Script("nope"); err == nil || !strings.Contains(err.Error(), "nope") {
t.Errorf("找不到脚本时该把 Loader 的错误带出来: %v", err)
}
}
// preparedLoader 声称源码已经打过包了。
type preparedLoader struct{ src string }
func (l preparedLoader) Load(string) (string, string, error) { return l.src, "v1", nil }
func (l preparedLoader) Prepared() bool { return true }
// plainLoader 不实现 Prepared,源码交给 Engine 打包。
type plainLoader struct{ src string }
func (l plainLoader) Load(string) (string, string, error) { return l.src, "v1", nil }
// Prepared 决定 Engine 打不打包这个 Loader 给的源码。
//
// 用一段原始 ESM 源码就能把两条路分辨开:Engine 打包的话 export 会被抹平,
// 不打包直接丢给 goja 的话,export 是保留字,编译当场失败。
func TestLoader_Prepared决定是否打包(t *testing.T) {
const esmSource = `export default class H { hi() { return "ok" } }`
// 不实现 PreparedEngine 负责打包,ESM 源码能用
e1 := newEngine(t, jscriptx.WithLoader(plainLoader{esmSource}))
s, err := e1.Script("x")
if err != nil {
t.Fatalf("Engine 应该打包这段源码: %v", err)
}
obj, err := s.New(context.Background())
if err != nil {
t.Fatal(err)
}
defer obj.Close()
if got, _ := obj.Call(context.Background(), "hi"); got != "ok" {
t.Errorf("got = %#v", got)
}
// 实现了 PreparedEngine 跳过打包,原始 ESM 直接交给 goja 就会语法错误
e2 := newEngine(t, jscriptx.WithLoader(preparedLoader{esmSource}))
if _, err := e2.Script("x"); err == nil {
t.Fatal("声称已打包却给原始 ESM,应该编译失败——说明 Prepared 没被识别,源码被重复打包了")
}
}