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

152 lines
5.0 KiB
Go
Raw 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"
"os"
"strings"
"testing"
"git.fsdpf.net/go/jscriptx"
)
// 产物必须干净:不带 esbuild 的 CommonJS interop helper。
// 那套 helper 每建一个 VM 都要重跑一遍,是建实例开销的大头(实测差 5 倍)。
func TestBundle_产物不含interop_helper(t *testing.T) {
for _, c := range []struct{ name, src string }{
{"default 导出", `export default class H { m() { return 1 } }`},
{"只有命名导出", `export function a() { return 1 }
export const B = 2`},
{"default + 命名", `export default class H {}
export function extra() {}`},
} {
t.Run(c.name, func(t *testing.T) {
code, err := jscriptx.Bundle("p.ts", c.src)
if err != nil {
t.Fatal(err)
}
for _, helper := range []string{"__defProp", "__toCommonJS", "__copyProps", "__export", "__esModule"} {
if strings.Contains(code, helper) {
t.Errorf("产物里出现了 interop helper %q,说明打包格式退回 IIFE/CJS 了", helper)
}
}
body := code[:strings.Index(code, "//# sourceMappingURL")]
if len(body) > 300 {
t.Errorf("代码部分 %d 字节,偏大,检查是不是又带上 helper 了:\n%s", len(body), body)
}
})
}
}
// 包装不能让行号偏移,否则报错定位不回源码。
func TestBundle_包装不影响行号(t *testing.T) {
code, err := jscriptx.Bundle("p.ts", `export default class H {
m() { return 1 }
}`)
if err != nil {
t.Fatal(err)
}
// 开头的 (() => { 必须紧贴原来的第一行,不能自己占一行
first := strings.SplitN(code, "\n", 2)[0]
if !strings.HasPrefix(first, "(() => {") {
t.Fatalf("第一行是 %q,应该以 (() => { 开头", first)
}
if strings.TrimSpace(first) == "(() => {" {
t.Error("包装单独占了一行,后面所有行号都会偏移一位")
}
// 端到端确认:脚本第 3 行抛错,报出来也得是第 3 行
e := newEngine(t)
s := mustCompile(t, e, "boom.ts", `export default class H {
ok() { return 1 }
boom() { throw new Error("炸了") }
}`)
obj, err := s.New(context.Background())
if err != nil {
t.Fatal(err)
}
defer obj.Close()
_, err = obj.Call(context.Background(), "boom")
var jsErr *jscriptx.Error
if !errors.As(err, &jsErr) || len(jsErr.Stack) == 0 {
t.Fatalf("没拿到栈信息: %v", err)
}
if jsErr.Stack[0].Line != 3 {
t.Errorf("行号 = %d, want 3——包装让行号偏移了", jsErr.Stack[0].Line)
}
}
// 改写要认得 esbuild 输出的各种 export 形态。
func TestBundle_各种导出形态的改写(t *testing.T) {
e := newEngine(t)
ctx := context.Background()
for _, c := range []struct {
name, src, fn string
want any
}{
{"default class", `export default class H { m() { return "class" } }`, "m", "class"},
{"default 匿名 class", `export default class { m() { return "匿名" } }`, "m", "匿名"},
{"default 对象", `export default { m() { return "对象" } }`, "m", "对象"},
{"default 具名函数", `export default function f() { return "具名" }`, jscriptx.DefaultFunc, "具名"},
{"default 匿名函数", `export default function () { return "匿名函数" }`, jscriptx.DefaultFunc, "匿名函数"},
{"default 箭头", `export default () => "箭头"`, jscriptx.DefaultFunc, "箭头"},
{"只有命名导出", `export function a() { return "命名a" }
export function b() { return "命名b" }`, "a", "命名a"},
{"default + 命名(用 default", `export default class H { m() { return "混合" } }
export function extra() { return "额外" }`, "m", "混合"},
{"import 后再导出", `import { helper } from "./dep"
export default class H { m() { return helper() } }`, "m", "来自依赖"},
} {
t.Run(c.name, func(t *testing.T) {
var s *jscriptx.Script
if strings.Contains(c.src, `"./dep"`) {
// 需要一个可解析的依赖目录
dir := t.TempDir()
writeFile(t, dir+"/dep.ts", `export function helper() { return "来自依赖" }`)
e2 := newEngine(t, jscriptx.WithBundleOptions(jscriptx.WithResolveDir(dir)))
s = mustCompile(t, e2, "m.ts", c.src)
} else {
s = mustCompile(t, e, "m.ts", c.src)
}
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
got, err := obj.Call(ctx, c.fn)
if err != nil {
t.Fatalf("调用失败: %v", err)
}
if got != c.want {
t.Errorf("got = %#v, want %#v", got, c.want)
}
})
}
}
// 认不出 export 语句时的兜底:当作没有导出,给出明确报错,而不是静默出错。
func TestBundle_没有导出时兜底(t *testing.T) {
e := newEngine(t)
s := mustCompile(t, e, "noexport.js", `function handle() { return 1 }`)
_, err := s.Call(context.Background(), "handle")
if !errors.Is(err, jscriptx.ErrFuncNotFound) {
t.Fatalf("want ErrFuncNotFound, got %v", err)
}
var jsErr *jscriptx.Error
errors.As(err, &jsErr)
if !strings.Contains(jsErr.Msg, "没有任何导出") {
t.Errorf("报错该说清楚是没写 export: %s", jsErr.Msg)
}
}
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}