除了常规用例,有几组是专门钉住「引擎的既定事实」的,改动时会先红:
- 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":
定时器必须保持未定义,库里的特性探测才能正常降级,防止以后有人把桩加回来。
193 lines
5.8 KiB
Go
193 lines
5.8 KiB
Go
package jscriptx_test
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.fsdpf.net/go/jscriptx"
|
||
)
|
||
|
||
func TestNew_全局名校验(t *testing.T) {
|
||
for _, name := range []string{"db.From", "2fast", "有中文", "", "a-b"} {
|
||
if _, err := jscriptx.New(jscriptx.WithGlobal(name, 1)); !errors.Is(err, jscriptx.ErrBadGlobal) {
|
||
t.Errorf("全局名 %q 应该被拒绝,got %v", name, err)
|
||
}
|
||
}
|
||
for _, name := range []string{"db", "req", "_x", "$", "a1"} {
|
||
if _, err := jscriptx.New(jscriptx.WithGlobal(name, 1)); err != nil {
|
||
t.Errorf("全局名 %q 应该被接受: %v", name, err)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 白名单之外的东西脚本一律看不见。
|
||
func TestGlobals_白名单之外看不见(t *testing.T) {
|
||
e := newEngine(t, jscriptx.WithGlobals(map[string]any{
|
||
"allowed": func() string { return "ok" },
|
||
}))
|
||
s := mustCompile(t, e, "wl", `
|
||
export function probe(name) { return typeof globalThis[name] }
|
||
`)
|
||
|
||
for _, tc := range []struct{ name, want string }{
|
||
{"allowed", "function"},
|
||
{"require", "undefined"},
|
||
{"process", "undefined"},
|
||
{"setTimeout", "undefined"},
|
||
{"db", "undefined"},
|
||
} {
|
||
got, err := s.Call(context.Background(), "probe", tc.name)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != tc.want {
|
||
t.Errorf("typeof %s = %v, want %v", tc.name, got, tc.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 注入的命名空间对象必须是只读的:否则同一个 Go map 被所有 VM 共享,
|
||
// 脚本改一下既污染别的 VM 又是数据竞争。
|
||
func TestGlobals_命名空间只读(t *testing.T) {
|
||
ns := map[string]any{
|
||
"helper": map[string]any{"double": func(n int) int { return n * 2 }},
|
||
}
|
||
e := newEngine(t, jscriptx.WithGlobals(ns))
|
||
s := mustCompile(t, e, "readonly", `
|
||
export function tamper() {
|
||
try { helper.double = null } catch (err) { /* 严格模式才会抛,这里吞掉 */ }
|
||
try { helper = null } catch (err) {}
|
||
return typeof helper.double
|
||
}
|
||
`)
|
||
|
||
for i := 0; i < 3; i++ {
|
||
got, err := s.Call(context.Background(), "tamper")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got != "function" {
|
||
t.Fatalf("第 %d 次: 白名单被脚本改掉了: %v", i, got)
|
||
}
|
||
}
|
||
// Go 侧的原始 map 也不能被动过
|
||
if _, ok := ns["helper"].(map[string]any)["double"].(func(int) int); !ok {
|
||
t.Error("Go 侧的白名单 map 被脚本改掉了")
|
||
}
|
||
}
|
||
|
||
// 多返回值约定:这是脚本作者唯一需要额外理解的规则,用测试把它钉死。
|
||
func TestMultiReturn_转换约定(t *testing.T) {
|
||
e := newEngine(t, jscriptx.WithGlobals(map[string]any{
|
||
"one": func() string { return "hello" },
|
||
"okPair": func() (string, error) { return "hello", nil },
|
||
"errPair": func() (string, error) { return "", errors.New("boom") },
|
||
"twoVals": func() (string, int) { return "a", 1 },
|
||
"threeVal": func() (string, int, error) { return "a", 1, nil },
|
||
"threeErr": func() (string, int, error) { return "", 0, errors.New("boom") },
|
||
}))
|
||
s := mustCompile(t, e, "multi", `
|
||
export function run() {
|
||
var out = {}
|
||
out.one = one()
|
||
out.okPair = okPair() // error 为 nil:拿到裸值
|
||
out.twoVals = twoVals().join(",") // 无 error:拿到数组
|
||
out.threeVal = threeVal().join(",") // 末位 error 为 nil:只剩前两个
|
||
try { errPair(); out.errPair = "没抛" } catch (err) { out.errPair = "抛了: " + err.message }
|
||
try { threeErr(); out.threeErr = "没抛" } catch (err) { out.threeErr = "抛了" }
|
||
return out
|
||
}
|
||
`)
|
||
|
||
got, err := s.Call(context.Background(), "run")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
out, _ := got.(map[string]any)
|
||
want := map[string]any{
|
||
"one": "hello",
|
||
"okPair": "hello",
|
||
"twoVals": "a,1",
|
||
"threeVal": "a,1",
|
||
"threeErr": "抛了",
|
||
}
|
||
for k, v := range want {
|
||
if out[k] != v {
|
||
t.Errorf("%s = %#v, want %#v", k, out[k], v)
|
||
}
|
||
}
|
||
if s, _ := out["errPair"].(string); !strings.HasPrefix(s, "抛了") {
|
||
t.Errorf("errPair = %#v,error 非 nil 时应该变成 JS 异常", out["errPair"])
|
||
}
|
||
t.Logf("多返回值转换结果: %v", out)
|
||
}
|
||
|
||
func TestEngine_Reload与Invalidate(t *testing.T) {
|
||
loader := newTestLoader()
|
||
loader.put("x", `export function hi() { return "v1" }`, "v1")
|
||
e := newEngine(t, jscriptx.WithLoader(loader))
|
||
|
||
s1, _ := e.Script("x")
|
||
loader.put("x", `export function hi() { return "v2" }`, "v2")
|
||
|
||
// 没开自动重载,普通取还是旧的
|
||
s2, _ := e.Script("x")
|
||
if s1 != s2 {
|
||
t.Error("没开自动重载不该自己更新")
|
||
}
|
||
|
||
s3, err := e.Reload("x")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got, _ := s3.Call(context.Background(), "hi"); got != "v2" {
|
||
t.Errorf("Reload 后 got %#v", got)
|
||
}
|
||
|
||
// 旧 Script 被顶替后会被关掉,正在进行的调用不受影响、之后的调用报 ErrClosed
|
||
if _, err := s1.Call(context.Background(), "hi"); !errors.Is(err, jscriptx.ErrClosed) {
|
||
t.Errorf("被顶替的旧脚本应该已关闭,got %v", err)
|
||
}
|
||
|
||
e.Invalidate("x")
|
||
if len(e.Names()) != 0 {
|
||
t.Errorf("Invalidate 之后缓存该空了: %v", e.Names())
|
||
}
|
||
}
|
||
|
||
func TestEngine_Close(t *testing.T) {
|
||
e, err := jscriptx.New(jscriptx.WithLogger(nil))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
s := mustCompile(t, e, "x", `export function f() { return 1 }`)
|
||
e.Close()
|
||
|
||
if _, err := e.Compile("y", `export function g() {}`); !errors.Is(err, jscriptx.ErrClosed) {
|
||
t.Errorf("want ErrClosed, got %v", err)
|
||
}
|
||
if _, err := s.Call(context.Background(), "f"); !errors.Is(err, jscriptx.ErrClosed) {
|
||
t.Errorf("want ErrClosed, got %v", err)
|
||
}
|
||
}
|
||
|
||
// 同名脚本重新 Compile 会顶替掉旧的。
|
||
func TestEngine_重复Compile顶替(t *testing.T) {
|
||
e := newEngine(t)
|
||
mustCompile(t, e, "x", `export function f() { return 1 }`)
|
||
s2 := mustCompile(t, e, "x", `export function f() { return 2 }`)
|
||
|
||
cached, err := e.Script("x")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if cached != s2 {
|
||
t.Error("缓存里应该是最后一次编译的版本")
|
||
}
|
||
if got, _ := cached.Call(context.Background(), "f"); got.(int64) != 2 {
|
||
t.Errorf("got %#v", got)
|
||
}
|
||
}
|