Files
jscriptx/loader_test.go
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

229 lines
6.1 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 没被识别,源码被重复打包了")
}
}