改完之后「某个行为在哪测」就是查同名文件:
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
挪成外部测试,挪不动。
687 lines
20 KiB
Go
687 lines
20 KiB
Go
package jscriptx_test
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"slices"
|
||
"sync"
|
||
"testing"
|
||
"time"
|
||
|
||
"git.fsdpf.net/go/jscriptx"
|
||
"github.com/dop251/goja"
|
||
)
|
||
|
||
// 这一组**不测本库,测 goja**——它是「为什么这个库要这么设计」的实验记录。
|
||
//
|
||
// 三件事:
|
||
//
|
||
// 1. Program 无状态,可以给任意多个 Runtime 共用——本库共用编译结果的前提
|
||
// 2. **模块顶层的变量跟着 Runtime 走,不是共享状态**
|
||
// 3. Go 的值进到脚本里长什么样(切片是不是真数组、nil 是 null 还是 undefined)
|
||
//
|
||
// 第 2 条是最容易误解的地方。它导致:一个脚本串行开发时表现正常(一直命中同一个
|
||
// Runtime),并发起来行为却随调度漂移——有几个 Runtime 就有几份独立的顶层状态,
|
||
// 一次调用落到哪个 Runtime 取决于当时的 goroutine 调度。
|
||
//
|
||
// 所以要在并发之间共享状态,只能把它放到 Go 侧(写一个扩展,见 Extension 接口):
|
||
// Go 对象可以被每个 Runtime 各自包一层,并发安全由 Go 自己保证。
|
||
//
|
||
// 第 3 条脚本作者是照着写代码的,一旦变了,业务脚本会**静默**走错分支(比如查出来的
|
||
// 记录突然 Array.isArray 为 false,整段列表被当成空的)。
|
||
//
|
||
// 这些行为变了,这一组会先红——那时候要改的是本库的设计,不是这里的断言。
|
||
// 其中有一条是哨兵:goja 哪天允许对象跨 Runtime 了,它会失败,提醒我们可以简化设计。
|
||
|
||
const gojaCounter = `
|
||
var counter = 0;
|
||
function bump() { return ++counter }
|
||
bump`
|
||
|
||
// compileCounter 编一份 Program。它是无状态的,几个用例共用同一份。
|
||
func compileCounter(t *testing.T) *goja.Program {
|
||
t.Helper()
|
||
prog, err := goja.Compile("counter.js", gojaCounter, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return prog
|
||
}
|
||
|
||
// bumpOf 在一个新 Runtime 里跑一遍 Program,拿到那个闭包函数。
|
||
func bumpOf(t *testing.T, prog *goja.Program) goja.Callable {
|
||
t.Helper()
|
||
rt := goja.New()
|
||
v, err := rt.RunProgram(prog)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fn, ok := goja.AssertFunction(v)
|
||
if !ok {
|
||
t.Fatalf("Program 的完成值不是函数: %v", v)
|
||
}
|
||
return fn
|
||
}
|
||
|
||
func callBump(t *testing.T, fn goja.Callable) int64 {
|
||
t.Helper()
|
||
v, err := fn(goja.Undefined())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return v.ToInteger()
|
||
}
|
||
|
||
// 同一个 Runtime 反复调用,顶层变量一路累加——闭包还在,Program 不会重跑。
|
||
func TestGoja_同一个Runtime里顶层变量会累加(t *testing.T) {
|
||
fn := bumpOf(t, compileCounter(t))
|
||
|
||
for i := int64(1); i <= 5; i++ {
|
||
if got := callBump(t, fn); got != i {
|
||
t.Fatalf("第 %d 次调用该拿到 %d,拿到 %d", i, i, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 换一个 Runtime 就是另一份顶层变量,哪怕用的是同一个 Program。
|
||
// 这正是「N 个 VM = N 份独立副本」的根源。
|
||
func TestGoja_不同Runtime各有一份顶层变量(t *testing.T) {
|
||
prog := compileCounter(t)
|
||
|
||
a := bumpOf(t, prog)
|
||
b := bumpOf(t, prog)
|
||
|
||
for range 3 { // a 先数到 3
|
||
callBump(t, a)
|
||
}
|
||
if got := callBump(t, b); got != 1 { // b 是全新的
|
||
t.Errorf("另一个 Runtime 该从 1 开始,拿到 %d", got)
|
||
}
|
||
if got := callBump(t, a); got != 4 { // a 不受影响
|
||
t.Errorf("原来那个 Runtime 该接着数到 4,拿到 %d", got)
|
||
}
|
||
}
|
||
|
||
// Program 可以并发给任意多个 Runtime 用。32 个各调一次,全都拿到 1
|
||
// ——32 份独立的顶层状态,互不影响。
|
||
func TestGoja_Program可以并发共用(t *testing.T) {
|
||
prog := compileCounter(t)
|
||
|
||
var mu sync.Mutex
|
||
var got []int64
|
||
var wg sync.WaitGroup
|
||
|
||
for range 32 {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
rt := goja.New()
|
||
v, err := rt.RunProgram(prog) // 同一份 Program,不同 Runtime
|
||
if err != nil {
|
||
t.Error(err)
|
||
return
|
||
}
|
||
fn, ok := goja.AssertFunction(v)
|
||
if !ok {
|
||
t.Error("完成值不是函数")
|
||
return
|
||
}
|
||
r, err := fn(goja.Undefined())
|
||
if err != nil {
|
||
t.Error(err)
|
||
return
|
||
}
|
||
mu.Lock()
|
||
got = append(got, r.ToInteger())
|
||
mu.Unlock()
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
|
||
fmt.Println(got)
|
||
|
||
if len(got) != 32 {
|
||
t.Fatalf("该有 32 个结果,拿到 %d 个", len(got))
|
||
}
|
||
for _, v := range got {
|
||
if v != 1 {
|
||
t.Fatalf("每个 Runtime 都是新的,该全是 1,出现了 %d;全部结果 %v", v, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 对象不能跨 Runtime 传。这是 goja 的硬限制,也是「JS 内部变量没法并发共享」的根据。
|
||
//
|
||
// 维护者在 dop251/goja#232 里说得很直白:非原始类型的 goja.Value 连并发只读都不安全,
|
||
// 用 Proxy 包一层也不行,因为 handler 最终还是要访问原来那个值。
|
||
//
|
||
// 这条测试是个哨兵:哪天 goja 放开了这个限制,它会失败,提醒我们重新评估
|
||
// 「共享状态必须放 Go 侧」这个结论。
|
||
func TestGoja_对象不能跨Runtime传(t *testing.T) {
|
||
rt1 := goja.New()
|
||
obj, err := rt1.RunString(`var o = { n: 1 }; o`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
rt2 := goja.New()
|
||
setErr := rt2.Set("borrowed", obj) // 把 rt1 的对象塞进 rt2
|
||
_, runErr := rt2.RunString(`borrowed.n`)
|
||
|
||
if setErr == nil && runErr == nil {
|
||
t.Fatal("跨 Runtime 传对象居然成功了——goja 的限制变了," +
|
||
"本库关于「共享状态只能放 Go 侧」的结论需要重新评估")
|
||
}
|
||
t.Logf("Set 的结果: %v", setErr)
|
||
t.Logf("读取的结果: %v", runErr)
|
||
}
|
||
|
||
// 原始值不受这条限制——字符串、数字、布尔不是对象,可以跨 Runtime 传。
|
||
func TestGoja_原始值可以跨Runtime传(t *testing.T) {
|
||
rt1 := goja.New()
|
||
v, err := rt1.RunString(`"hello"`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
rt2 := goja.New()
|
||
if err := rt2.Set("s", v); err != nil {
|
||
t.Fatalf("原始值该能传: %v", err)
|
||
}
|
||
got, err := rt2.RunString(`s + " world"`)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got.String() != "hello world" {
|
||
t.Errorf("拿到 %q", got.String())
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 并发之间要真正共享一个计数器(1,2,3… 不重不漏)该怎么做
|
||
// ============================================================================
|
||
|
||
// sharedCounter 是放在 Go 侧的计数器,并发安全由它自己保证。
|
||
// 每个 Runtime 各自把它包一层——这是 goja 维护者认可的唯一共享方式。
|
||
type sharedCounter struct {
|
||
mu sync.Mutex
|
||
n int64
|
||
}
|
||
|
||
func (c *sharedCounter) Bump() int64 {
|
||
c.mu.Lock()
|
||
defer c.mu.Unlock()
|
||
c.n++
|
||
return c.n
|
||
}
|
||
|
||
// 方案一:状态放 Go 侧,Runtime 各自独立。**推荐这条。**
|
||
//
|
||
// 32 个 goroutine 各有各的 Runtime(互不阻塞),但 Set 进去的是同一个 Go 对象,
|
||
// 所以 Bump() 拿到的是同一个计数器:结果正好 1..32,不重不漏。
|
||
//
|
||
// 只有 Bump 里那把 Go 锁是串行的,脚本执行本身全程并行。
|
||
// 把它包成扩展之后,脚本侧写 store.Incr("counter", 1) 就行——本仓库的
|
||
// internal/testext 是个最小示例,框架层那份在 framework 的 internal/jscript/ext/store。
|
||
func TestGoja_共享状态放Go侧(t *testing.T) {
|
||
const n = 32
|
||
|
||
prog, err := goja.Compile("shared.js", `
|
||
function bump() { return shared.Bump() }
|
||
bump`, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
counter := &sharedCounter{} // 进程级,只有一个
|
||
got := runConcurrent(t, n, func() (int64, error) {
|
||
rt := goja.New()
|
||
if err := rt.Set("shared", counter); err != nil { // 同一个指针,各包一层
|
||
return 0, err
|
||
}
|
||
v, err := rt.RunProgram(prog)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
fn, ok := goja.AssertFunction(v)
|
||
if !ok {
|
||
return 0, fmt.Errorf("完成值不是函数")
|
||
}
|
||
r, err := fn(goja.Undefined())
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return r.ToInteger(), nil
|
||
})
|
||
|
||
fmt.Println("方案一 共享 Go 对象:", got)
|
||
assertSequence(t, got, n)
|
||
}
|
||
|
||
// 方案二:共用一个 Runtime,靠锁串行。
|
||
//
|
||
// 这样 JS 里的 counter 是真共享的——因为只有一个 Runtime。代价是**所有脚本执行
|
||
// 完全串行**:脚本跑 1ms,32 个并发就是 32ms 串起来。
|
||
//
|
||
// 除非脚本极快且并发很低,否则别走这条。状态放 Go 侧更安全,也不用牺牲并发。
|
||
func TestGoja_共用一个Runtime靠锁串行(t *testing.T) {
|
||
const n = 32
|
||
|
||
prog, err := goja.Compile("locked.js", `
|
||
var counter = 0;
|
||
function bump() { return ++counter }
|
||
bump`, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
rt := goja.New()
|
||
v, err := rt.RunProgram(prog)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fn, ok := goja.AssertFunction(v)
|
||
if !ok {
|
||
t.Fatal("完成值不是函数")
|
||
}
|
||
|
||
var rtMu sync.Mutex // Runtime 不是并发安全的,这把锁不能省
|
||
got := runConcurrent(t, n, func() (int64, error) {
|
||
rtMu.Lock()
|
||
defer rtMu.Unlock()
|
||
r, err := fn(goja.Undefined())
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return r.ToInteger(), nil
|
||
})
|
||
|
||
fmt.Println("方案二 共用 Runtime:", got)
|
||
assertSequence(t, got, n)
|
||
}
|
||
|
||
// runConcurrent 并发跑 n 次 do,收集结果并排序。
|
||
func runConcurrent(t *testing.T, n int, do func() (int64, error)) []int64 {
|
||
t.Helper()
|
||
|
||
var mu sync.Mutex
|
||
out := make([]int64, 0, n)
|
||
var wg sync.WaitGroup
|
||
|
||
for range n {
|
||
wg.Add(1)
|
||
go func() {
|
||
defer wg.Done()
|
||
v, err := do()
|
||
if err != nil {
|
||
t.Error(err)
|
||
return
|
||
}
|
||
mu.Lock()
|
||
out = append(out, v)
|
||
mu.Unlock()
|
||
}()
|
||
}
|
||
wg.Wait()
|
||
|
||
slices.Sort(out)
|
||
return out
|
||
}
|
||
|
||
// assertSequence 断言结果正好是 1..n,不重不漏。
|
||
func assertSequence(t *testing.T, got []int64, n int) {
|
||
t.Helper()
|
||
|
||
if len(got) != n {
|
||
t.Fatalf("该有 %d 个结果,拿到 %d 个: %v", n, len(got), got)
|
||
}
|
||
for i, v := range got {
|
||
if v != int64(i+1) {
|
||
t.Fatalf("该是 1..%d 的连续序列,第 %d 个是 %d;全部结果 %v", n, i+1, v, got)
|
||
}
|
||
}
|
||
}
|
||
|
||
// ============================================================================
|
||
// 脚本里 +1 之后阻塞会怎样:两个方案在这里差别最大
|
||
// ============================================================================
|
||
|
||
const blockOnce = 100 * time.Millisecond
|
||
|
||
// sleeper 暴露给脚本的阻塞函数,模拟脚本里做 IO(查库、调接口)。
|
||
func sleeper(d time.Duration) func() { return func() { time.Sleep(d) } }
|
||
|
||
// 方案一:计数器在 Go 侧,Runtime 各自独立。
|
||
//
|
||
// 脚本先 +1 拿到自己的号,再阻塞。16 个各睡 20ms,总耗时接近 20ms 而不是 320ms
|
||
// ——一个脚本卡在 IO 上,不挡别人。而且 +1 在阻塞之前就已经对其他人可见了。
|
||
func TestGoja_阻塞时独立Runtime并行(t *testing.T) {
|
||
const n = 16
|
||
|
||
prog, err := goja.Compile("blocking.js", `
|
||
function bump() {
|
||
var mine = shared.Bump(); // 先 +1,拿到自己的号
|
||
sleep(); // 再阻塞
|
||
return mine
|
||
}
|
||
bump`, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
counter := &sharedCounter{}
|
||
sleep := sleeper(blockOnce)
|
||
start := time.Now()
|
||
|
||
got := runConcurrent(t, n, func() (int64, error) {
|
||
rt := goja.New()
|
||
if err := rt.Set("shared", counter); err != nil {
|
||
return 0, err
|
||
}
|
||
if err := rt.Set("sleep", sleep); err != nil {
|
||
return 0, err
|
||
}
|
||
v, err := rt.RunProgram(prog)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
fn, ok := goja.AssertFunction(v)
|
||
if !ok {
|
||
return 0, fmt.Errorf("完成值不是函数")
|
||
}
|
||
r, err := fn(goja.Undefined())
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return r.ToInteger(), nil
|
||
})
|
||
|
||
elapsed := time.Since(start)
|
||
fmt.Printf("方案一 独立 Runtime: %d 个各阻塞 %v,总耗时 %v\n",
|
||
n, blockOnce, elapsed.Round(time.Millisecond))
|
||
|
||
fmt.Println(got)
|
||
|
||
assertSequence(t, got, n)
|
||
|
||
if limit := blockOnce * n / 2; elapsed > limit {
|
||
t.Errorf("独立 Runtime 该并行,总耗时 %v 超过了 %v——像是串行了", elapsed, limit)
|
||
}
|
||
}
|
||
|
||
// 方案二:计数器就在 JS 里,共用一个 Runtime。
|
||
//
|
||
// 脚本先 counter++(JS 侧的变量,真共享),再阻塞。同样 16 个各睡 20ms,
|
||
// 总耗时接近 320ms:Runtime 不是并发安全的,锁必须一直持到脚本返回,
|
||
// **脚本在 IO 上卡多久,锁就被占多久**,包括 +1 之后那段。
|
||
//
|
||
// 所以"共用 Runtime 换 JS 侧共享状态"的代价不是慢一点,是并发归零。
|
||
func TestGoja_阻塞时共用Runtime串行(t *testing.T) {
|
||
const n = 16
|
||
|
||
// 导出两个方法:最后一个表达式是什么,RunProgram 就返回什么。
|
||
// 想导出多个就返回一个对象——注意外面要包一层括号,
|
||
// 不然 { 会被当成语句块而不是对象字面量。
|
||
prog, err := goja.Compile("blocking.js", `
|
||
var counter = 0;
|
||
function bump() {
|
||
counter++; // 先 +1,这是 JS 里的变量,共用 Runtime 才共享得了
|
||
sleep(); // 再阻塞
|
||
return counter
|
||
}
|
||
function read() { return counter } // 不 +1,只读当前值
|
||
({ bump: bump, read: read })`, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
rt := goja.New()
|
||
if err := rt.Set("sleep", sleeper(blockOnce)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
v, err := rt.RunProgram(prog)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
exports := v.ToObject(rt)
|
||
fn, ok := goja.AssertFunction(exports.Get("bump"))
|
||
if !ok {
|
||
t.Fatal("bump 不是函数")
|
||
}
|
||
read, ok := goja.AssertFunction(exports.Get("read"))
|
||
if !ok {
|
||
t.Fatal("read 不是函数")
|
||
}
|
||
|
||
var rtMu sync.Mutex
|
||
start := time.Now()
|
||
|
||
got := runConcurrent(t, n, func() (int64, error) {
|
||
rtMu.Lock()
|
||
defer rtMu.Unlock() // 锁持到脚本返回,包括 +1 之后阻塞的那段
|
||
r, err := fn(goja.Undefined())
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
return r.ToInteger(), nil
|
||
})
|
||
|
||
fmt.Println(got)
|
||
|
||
elapsed := time.Since(start)
|
||
fmt.Printf("方案二 共用 Runtime: %d 个各阻塞 %v,总耗时 %v\n",
|
||
n, blockOnce, elapsed.Round(time.Millisecond))
|
||
|
||
assertSequence(t, got, n)
|
||
|
||
if floor := blockOnce * n * 4 / 5; elapsed < floor {
|
||
t.Errorf("共用 Runtime 必然串行,总耗时才 %v,不到 %v——是不是没锁住", elapsed, floor)
|
||
}
|
||
|
||
// 全部并发结束后读最终值。读也要加锁——它跟 bump 用的是同一个 Runtime。
|
||
rtMu.Lock()
|
||
final, err := read(goja.Undefined())
|
||
rtMu.Unlock()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fmt.Printf(" 并发结束后 read(): %d\n", final.ToInteger())
|
||
|
||
if got := final.ToInteger(); got != int64(n) {
|
||
t.Errorf("%d 个并发各 +1 一次,最终该是 %d,读到 %d", n, n, got)
|
||
}
|
||
}
|
||
|
||
// 对照:独立 Runtime + JS 侧的 counter++。
|
||
//
|
||
// 跟"共用 Runtime"那条用的是**完全相同的脚本**,只有 Runtime 的用法不同。
|
||
// 结果:并行是并行了(~20ms 而不是 320ms),但 16 个全拿到 1
|
||
// ——每个 Runtime 一份自己的 counter,压根没共享。
|
||
//
|
||
// 这条和前两条合起来是个 2×2:
|
||
//
|
||
// JS 侧 counter Go 侧 counter
|
||
// 独立 Runtime 全是 1,并行 ✗ 1..16,并行 ✓
|
||
// 共用 Runtime 1..16,串行 ✗ (没必要,白白串行)
|
||
//
|
||
// 只有右上角那格两头都占。
|
||
func TestGoja_独立Runtime里的JS计数器不共享(t *testing.T) {
|
||
const n = 16
|
||
|
||
// 跟 TestGoja_阻塞时共用Runtime串行 用的是同一段脚本
|
||
prog, err := goja.Compile("blocking.js", `
|
||
var counter = 0;
|
||
function bump() {
|
||
counter++;
|
||
sleep();
|
||
return counter
|
||
}
|
||
function read() { return counter }
|
||
({ bump: bump, read: read })`, false)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
sleep := sleeper(blockOnce)
|
||
|
||
// 把每个 Runtime 的 read 留下来,跑完再各读各的
|
||
var readMu sync.Mutex
|
||
var readers []goja.Callable
|
||
|
||
start := time.Now()
|
||
|
||
got := runConcurrent(t, n, func() (int64, error) {
|
||
rt := goja.New() // 每个 goroutine 一个 Runtime
|
||
if err := rt.Set("sleep", sleep); err != nil {
|
||
return 0, err
|
||
}
|
||
v, err := rt.RunProgram(prog)
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
exports := v.ToObject(rt)
|
||
fn, ok := goja.AssertFunction(exports.Get("bump"))
|
||
if !ok {
|
||
return 0, fmt.Errorf("bump 不是函数")
|
||
}
|
||
read, ok := goja.AssertFunction(exports.Get("read"))
|
||
if !ok {
|
||
return 0, fmt.Errorf("read 不是函数")
|
||
}
|
||
|
||
r, err := fn(goja.Undefined())
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
|
||
readMu.Lock()
|
||
readers = append(readers, read)
|
||
readMu.Unlock()
|
||
|
||
return r.ToInteger(), nil
|
||
})
|
||
|
||
elapsed := time.Since(start)
|
||
fmt.Printf("对照 独立 Runtime + JS 计数器: %v 总耗时 %v\n",
|
||
got, elapsed.Round(time.Millisecond))
|
||
|
||
// 并行了
|
||
if limit := blockOnce * n / 2; elapsed > limit {
|
||
t.Errorf("独立 Runtime 该并行,总耗时 %v 超过了 %v", elapsed, limit)
|
||
}
|
||
// 但没共享:每个 Runtime 自己的 counter 都是第一次 +1
|
||
for _, v := range got {
|
||
if v != 1 {
|
||
t.Fatalf("每个 Runtime 一份 counter,该全是 1,出现了 %d;全部结果 %v", v, got)
|
||
}
|
||
}
|
||
|
||
// 全部结束后各读各的:16 个 Runtime,16 份 counter,每份都是 1。
|
||
// 对比共用 Runtime 那条——那边最终读到 16。
|
||
//
|
||
// 每个 Runtime 只有创建它的那条路径碰过,此刻也没人在跑,所以读不用加锁。
|
||
finals := make([]int64, 0, len(readers))
|
||
for _, read := range readers {
|
||
v, err := read(goja.Undefined())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
finals = append(finals, v.ToInteger())
|
||
}
|
||
fmt.Printf(" 并发结束后各自 read(): %v\n", finals)
|
||
|
||
for i, v := range finals {
|
||
if v != 1 {
|
||
t.Errorf("第 %d 个 Runtime 的 counter 该是 1,读到 %d", i, v)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|
||
}
|