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:
+228
@@ -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" } }`
|
||||
|
||||
// 不实现 Prepared:Engine 负责打包,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)
|
||||
}
|
||||
|
||||
// 实现了 Prepared:Engine 跳过打包,原始 ESM 直接交给 goja 就会语法错误
|
||||
e2 := newEngine(t, jscriptx.WithLoader(preparedLoader{esmSource}))
|
||||
if _, err := e2.Script("x"); err == nil {
|
||||
t.Fatal("声称已打包却给原始 ESM,应该编译失败——说明 Prepared 没被识别,源码被重复打包了")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user