test: 测试文件跟源文件同名,删掉复制粘贴出来的重复
改完之后「某个行为在哪测」就是查同名文件:
lazyglobal_test.go → engine_globals_test.go
static_test.go → script_static_test.go
scope_pool_test.go → script_vm_test.go 它测的是 VM 池,不是作用域
esmwrap_test.go → bundle_finalize_test.go
missing_global_test.go → errors_hints_test.go
entry_test.go → bundle_entry_test.go 并收下 bundle_test.go 里
那三个 TestEntry_*
删掉 5 组复制粘贴。其中 TestNew_同实例调用串行 和 TestInstance_调用串行 **函数体
完全一致**,只差 mustCompile 的文件名参数 counter.js / counter.ts。Instance 的
测试全部归 instance_test.go 并统一前缀成 TestInstance_,scope_test.go 只留作用域。
vmstate_test.go + govalue_test.go → goja_facts_test.go。这两个测的都是 **goja
本身**,不是这个库:Program 能不能给多个 Runtime 共用、顶层变量跟不跟 Runtime 走、
Go 的切片进到脚本里是不是真数组。它们是「为什么这个库要这么设计」的实验记录,
goja 行为变了会先红。原来那个名字看不出这层定位。
helper 集中到 helper_test.go:newEngine / mustCompile 被十几个文件依赖,原来住在
script_test.go 里,那个名字暗示「测 Script」,找不到。
caller_example_test.go 并进 caller_test.go——同一个类型没必要两个文件。
overlay_test.go 和 script_vm_test.go 保持内部测试,各写了一句为什么:前者要读
Engine.loader 和 isPrepared,后者要直接断言 vmHandle.scoped。我试过把 overlay
挪成外部测试,挪不动。
This commit is contained in:
@@ -211,3 +211,59 @@ func TestEntry_池化下实例状态会丢(t *testing.T) {
|
||||
t.Errorf("池化模式换了 VM,实例状态应该归零,got %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 直接 Compile 的源码不能有 import,报错要指出走 esm 子包。
|
||||
func TestEntry_Compile不支持import(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
_, err := e.Compile("imp.ts", `import x from "./other"
|
||||
export default class H { m() { return x } }`)
|
||||
if err == nil {
|
||||
t.Fatal("应该报错")
|
||||
}
|
||||
for _, want := range []string{"Could not resolve", "jscriptx/esm"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("报错里没有 %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
t.Logf("%v", err)
|
||||
}
|
||||
|
||||
// 配了 WithResolveDir 之后,直接 Compile 的源码也能 import。
|
||||
func TestEntry_ResolveDir(t *testing.T) {
|
||||
e := newEngine(t, jscriptx.WithBundleOptions(
|
||||
jscriptx.WithResolveDir("esm/testdata/app/src/Resource"),
|
||||
))
|
||||
s, err := e.Compile("inline.ts", `import ResExecuter from "./ResExecuter"
|
||||
export default class H {
|
||||
m() { return new ResExecuter().Store("cfg") }
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatalf("配了 resolveDir 应该能 import: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
obj, err := s.New(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
if got, _ := obj.Call(ctx, "m"); got != "stored:cfg" {
|
||||
t.Errorf("got = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 语法错误在 Compile 阶段就报,带文件位置。
|
||||
func TestEntry_语法错误(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
_, err := e.Compile("bad.ts", `export default class { 语法错误 (((`)
|
||||
if err == nil {
|
||||
t.Fatal("应该报语法错误")
|
||||
}
|
||||
var jsErr *jscriptx.Error
|
||||
if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindCompile {
|
||||
t.Fatalf("错误分类不对: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "bad.ts") {
|
||||
t.Errorf("报错没带文件名: %v", err)
|
||||
}
|
||||
t.Logf("%v", err)
|
||||
}
|
||||
@@ -2,9 +2,7 @@ package jscriptx_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/jscriptx"
|
||||
@@ -100,59 +98,3 @@ export function shout(s: string): string { return upper(s) }`
|
||||
t.Errorf("got = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 直接 Compile 的源码不能有 import,报错要指出走 esm 子包。
|
||||
func TestEntry_Compile不支持import(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
_, err := e.Compile("imp.ts", `import x from "./other"
|
||||
export default class H { m() { return x } }`)
|
||||
if err == nil {
|
||||
t.Fatal("应该报错")
|
||||
}
|
||||
for _, want := range []string{"Could not resolve", "jscriptx/esm"} {
|
||||
if !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("报错里没有 %q: %v", want, err)
|
||||
}
|
||||
}
|
||||
t.Logf("%v", err)
|
||||
}
|
||||
|
||||
// 配了 WithResolveDir 之后,直接 Compile 的源码也能 import。
|
||||
func TestEntry_ResolveDir(t *testing.T) {
|
||||
e := newEngine(t, jscriptx.WithBundleOptions(
|
||||
jscriptx.WithResolveDir("esm/testdata/app/src/Resource"),
|
||||
))
|
||||
s, err := e.Compile("inline.ts", `import ResExecuter from "./ResExecuter"
|
||||
export default class H {
|
||||
m() { return new ResExecuter().Store("cfg") }
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatalf("配了 resolveDir 应该能 import: %v", err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
obj, err := s.New(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
if got, _ := obj.Call(ctx, "m"); got != "stored:cfg" {
|
||||
t.Errorf("got = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 语法错误在 Compile 阶段就报,带文件位置。
|
||||
func TestEntry_语法错误(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
_, err := e.Compile("bad.ts", `export default class { 语法错误 (((`)
|
||||
if err == nil {
|
||||
t.Fatal("应该报语法错误")
|
||||
}
|
||||
var jsErr *jscriptx.Error
|
||||
if !errors.As(err, &jsErr) || jsErr.Kind != jscriptx.KindCompile {
|
||||
t.Fatalf("错误分类不对: %v", err)
|
||||
}
|
||||
if !strings.Contains(err.Error(), "bad.ts") {
|
||||
t.Errorf("报错没带文件名: %v", err)
|
||||
}
|
||||
t.Logf("%v", err)
|
||||
}
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
package jscriptx_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.fsdpf.net/go/jscriptx"
|
||||
)
|
||||
|
||||
// Caller 用来实现自定义的调用约定——本库不预设脚本回调该长什么样,
|
||||
// 这个例子实现的是「回调 + next」这种中间件式的约定:脚本函数写成哪种形状,
|
||||
// 运行时探测出来自动适配,脚本作者不用额外声明签名。
|
||||
//
|
||||
// 它同时也说明了为什么需要 Caller 而不是直接用 Call:next 是个 Go 闭包,
|
||||
// 得先看脚本声明了几个形参,才知道要不要把它传进去——这些都必须在同一次
|
||||
// VM 借出期间完成。
|
||||
func ExampleCaller() {
|
||||
e, err := jscriptx.New(jscriptx.WithLogger(nil))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer e.Close()
|
||||
|
||||
// 三种回调写法,脚本作者挑顺手的用
|
||||
s, err := e.Compile("ctrl.ts", `
|
||||
export function twoArgs(v, next) { return next(v) + "|脚本自己调的 next" }
|
||||
export function decorator(v) { return v + "|装饰过" }
|
||||
export function sideEffect(v) { /* 只做事,不返回 */ }
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
next := func(v string) string { return "[" + v + "]" }
|
||||
|
||||
// 回调签名的约定是**调用方**定的(这里定成"1 个或 2 个形参"),
|
||||
// 所以哨兵错误也归调用方声明,本库不提供。
|
||||
errBadSignature := errors.New("回调签名不受支持")
|
||||
|
||||
// dispatch 按脚本函数的形状分派
|
||||
dispatch := func(fn, value string) (string, error) {
|
||||
var out string
|
||||
err := s.WithCall(context.Background(), fn, func(c jscriptx.Caller) error {
|
||||
switch n := c.Arity(); n {
|
||||
case 2:
|
||||
// handle(v, next):调不调 next 由脚本决定,但得把结果返回出来
|
||||
res, err := c.Call(value, next)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.IsEmpty() {
|
||||
return fmt.Errorf("%w: 两个形参的回调必须返回结果",
|
||||
errBadSignature)
|
||||
}
|
||||
return res.Into(&out)
|
||||
|
||||
case 1:
|
||||
// handle(v):有返回值当装饰器,没返回值当纯副作用
|
||||
res, err := c.Call(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.IsEmpty() {
|
||||
out = next(value)
|
||||
return nil
|
||||
}
|
||||
var decorated string
|
||||
if err := res.Into(&decorated); err != nil {
|
||||
return err
|
||||
}
|
||||
out = next(decorated)
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("%w: 形参个数必须是 1 或 2,当前是 %d",
|
||||
errBadSignature, n)
|
||||
}
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
for _, fn := range []string{"twoArgs", "decorator", "sideEffect"} {
|
||||
got, err := dispatch(fn, "输入")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%-11s → %s\n", fn, got)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// twoArgs → [输入]|脚本自己调的 next
|
||||
// decorator → [输入|装饰过]
|
||||
// sideEffect → [输入]
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package jscriptx_test
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -207,3 +209,89 @@ func TestWithCall_在实例上(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Caller 用来实现自定义的调用约定——本库不预设脚本回调该长什么样,
|
||||
// 这个例子实现的是「回调 + next」这种中间件式的约定:脚本函数写成哪种形状,
|
||||
// 运行时探测出来自动适配,脚本作者不用额外声明签名。
|
||||
//
|
||||
// 它同时也说明了为什么需要 Caller 而不是直接用 Call:next 是个 Go 闭包,
|
||||
// 得先看脚本声明了几个形参,才知道要不要把它传进去——这些都必须在同一次
|
||||
// VM 借出期间完成。
|
||||
func ExampleCaller() {
|
||||
e, err := jscriptx.New(jscriptx.WithLogger(nil))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer e.Close()
|
||||
|
||||
// 三种回调写法,脚本作者挑顺手的用
|
||||
s, err := e.Compile("ctrl.ts", `
|
||||
export function twoArgs(v, next) { return next(v) + "|脚本自己调的 next" }
|
||||
export function decorator(v) { return v + "|装饰过" }
|
||||
export function sideEffect(v) { /* 只做事,不返回 */ }
|
||||
`)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
next := func(v string) string { return "[" + v + "]" }
|
||||
|
||||
// 回调签名的约定是**调用方**定的(这里定成"1 个或 2 个形参"),
|
||||
// 所以哨兵错误也归调用方声明,本库不提供。
|
||||
errBadSignature := errors.New("回调签名不受支持")
|
||||
|
||||
// dispatch 按脚本函数的形状分派
|
||||
dispatch := func(fn, value string) (string, error) {
|
||||
var out string
|
||||
err := s.WithCall(context.Background(), fn, func(c jscriptx.Caller) error {
|
||||
switch n := c.Arity(); n {
|
||||
case 2:
|
||||
// handle(v, next):调不调 next 由脚本决定,但得把结果返回出来
|
||||
res, err := c.Call(value, next)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.IsEmpty() {
|
||||
return fmt.Errorf("%w: 两个形参的回调必须返回结果",
|
||||
errBadSignature)
|
||||
}
|
||||
return res.Into(&out)
|
||||
|
||||
case 1:
|
||||
// handle(v):有返回值当装饰器,没返回值当纯副作用
|
||||
res, err := c.Call(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.IsEmpty() {
|
||||
out = next(value)
|
||||
return nil
|
||||
}
|
||||
var decorated string
|
||||
if err := res.Into(&decorated); err != nil {
|
||||
return err
|
||||
}
|
||||
out = next(decorated)
|
||||
return nil
|
||||
|
||||
default:
|
||||
return fmt.Errorf("%w: 形参个数必须是 1 或 2,当前是 %d",
|
||||
errBadSignature, n)
|
||||
}
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
for _, fn := range []string{"twoArgs", "decorator", "sideEffect"} {
|
||||
got, err := dispatch(fn, "输入")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fmt.Printf("%-11s → %s\n", fn, got)
|
||||
}
|
||||
|
||||
// Output:
|
||||
// twoArgs → [输入]|脚本自己调的 next
|
||||
// decorator → [输入|装饰过]
|
||||
// sideEffect → [输入]
|
||||
}
|
||||
|
||||
@@ -1,19 +1,24 @@
|
||||
package jscriptx_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.fsdpf.net/go/jscriptx"
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
// 这一组直接用 goja,不经过本库的任何封装,钉住整个设计赖以成立的两条引擎事实:
|
||||
// 这一组**不测本库,测 goja**——它是「为什么这个库要这么设计」的实验记录。
|
||||
//
|
||||
// 三件事:
|
||||
//
|
||||
// 1. Program 无状态,可以给任意多个 Runtime 共用——本库共用编译结果的前提
|
||||
// 2. **模块顶层的变量跟着 Runtime 走,不是共享状态**
|
||||
// 3. Go 的值进到脚本里长什么样(切片是不是真数组、nil 是 null 还是 undefined)
|
||||
//
|
||||
// 第 2 条是最容易误解的地方。它导致:一个脚本串行开发时表现正常(一直命中同一个
|
||||
// Runtime),并发起来行为却随调度漂移——有几个 Runtime 就有几份独立的顶层状态,
|
||||
@@ -21,6 +26,12 @@ import (
|
||||
//
|
||||
// 所以要在并发之间共享状态,只能把它放到 Go 侧(写一个扩展,见 Extension 接口):
|
||||
// Go 对象可以被每个 Runtime 各自包一层,并发安全由 Go 自己保证。
|
||||
//
|
||||
// 第 3 条脚本作者是照着写代码的,一旦变了,业务脚本会**静默**走错分支(比如查出来的
|
||||
// 记录突然 Array.isArray 为 false,整段列表被当成空的)。
|
||||
//
|
||||
// 这些行为变了,这一组会先红——那时候要改的是本库的设计,不是这里的断言。
|
||||
// 其中有一条是哨兵:goja 哪天允许对象跨 Runtime 了,它会失败,提醒我们可以简化设计。
|
||||
|
||||
const gojaCounter = `
|
||||
var counter = 0;
|
||||
@@ -585,3 +596,91 @@ func TestGoja_独立Runtime里的JS计数器不共享(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Go 的切片和 map 进到脚本里长什么样。
|
||||
//
|
||||
// 这不是 jscriptx 的行为,是 goja 的——但脚本作者是照着这个写代码的,一旦变了,
|
||||
// 业务脚本会**静默**走错分支(比如查出来的记录突然 Array.isArray 为 false,
|
||||
// 整段列表被当成空的),所以在这里钉住。
|
||||
//
|
||||
// 之所以走 jscriptx 而不是直接用 goja:注入前还经过一层 freeze,得连那层一起验。
|
||||
func TestGo值到脚本里的形状(t *testing.T) {
|
||||
e, err := jscriptx.New(jscriptx.WithLogger(nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer e.Close()
|
||||
|
||||
// 照着资源查询的返回长的:[]map[string]any,里面混着切片、字符串和 nil
|
||||
records := func() []map[string]any {
|
||||
return []map[string]any{{
|
||||
"role": "user",
|
||||
"content": "你好",
|
||||
"attempts": []any{map[string]any{"name": "a.pdf"}, map[string]any{"url": "http://x"}},
|
||||
"tool_calls": `[{"id":"1"}]`, // 列里存的是 JSON 字符串
|
||||
"reasoning": nil,
|
||||
}}
|
||||
}
|
||||
|
||||
s, err := e.Compile("shape.ts", `
|
||||
export default class C {
|
||||
Run() {
|
||||
const rows = getRecords()
|
||||
const r = rows[0]
|
||||
return {
|
||||
// 切片是真数组:length / for-of / map 都能用
|
||||
rowsIsArray: Array.isArray(rows),
|
||||
rowsLen: rows.length,
|
||||
attemptsIsArray: Array.isArray(r.attempts),
|
||||
attemptsLen: r.attempts.length,
|
||||
mapped: r.attempts.map(a => a.name || a.url).join(","),
|
||||
|
||||
// map[string]any 的值按 Go 的类型各自转换,不会统一成字符串
|
||||
contentType: typeof r.content,
|
||||
toolCallsType: typeof r.tool_calls,
|
||||
|
||||
// Go 的 nil 是 null,不是 undefined——判空要用 == null 或 === null
|
||||
reasoningIsNull: r.reasoning === null,
|
||||
reasoningIsUndef: r.reasoning === undefined,
|
||||
|
||||
// 没有的列是 undefined,不会报错
|
||||
missingIsUndef: r.nope === undefined,
|
||||
}
|
||||
}
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := jscriptx.WithScope(context.Background(),
|
||||
jscriptx.ScopeGlobals(map[string]any{"getRecords": records}))
|
||||
|
||||
obj, err := s.New(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
var got map[string]any
|
||||
if err := obj.CallInto(ctx, "Run", &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := map[string]any{
|
||||
"rowsIsArray": true,
|
||||
"rowsLen": int64(1),
|
||||
"attemptsIsArray": true,
|
||||
"attemptsLen": int64(2),
|
||||
"mapped": "a.pdf,http://x",
|
||||
"contentType": "string",
|
||||
"toolCallsType": "string",
|
||||
"reasoningIsNull": true,
|
||||
"reasoningIsUndef": false,
|
||||
"missingIsUndef": true,
|
||||
}
|
||||
for k, w := range want {
|
||||
if got[k] != w {
|
||||
t.Errorf("%s = %#v,该是 %#v", k, got[k], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
package jscriptx_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/jscriptx"
|
||||
)
|
||||
|
||||
// Go 的切片和 map 进到脚本里长什么样。
|
||||
//
|
||||
// 这不是 jscriptx 的行为,是 goja 的——但脚本作者是照着这个写代码的,一旦变了,
|
||||
// 业务脚本会**静默**走错分支(比如查出来的记录突然 Array.isArray 为 false,
|
||||
// 整段列表被当成空的),所以在这里钉住。
|
||||
//
|
||||
// 之所以走 jscriptx 而不是直接用 goja:注入前还经过一层 freeze,得连那层一起验。
|
||||
func TestGo值到脚本里的形状(t *testing.T) {
|
||||
e, err := jscriptx.New(jscriptx.WithLogger(nil))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer e.Close()
|
||||
|
||||
// 照着资源查询的返回长的:[]map[string]any,里面混着切片、字符串和 nil
|
||||
records := func() []map[string]any {
|
||||
return []map[string]any{{
|
||||
"role": "user",
|
||||
"content": "你好",
|
||||
"attempts": []any{map[string]any{"name": "a.pdf"}, map[string]any{"url": "http://x"}},
|
||||
"tool_calls": `[{"id":"1"}]`, // 列里存的是 JSON 字符串
|
||||
"reasoning": nil,
|
||||
}}
|
||||
}
|
||||
|
||||
s, err := e.Compile("shape.ts", `
|
||||
export default class C {
|
||||
Run() {
|
||||
const rows = getRecords()
|
||||
const r = rows[0]
|
||||
return {
|
||||
// 切片是真数组:length / for-of / map 都能用
|
||||
rowsIsArray: Array.isArray(rows),
|
||||
rowsLen: rows.length,
|
||||
attemptsIsArray: Array.isArray(r.attempts),
|
||||
attemptsLen: r.attempts.length,
|
||||
mapped: r.attempts.map(a => a.name || a.url).join(","),
|
||||
|
||||
// map[string]any 的值按 Go 的类型各自转换,不会统一成字符串
|
||||
contentType: typeof r.content,
|
||||
toolCallsType: typeof r.tool_calls,
|
||||
|
||||
// Go 的 nil 是 null,不是 undefined——判空要用 == null 或 === null
|
||||
reasoningIsNull: r.reasoning === null,
|
||||
reasoningIsUndef: r.reasoning === undefined,
|
||||
|
||||
// 没有的列是 undefined,不会报错
|
||||
missingIsUndef: r.nope === undefined,
|
||||
}
|
||||
}
|
||||
}`)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ctx := jscriptx.WithScope(context.Background(),
|
||||
jscriptx.ScopeGlobals(map[string]any{"getRecords": records}))
|
||||
|
||||
obj, err := s.New(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
var got map[string]any
|
||||
if err := obj.CallInto(ctx, "Run", &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := map[string]any{
|
||||
"rowsIsArray": true,
|
||||
"rowsLen": int64(1),
|
||||
"attemptsIsArray": true,
|
||||
"attemptsLen": int64(2),
|
||||
"mapped": "a.pdf,http://x",
|
||||
"contentType": "string",
|
||||
"toolCallsType": "string",
|
||||
"reasoningIsNull": true,
|
||||
"reasoningIsUndef": false,
|
||||
"missingIsUndef": true,
|
||||
}
|
||||
for k, w := range want {
|
||||
if got[k] != w {
|
||||
t.Errorf("%s = %#v,该是 %#v", k, got[k], w)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package jscriptx_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"git.fsdpf.net/go/jscriptx"
|
||||
)
|
||||
|
||||
// 各个测试文件共用的脚手架。放一起是因为它们被十几个文件依赖——
|
||||
// 原来住在 script_test.go 里,那个名字暗示「测 Script」,找不到。
|
||||
|
||||
// newEngine 造一个测试用引擎,默认不打日志(避免测试输出被 console 刷屏)。
|
||||
func newEngine(t *testing.T, opts ...jscriptx.Option) *jscriptx.Engine {
|
||||
t.Helper()
|
||||
opts = append([]jscriptx.Option{jscriptx.WithLogger(nil)}, opts...)
|
||||
e, err := jscriptx.New(opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("New 失败: %v", err)
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
return e
|
||||
}
|
||||
|
||||
func mustCompile(t *testing.T, e *jscriptx.Engine, name, src string) *jscriptx.Script {
|
||||
t.Helper()
|
||||
s, err := e.Compile(name, src)
|
||||
if err != nil {
|
||||
t.Fatalf("编译 %s 失败: %v", name, err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// toNumber 把脚本返回的数字统一成 float64:goja 会按数值是否为整数
|
||||
// 导出成 int64 或 float64,测试里不必纠结这个差异。
|
||||
func toNumber(t *testing.T, v any) float64 {
|
||||
t.Helper()
|
||||
switch n := v.(type) {
|
||||
case int64:
|
||||
return float64(n)
|
||||
case float64:
|
||||
return n
|
||||
default:
|
||||
t.Fatalf("不是数字: %#v", v)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
+129
-110
@@ -3,12 +3,14 @@ package jscriptx_test
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.fsdpf.net/go/jscriptx"
|
||||
"git.fsdpf.net/go/jscriptx/internal/testext"
|
||||
)
|
||||
|
||||
const objScript = `
|
||||
@@ -24,79 +26,8 @@ export default class DeviceHandler {
|
||||
}
|
||||
`
|
||||
|
||||
// 最基本的:new 出对象,当普通 Go 对象用。
|
||||
func TestNew_当对象用(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
|
||||
obj, err := s.New(context.Background(), "dev-1", "温控器")
|
||||
if err != nil {
|
||||
t.Fatalf("New 失败: %v", err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
ctx := context.Background()
|
||||
got, err := obj.Call(ctx, "info")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "dev-1/温控器" {
|
||||
t.Errorf("info = %#v", got)
|
||||
}
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
got, err := obj.Call(ctx, "onMessage", "p")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.(int64) != int64(i) {
|
||||
t.Errorf("第 %d 次 = %#v", i, got)
|
||||
}
|
||||
}
|
||||
if obj.Calls() != 4 {
|
||||
t.Errorf("Calls = %d, want 4", obj.Calls())
|
||||
}
|
||||
if obj.Name() != "device.js" {
|
||||
t.Errorf("Name = %q", obj.Name())
|
||||
}
|
||||
}
|
||||
|
||||
// 多个实例互相独立。
|
||||
func TestNew_实例互相独立(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
ctx := context.Background()
|
||||
|
||||
a, err := s.New(ctx, "dev-A", "型号A")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer a.Close()
|
||||
b, err := s.New(ctx, "dev-B", "型号B")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer b.Close()
|
||||
|
||||
a.Call(ctx, "onMessage", "x")
|
||||
a.Call(ctx, "onMessage", "x")
|
||||
got, err := b.Call(ctx, "onMessage", "x")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.(int64) != 1 {
|
||||
t.Errorf("B 被 A 污染: %#v", got)
|
||||
}
|
||||
if got, _ := a.Call(ctx, "info"); got != "dev-A/型号A" {
|
||||
t.Errorf("A 的构造参数不对: %#v", got)
|
||||
}
|
||||
if got, _ := b.Call(ctx, "info"); got != "dev-B/型号B" {
|
||||
t.Errorf("B 的构造参数不对: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// constructor 抛异常要在 New 就报出来,不用等到第一次调用。
|
||||
func TestNew_构造失败当场报错(t *testing.T) {
|
||||
func TestInstance_构造失败当场报错(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "bad.js", `
|
||||
export default class H {
|
||||
@@ -121,7 +52,7 @@ func TestNew_构造失败当场报错(t *testing.T) {
|
||||
t.Logf("%v", err)
|
||||
}
|
||||
|
||||
func TestNew_Close后拒绝调用(t *testing.T) {
|
||||
func TestInstance_Close后拒绝调用(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
ctx := context.Background()
|
||||
@@ -137,10 +68,69 @@ func TestNew_Close后拒绝调用(t *testing.T) {
|
||||
obj.Close() // 重复关闭不该 panic
|
||||
}
|
||||
|
||||
// 同一个实例被并发调用时必须串行。
|
||||
func TestNew_同实例调用串行(t *testing.T) {
|
||||
func TestInstance_Has(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "counter.js", `
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
obj, _ := s.New(context.Background(), "d", "m")
|
||||
defer obj.Close()
|
||||
|
||||
if !obj.Has("onMessage") || !obj.Has("info") {
|
||||
t.Error("Has 认不出实例方法")
|
||||
}
|
||||
if obj.Has("nope") {
|
||||
t.Error("Has 不该认出不存在的方法")
|
||||
}
|
||||
}
|
||||
|
||||
// 实例独占 VM,this.xxx 在它活着期间跨调用保持。
|
||||
func TestInstance_状态跨调用保持(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.ts", deviceScript)
|
||||
ctx := context.Background()
|
||||
|
||||
obj, err := s.New(ctx, "device-A", "温控器")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
for i, want := range []string{"device-A#1:m1", "device-A#2:m2", "device-A#3:m3"} {
|
||||
got, err := obj.Call(ctx, "onMessage", fmt.Sprintf("m%d", i+1))
|
||||
if err != nil {
|
||||
t.Fatalf("第 %d 条失败: %v", i+1, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("第 %d 条 = %#v, want %q", i+1, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不同实例互不相干,这是并发的基础。
|
||||
func TestInstance_互相隔离(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.ts", deviceScript)
|
||||
ctx := context.Background()
|
||||
|
||||
a, _ := s.New(ctx, "device-A", "型号A")
|
||||
defer a.Close()
|
||||
b, _ := s.New(ctx, "device-B", "型号B")
|
||||
defer b.Close()
|
||||
|
||||
a.Call(ctx, "onMessage", "x")
|
||||
a.Call(ctx, "onMessage", "x")
|
||||
got, err := b.Call(ctx, "onMessage", "x")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "device-B#1:x" {
|
||||
t.Errorf("B 被 A 污染: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 同一个实例的调用串行,保护 this.xxx 不错乱。
|
||||
func TestInstance_调用串行(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "counter.ts", `
|
||||
export default class C {
|
||||
constructor() { this.n = 0 }
|
||||
bump() { this.n++; return this.n }
|
||||
@@ -176,16 +166,62 @@ func TestNew_同实例调用串行(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 脚本出错会重建实例,状态归零,Resets 能查到。
|
||||
func TestNew_出错后重建(t *testing.T) {
|
||||
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
ctx := context.Background()
|
||||
// 多个实例并发跑,各自的状态正确。
|
||||
func TestInstance_多实例并发(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.ts", deviceScript)
|
||||
|
||||
obj, _ := s.New(ctx, "dev-1", "温控器")
|
||||
const devices, messages = 16, 30
|
||||
var wg sync.WaitGroup
|
||||
for d := 0; d < devices; d++ {
|
||||
id := fmt.Sprintf("device-%d", d)
|
||||
wg.Add(1)
|
||||
go func(id string) {
|
||||
defer wg.Done()
|
||||
obj, err := s.New(context.Background(), id, "型号")
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
defer obj.Close()
|
||||
obj.Call(ctx, "onMessage", "x")
|
||||
obj.Call(ctx, "onMessage", "x")
|
||||
for m := 0; m < messages; m++ {
|
||||
got, err := obj.Call(context.Background(), "onMessage", "m")
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", id, err)
|
||||
return
|
||||
}
|
||||
want := fmt.Sprintf("%s#%d:m", id, m+1)
|
||||
if got != want {
|
||||
t.Errorf("%s 第 %d 条 = %#v, want %q", id, m+1, got, want)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(id)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 脚本出错会丢 this 上的状态,但扩展里的东西还在。
|
||||
func TestInstance_出错后状态归零(t *testing.T) {
|
||||
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
|
||||
s := mustCompile(t, e, "mix.ts", `
|
||||
export default class M {
|
||||
constructor() { this.n = 0 }
|
||||
bump() { store.Incr("kept", 1); return ++this.n }
|
||||
spin() { while (true) {} }
|
||||
}
|
||||
`)
|
||||
|
||||
st := testext.New()
|
||||
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st))
|
||||
obj, err := s.New(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
obj.Call(ctx, "bump")
|
||||
obj.Call(ctx, "bump")
|
||||
|
||||
if _, err := obj.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) {
|
||||
t.Fatalf("want ErrTimeout, got %v", err)
|
||||
@@ -194,44 +230,27 @@ func TestNew_出错后重建(t *testing.T) {
|
||||
t.Errorf("Resets = %d, want 1", obj.Resets())
|
||||
}
|
||||
|
||||
got, err := obj.Call(ctx, "onMessage", "x")
|
||||
got, err := obj.Call(ctx, "bump")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.(int64) != 1 {
|
||||
t.Errorf("重建后状态应该归零, got %#v", got)
|
||||
t.Errorf("this 上的状态应该归零, got %#v", got)
|
||||
}
|
||||
// 构造参数会重新传一遍,所以 info 仍然正确
|
||||
if got, _ := obj.Call(ctx, "info"); got != "dev-1/温控器" {
|
||||
t.Errorf("重建后构造参数丢了: %#v", got)
|
||||
// 扩展在 Go 侧,不受 VM 重建影响
|
||||
if n := toNumber(t, st.Get("kept")); n != 3 {
|
||||
t.Errorf("扩展里的状态不该丢, got %v want 3", n)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNew_Has(t *testing.T) {
|
||||
// 脚本关闭后实例失效。
|
||||
func TestInstance_脚本关闭后失效(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
obj, _ := s.New(context.Background(), "d", "m")
|
||||
defer obj.Close()
|
||||
|
||||
if !obj.Has("onMessage") || !obj.Has("info") {
|
||||
t.Error("Has 认不出实例方法")
|
||||
}
|
||||
if obj.Has("nope") {
|
||||
t.Error("Has 不该认出不存在的方法")
|
||||
}
|
||||
}
|
||||
|
||||
// 脚本关闭后,已经 new 出来的实例也不能再用。
|
||||
func TestNew_脚本关闭后失效(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.js", objScript)
|
||||
s := mustCompile(t, e, "device.ts", deviceScript)
|
||||
obj, _ := s.New(context.Background(), "d", "m")
|
||||
|
||||
s.Close()
|
||||
if _, err := obj.Call(context.Background(), "info"); !errors.Is(err, jscriptx.ErrClosed) {
|
||||
if _, err := obj.Call(context.Background(), "onMessage", "x"); !errors.Is(err, jscriptx.ErrClosed) {
|
||||
t.Errorf("want ErrClosed, got %v", err)
|
||||
}
|
||||
if _, err := s.New(context.Background(), "d", "m"); !errors.Is(err, jscriptx.ErrClosed) {
|
||||
t.Errorf("关闭的脚本不该能 New, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package jscriptx
|
||||
|
||||
// 这是**内部测试**(package jscriptx,不是 jscriptx_test):有几条要读
|
||||
// Engine.loader 这个未导出字段、调 isPrepared 这个未导出函数,验的是"多层
|
||||
// Loader 合成之后引擎实际拿到的是什么",从外面看不见。
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
-190
@@ -3,8 +3,6 @@ package jscriptx_test
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -27,51 +25,6 @@ const deviceScript = `
|
||||
}
|
||||
`
|
||||
|
||||
// 实例独占 VM,this.xxx 在它活着期间跨调用保持。
|
||||
func TestInstance_状态跨调用保持(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.ts", deviceScript)
|
||||
ctx := context.Background()
|
||||
|
||||
obj, err := s.New(ctx, "device-A", "温控器")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
for i, want := range []string{"device-A#1:m1", "device-A#2:m2", "device-A#3:m3"} {
|
||||
got, err := obj.Call(ctx, "onMessage", fmt.Sprintf("m%d", i+1))
|
||||
if err != nil {
|
||||
t.Fatalf("第 %d 条失败: %v", i+1, err)
|
||||
}
|
||||
if got != want {
|
||||
t.Errorf("第 %d 条 = %#v, want %q", i+1, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 不同实例互不相干,这是并发的基础。
|
||||
func TestInstance_互相隔离(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.ts", deviceScript)
|
||||
ctx := context.Background()
|
||||
|
||||
a, _ := s.New(ctx, "device-A", "型号A")
|
||||
defer a.Close()
|
||||
b, _ := s.New(ctx, "device-B", "型号B")
|
||||
defer b.Close()
|
||||
|
||||
a.Call(ctx, "onMessage", "x")
|
||||
a.Call(ctx, "onMessage", "x")
|
||||
got, err := b.Call(ctx, "onMessage", "x")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "device-B#1:x" {
|
||||
t.Errorf("B 被 A 污染: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 作用域的核心用途:同一个 ctx 下的多个脚本共享扩展。
|
||||
func TestScope_多脚本共享扩展(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
@@ -289,149 +242,6 @@ func TestScope_取回扩展(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// 同一个实例的调用串行,保护 this.xxx 不错乱。
|
||||
func TestInstance_调用串行(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "counter.ts", `
|
||||
export default class C {
|
||||
constructor() { this.n = 0 }
|
||||
bump() { this.n++; return this.n }
|
||||
total() { return this.n }
|
||||
}
|
||||
`)
|
||||
|
||||
obj, err := s.New(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
const goroutines, per = 8, 100
|
||||
var wg sync.WaitGroup
|
||||
for g := 0; g < goroutines; g++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for i := 0; i < per; i++ {
|
||||
if _, err := obj.Call(context.Background(), "bump"); err != nil {
|
||||
t.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
got, _ := obj.Call(context.Background(), "total")
|
||||
if got.(int64) != goroutines*per {
|
||||
t.Errorf("串行保证失效, total = %#v, want %d", got, goroutines*per)
|
||||
}
|
||||
}
|
||||
|
||||
// 多个实例并发跑,各自的状态正确。
|
||||
func TestInstance_多实例并发(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.ts", deviceScript)
|
||||
|
||||
const devices, messages = 16, 30
|
||||
var wg sync.WaitGroup
|
||||
for d := 0; d < devices; d++ {
|
||||
id := fmt.Sprintf("device-%d", d)
|
||||
wg.Add(1)
|
||||
go func(id string) {
|
||||
defer wg.Done()
|
||||
obj, err := s.New(context.Background(), id, "型号")
|
||||
if err != nil {
|
||||
t.Errorf("%v", err)
|
||||
return
|
||||
}
|
||||
defer obj.Close()
|
||||
for m := 0; m < messages; m++ {
|
||||
got, err := obj.Call(context.Background(), "onMessage", "m")
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", id, err)
|
||||
return
|
||||
}
|
||||
want := fmt.Sprintf("%s#%d:m", id, m+1)
|
||||
if got != want {
|
||||
t.Errorf("%s 第 %d 条 = %#v, want %q", id, m+1, got, want)
|
||||
return
|
||||
}
|
||||
}
|
||||
}(id)
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
// 脚本出错会丢 this 上的状态,但扩展里的东西还在。
|
||||
func TestInstance_出错后状态归零(t *testing.T) {
|
||||
e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond))
|
||||
s := mustCompile(t, e, "mix.ts", `
|
||||
export default class M {
|
||||
constructor() { this.n = 0 }
|
||||
bump() { store.Incr("kept", 1); return ++this.n }
|
||||
spin() { while (true) {} }
|
||||
}
|
||||
`)
|
||||
|
||||
st := testext.New()
|
||||
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st))
|
||||
obj, err := s.New(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer obj.Close()
|
||||
|
||||
obj.Call(ctx, "bump")
|
||||
obj.Call(ctx, "bump")
|
||||
|
||||
if _, err := obj.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) {
|
||||
t.Fatalf("want ErrTimeout, got %v", err)
|
||||
}
|
||||
if obj.Resets() != 1 {
|
||||
t.Errorf("Resets = %d, want 1", obj.Resets())
|
||||
}
|
||||
|
||||
got, err := obj.Call(ctx, "bump")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.(int64) != 1 {
|
||||
t.Errorf("this 上的状态应该归零, got %#v", got)
|
||||
}
|
||||
// 扩展在 Go 侧,不受 VM 重建影响
|
||||
if n := toNumber(t, st.Get("kept")); n != 3 {
|
||||
t.Errorf("扩展里的状态不该丢, got %v want 3", n)
|
||||
}
|
||||
}
|
||||
|
||||
// 脚本关闭后实例失效。
|
||||
func TestInstance_脚本关闭后失效(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "device.ts", deviceScript)
|
||||
obj, _ := s.New(context.Background(), "d", "m")
|
||||
|
||||
s.Close()
|
||||
if _, err := obj.Call(context.Background(), "onMessage", "x"); !errors.Is(err, jscriptx.ErrClosed) {
|
||||
t.Errorf("want ErrClosed, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// toNumber 把脚本返回的数字统一成 float64:goja 会按数值是否为整数
|
||||
// 导出成 int64 或 float64,测试里不必纠结这个差异。
|
||||
func toNumber(t *testing.T, v any) float64 {
|
||||
t.Helper()
|
||||
switch n := v.(type) {
|
||||
case int64:
|
||||
return float64(n)
|
||||
case float64:
|
||||
return n
|
||||
default:
|
||||
t.Fatalf("不是数字: %#v", v)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
// 作用域实例重建 VM 时,要回到它**创建时**那个作用域。
|
||||
//
|
||||
// Instance 的 VM 超时后会被丢弃,下次调用重建(见 Resets)。重建走的是 i.scope,
|
||||
|
||||
@@ -9,27 +9,6 @@ import (
|
||||
"git.fsdpf.net/go/jscriptx"
|
||||
)
|
||||
|
||||
// newEngine 造一个测试用引擎,默认不打日志(避免测试输出被 console 刷屏)。
|
||||
func newEngine(t *testing.T, opts ...jscriptx.Option) *jscriptx.Engine {
|
||||
t.Helper()
|
||||
opts = append([]jscriptx.Option{jscriptx.WithLogger(nil)}, opts...)
|
||||
e, err := jscriptx.New(opts...)
|
||||
if err != nil {
|
||||
t.Fatalf("New 失败: %v", err)
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
return e
|
||||
}
|
||||
|
||||
func mustCompile(t *testing.T, e *jscriptx.Engine, name, src string) *jscriptx.Script {
|
||||
t.Helper()
|
||||
s, err := e.Compile(name, src)
|
||||
if err != nil {
|
||||
t.Fatalf("编译 %s 失败: %v", name, err)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func TestCall_基本调用(t *testing.T) {
|
||||
e := newEngine(t)
|
||||
s := mustCompile(t, e, "calc", `
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
package jscriptx
|
||||
|
||||
// 这是**内部测试**(package jscriptx,不是 jscriptx_test):它要直接断言
|
||||
// vmHandle.scoped 这个未导出字段。行为测试盖不住那条——borrowStatic 那条路
|
||||
// 靠 finish 永远丢弃兜底,漏标 scoped 从外面看不出来,标志位本身才是真相。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
Reference in New Issue
Block a user