除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:
- 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":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
420 lines
12 KiB
Go
420 lines
12 KiB
Go
package jscriptx
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// srcLoader 是个最简单的内存 Loader,用来搭叠层测试。
|
|
type srcLoader struct {
|
|
src map[string]string
|
|
ver map[string]string
|
|
prepared bool
|
|
names []string
|
|
rebuilt int
|
|
fail error // 非 ErrScriptNotFound 的故障
|
|
}
|
|
|
|
func (l *srcLoader) Load(name string) (string, string, error) {
|
|
if l.fail != nil {
|
|
return "", "", l.fail
|
|
}
|
|
s, ok := l.src[name]
|
|
if !ok {
|
|
return "", "", fmt.Errorf("%w: %s", ErrScriptNotFound, name)
|
|
}
|
|
return s, l.ver[name], nil
|
|
}
|
|
func (l *srcLoader) Prepared() bool { return l.prepared }
|
|
func (l *srcLoader) Names() []string { return l.names }
|
|
func (l *srcLoader) Rebuild() error { l.rebuilt++; return nil }
|
|
|
|
func clsOf(v string) string {
|
|
return "export default class C { V() { return " + `"` + v + `"` + " } }"
|
|
}
|
|
|
|
func TestOverlay_后面的盖前面的(t *testing.T) {
|
|
base := &srcLoader{src: map[string]string{"A": clsOf("base-A"), "B": clsOf("base-B")}}
|
|
over := &srcLoader{src: map[string]string{"A": clsOf("over-A"), "C": clsOf("over-C")}}
|
|
|
|
o, err := Overlay(base, over)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range []struct{ name, want string }{
|
|
{"A", "over-A"}, // 两层都有,取后面那层
|
|
{"B", "base-B"}, // 只有前面那层有
|
|
{"C", "over-C"}, // 只有后面那层有
|
|
} {
|
|
src, _, err := o.Load(tc.name)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.name, err)
|
|
}
|
|
if !strings.Contains(src, tc.want) {
|
|
t.Errorf("%s 取到的不是 %s: %s", tc.name, tc.want, src)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOverlay_都没有时报脚本不存在(t *testing.T) {
|
|
o, err := Overlay(&srcLoader{}, &srcLoader{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, _, err = o.Load("X")
|
|
if !errors.Is(err, ErrScriptNotFound) {
|
|
t.Fatalf("要能被 ErrScriptNotFound 匹配上: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "X") {
|
|
t.Errorf("报错里要带脚本名: %v", err)
|
|
}
|
|
}
|
|
|
|
// 某一层出故障(不是"脚本不存在")时要立刻报出来,
|
|
// 不能被下面那层的结果悄悄盖掉——否则线上换了实现都不知道。
|
|
func TestOverlay_故障不被下层掩盖(t *testing.T) {
|
|
boom := errors.New("数据库连不上")
|
|
base := &srcLoader{src: map[string]string{"A": clsOf("base-A")}}
|
|
over := &srcLoader{fail: boom}
|
|
|
|
o, _ := Overlay(base, over)
|
|
_, _, err := o.Load("A")
|
|
if !errors.Is(err, boom) {
|
|
t.Fatalf("故障被吞了: %v", err)
|
|
}
|
|
}
|
|
|
|
// 版本号要带上层号:覆盖层撤掉后落回下层,两层版本号万一撞上,
|
|
// 不带层号引擎就看不出脚本已经换了人。
|
|
func TestOverlay_版本号带层号(t *testing.T) {
|
|
base := &srcLoader{
|
|
src: map[string]string{"A": clsOf("base-A")},
|
|
ver: map[string]string{"A": "v1"},
|
|
}
|
|
over := &srcLoader{
|
|
src: map[string]string{"A": clsOf("over-A")},
|
|
ver: map[string]string{"A": "v1"}, // 故意跟 base 撞
|
|
}
|
|
|
|
o, _ := Overlay(base, over)
|
|
_, v1, _ := o.Load("A")
|
|
|
|
delete(over.src, "A") // 撤掉覆盖
|
|
_, v2, _ := o.Load("A")
|
|
|
|
if v1 == v2 {
|
|
t.Fatalf("落回下层后版本号没变(都是 %q),引擎会继续用旧的编译结果", v1)
|
|
}
|
|
}
|
|
|
|
// 成员不给版本号时要原样透传空串,让引擎去按源码算哈希。
|
|
func TestOverlay_空版本号原样透传(t *testing.T) {
|
|
o, _ := Overlay(&srcLoader{src: map[string]string{"A": clsOf("a")}})
|
|
if _, v, _ := o.Load("A"); v != "" {
|
|
t.Errorf("版本号该是空的,拿到 %q", v)
|
|
}
|
|
}
|
|
|
|
func TestOverlay_Prepared必须一致(t *testing.T) {
|
|
yes := &srcLoader{prepared: true}
|
|
no := &srcLoader{prepared: false}
|
|
|
|
if _, err := Overlay(yes, no); err == nil {
|
|
t.Error("混着来该报错")
|
|
}
|
|
if _, err := Overlay(yes, yes); err != nil {
|
|
t.Errorf("都是打好包的该通过: %v", err)
|
|
}
|
|
o, err := Overlay(no, no)
|
|
if err != nil {
|
|
t.Fatalf("都是原始源码该通过: %v", err)
|
|
}
|
|
if o.Prepared() {
|
|
t.Error("Prepared 该跟成员一致")
|
|
}
|
|
}
|
|
|
|
func TestOverlay_参数校验(t *testing.T) {
|
|
if _, err := Overlay(); err == nil {
|
|
t.Error("一个都不给该报错")
|
|
}
|
|
if _, err := Overlay(&srcLoader{}, nil); err == nil {
|
|
t.Error("给 nil 该报错")
|
|
}
|
|
}
|
|
|
|
func TestOverlay_Names去重排序(t *testing.T) {
|
|
base := &srcLoader{names: []string{"B/b", "A/a"}}
|
|
over := &srcLoader{names: []string{"A/a", "C/c"}}
|
|
plain := LoaderFunc(func(string) (string, string, error) { return "", "", nil }) // 没有 Names
|
|
|
|
o, _ := Overlay(base, over, plain)
|
|
got := strings.Join(o.Names(), ",")
|
|
if got != "A/a,B/b,C/c" {
|
|
t.Errorf("Names() = %s", got)
|
|
}
|
|
}
|
|
|
|
func TestOverlay_Rebuild走遍每一层(t *testing.T) {
|
|
base, over := &srcLoader{}, &srcLoader{}
|
|
o, _ := Overlay(base, over, LoaderFunc(func(string) (string, string, error) { return "", "", nil }))
|
|
if err := o.Rebuild(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if base.rebuilt != 1 || over.rebuilt != 1 {
|
|
t.Errorf("每层都该重建一次: base=%d over=%d", base.rebuilt, over.rebuilt)
|
|
}
|
|
}
|
|
|
|
// WithLoader 给多个时自动叠起来,跟 Overlay 的语义一致。
|
|
func TestWithLoader_多个自动叠层(t *testing.T) {
|
|
base := &srcLoader{src: map[string]string{"A": clsOf("base-A"), "B": clsOf("base-B")}}
|
|
over := &srcLoader{src: map[string]string{"A": clsOf("over-A")}}
|
|
|
|
e, err := New(WithLoader(base, over))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer e.Close()
|
|
|
|
mustCall := func(name, want string) {
|
|
t.Helper()
|
|
s, err := e.Script(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := s.Call(t.Context(), "V")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got != want {
|
|
t.Errorf("%s = %v, 想要 %s", name, got, want)
|
|
}
|
|
}
|
|
mustCall("A", "over-A")
|
|
mustCall("B", "base-B")
|
|
}
|
|
|
|
// 多次调用 WithLoader 继续往后叠,效果跟一次传多个一样。
|
|
func TestWithLoader_多次调用继续叠(t *testing.T) {
|
|
base := &srcLoader{src: map[string]string{"A": clsOf("base-A")}}
|
|
over := &srcLoader{src: map[string]string{"A": clsOf("over-A")}}
|
|
|
|
e, err := New(WithLoader(base), WithLoader(over))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer e.Close()
|
|
|
|
s, err := e.Script("A")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, _ := s.Call(t.Context(), "V"); got != "over-A" {
|
|
t.Errorf("后面那次该盖住前面的,拿到 %v", got)
|
|
}
|
|
}
|
|
|
|
// 只给一个时不该套上 Overlay,Prepared 之类的行为要原样保留。
|
|
func TestWithLoader_单个不套壳(t *testing.T) {
|
|
l := &srcLoader{prepared: true}
|
|
e, err := New(WithLoader(l))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer e.Close()
|
|
if e.loader != Loader(l) {
|
|
t.Errorf("单个 Loader 该原样用,拿到 %T", e.loader)
|
|
}
|
|
}
|
|
|
|
func TestWithLoader_层间Prepared不一致时New报错(t *testing.T) {
|
|
_, err := New(WithLoader(&srcLoader{prepared: true}, &srcLoader{prepared: false}))
|
|
if err == nil {
|
|
t.Fatal("该报错")
|
|
}
|
|
if !strings.Contains(err.Error(), "Prepared") {
|
|
t.Errorf("报错要说清楚是 Prepared 不一致: %v", err)
|
|
}
|
|
}
|
|
|
|
// tagged 是带版本标签的内存 Loader。
|
|
type tagged struct {
|
|
*srcLoader
|
|
ver string
|
|
}
|
|
|
|
func (t *tagged) Version() string { return t.ver }
|
|
|
|
func TestVersion_点名要哪一层(t *testing.T) {
|
|
v1 := &tagged{&srcLoader{src: map[string]string{"A": clsOf("v1-A"), "OnlyV1": clsOf("x")}}, "v1"}
|
|
v2 := &tagged{&srcLoader{src: map[string]string{"A": clsOf("v2-A")}}, "v2"}
|
|
|
|
o, err := Overlay(v1, v2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
for _, tc := range []struct{ name, want string }{
|
|
{"A", "v2-A"}, // 不点名:上层盖下层
|
|
{"A@v2", "v2-A"}, // 点名上层
|
|
{"A@v1", "v1-A"}, // 点名下层
|
|
{"OnlyV1@v1", "x"}, // 点名的那层独有的
|
|
} {
|
|
src, _, err := o.Load(tc.name)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", tc.name, err)
|
|
}
|
|
if !strings.Contains(src, tc.want) {
|
|
t.Errorf("%s 该拿到 %s: %s", tc.name, tc.want, src)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 点名是"只认这一层":那层没有就报错,不能掉到别的层去。
|
|
func TestVersion_点名后不回落(t *testing.T) {
|
|
v1 := &tagged{&srcLoader{src: map[string]string{"A": clsOf("v1-A")}}, "v1"}
|
|
v2 := &tagged{&srcLoader{}, "v2"} // 空的
|
|
|
|
o, _ := Overlay(v1, v2)
|
|
_, _, err := o.Load("A@v2")
|
|
if !errors.Is(err, ErrScriptNotFound) {
|
|
t.Fatalf("v2 层没有 A,该报不存在而不是回落到 v1: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVersion_点名不存在的版本(t *testing.T) {
|
|
v1 := &tagged{&srcLoader{src: map[string]string{"A": clsOf("a")}}, "v1"}
|
|
o, _ := Overlay(v1)
|
|
|
|
_, _, err := o.Load("A@v9")
|
|
if !errors.Is(err, ErrScriptNotFound) {
|
|
t.Fatalf("该报不存在: %v", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "v1") {
|
|
t.Errorf("报错该提示有哪些版本: %v", err)
|
|
}
|
|
}
|
|
|
|
// 点名不同的层,版本号里的层号要跟着变,两份实现才能各自缓存。
|
|
func TestVersion_点名时版本号带层号(t *testing.T) {
|
|
v1 := &tagged{&srcLoader{
|
|
src: map[string]string{"A": clsOf("v1-A")},
|
|
ver: map[string]string{"A": "same"},
|
|
}, "v1"}
|
|
v2 := &tagged{&srcLoader{
|
|
src: map[string]string{"A": clsOf("v2-A")},
|
|
ver: map[string]string{"A": "same"}, // 故意撞
|
|
}, "v2"}
|
|
|
|
o, _ := Overlay(v1, v2)
|
|
_, a, _ := o.Load("A@v1")
|
|
_, b, _ := o.Load("A@v2")
|
|
if a == b {
|
|
t.Fatalf("两层的版本号该区分开,都是 %q", a)
|
|
}
|
|
}
|
|
|
|
func TestVersion_标签校验(t *testing.T) {
|
|
a := &tagged{&srcLoader{}, "v1"}
|
|
b := &tagged{&srcLoader{}, "v1"}
|
|
if _, err := Overlay(a, b); err == nil {
|
|
t.Error("标签重名该报错")
|
|
}
|
|
bad := &tagged{&srcLoader{}, "v@1"}
|
|
if _, err := Overlay(bad); err == nil {
|
|
t.Error("标签里带分隔符该报错")
|
|
}
|
|
}
|
|
|
|
func TestVersion_Tag给任意Loader贴标签(t *testing.T) {
|
|
plain := &srcLoader{src: map[string]string{"A": clsOf("plain-A")}}
|
|
other := &srcLoader{src: map[string]string{"A": clsOf("other-A")}}
|
|
|
|
o, err := Overlay(plain, Tag("hotfix", other))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
src, _, err := o.Load("A@hotfix")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(src, "other-A") {
|
|
t.Errorf("该拿到贴了标签那层的: %s", src)
|
|
}
|
|
if got := strings.Join(o.Versions(), ","); got != "hotfix" {
|
|
t.Errorf("Versions() = %s", got)
|
|
}
|
|
}
|
|
|
|
// 贴标签不该改变底下 Loader 的 Prepared,否则叠层的一致性校验会误判。
|
|
func TestVersion_Tag透传Prepared(t *testing.T) {
|
|
if !isPrepared(Tag("v1", &srcLoader{prepared: true})) {
|
|
t.Error("底下是 prepared 的,贴完标签该还是")
|
|
}
|
|
if isPrepared(Tag("v1", &srcLoader{prepared: false})) {
|
|
t.Error("底下不是 prepared 的,贴完标签不该变成是")
|
|
}
|
|
// 一层贴了标签一层没贴,Prepared 一致就该能叠
|
|
if _, err := Overlay(&srcLoader{prepared: true}, Tag("v1", &srcLoader{prepared: true})); err != nil {
|
|
t.Errorf("该能叠: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestVersion_拆名字(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
in, bare, ver string
|
|
ok bool
|
|
}{
|
|
{"Foo/Bar@v1", "Foo/Bar", "v1", true},
|
|
{"Foo/Bar", "Foo/Bar", "", false},
|
|
{"@v1", "@v1", "", false}, // 左边空着,不算点名
|
|
{"Foo@", "Foo@", "", false}, // 右边空着,不算点名
|
|
{"a@b@v1", "a@b", "v1", true}, // 用最后一个分隔符
|
|
} {
|
|
bare, ver, ok := splitVersion(tc.in)
|
|
if bare != tc.bare || ver != tc.ver || ok != tc.ok {
|
|
t.Errorf("splitVersion(%q) = %q,%q,%v;想要 %q,%q,%v",
|
|
tc.in, bare, ver, ok, tc.bare, tc.ver, tc.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 走完整链路:从 Engine 点名要某个版本。
|
|
func TestVersion_从Engine点名(t *testing.T) {
|
|
v1 := &tagged{&srcLoader{src: map[string]string{"A": clsOf("v1-A")}}, "v1"}
|
|
v2 := &tagged{&srcLoader{src: map[string]string{"A": clsOf("v2-A")}}, "v2"}
|
|
|
|
e, err := New(WithLoader(v1, v2))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer e.Close()
|
|
|
|
call := func(name string) any {
|
|
t.Helper()
|
|
s, err := e.Script(name)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := s.Call(t.Context(), "V")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return got
|
|
}
|
|
if got := call("A"); got != "v2-A" {
|
|
t.Errorf("不点名该拿到上层: %v", got)
|
|
}
|
|
if got := call("A@v1"); got != "v1-A" {
|
|
t.Errorf("点名 v1 该拿到 v1: %v", got)
|
|
}
|
|
// 两个版本各自缓存,互不干扰
|
|
if got := call("A@v2"); got != "v2-A" {
|
|
t.Errorf("点名 v2 该拿到 v2: %v", got)
|
|
}
|
|
}
|