Files
jscriptx/scope_pool_test.go
T
what 111665d2e7 fix: HasStatic 让 Stats 越用越偏;borrowStatic 漏标 scoped
两个都是静态调用那条路上的。

HasStatic 建了 VM 却不记丢弃。Created 在 load 里加(所有建 VM 的路径都走它),
Dropped 在 staticTarget.finish 里加,而 HasStatic 拿到 VM 后直接返回,不走
finish。每调一次 Created-Dropped 就永久 +1——Stats 是导出的观测结构,这个
偏差会直接体现在监控上。改成走同一套记账。

borrowStatic 同样会注入作用域扩展,却不设 vm.scoped。release 靠这个标志拒绝
把带扩展的 VM 放回池子,漏标就是跨调用泄漏。今天不出事只是因为
staticTarget.finish 永远丢弃——同一个约束靠两套机制守,以后有人把静态 VM
接进 release 就会漏。

测试:
  - Stats 那条断言「在飞的 VM 数」调用前后不变
  - scoped 那条是**内部测试**,直接断言标志位。行为测试盖不住它——borrowStatic
    那条路今天靠 finish 兜底,从外面看不出漏标
  两条都做了破坏性验证,摘掉修复会红。

顺带把 WithTimeout 的文档补上一句:要新建 VM 的路径上是顶层和函数调用两段
各自计时,墙钟上限是 2×d。行为不改——两段各自计时是合理的,缺的只是说明。
2026-09-10 15:21:17 +08:00

222 lines
5.9 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
import (
"context"
"strings"
"sync"
"testing"
)
// recorder 是个带状态的扩展,用来验证作用域之间有没有串。
type recorder struct {
mu sync.Mutex
tag string
seen []string
}
func (*recorder) Name() string { return "rec" }
func (*recorder) Module() (string, string) { return "", "" }
func (r *recorder) Bindings() map[string]any {
return map[string]any{
"tag": func() string { return r.tag },
"note": func(m string) {
r.mu.Lock()
defer r.mu.Unlock()
r.seen = append(r.seen, m)
},
}
}
func scoped(t *testing.T, r *recorder) context.Context {
t.Helper()
return WithScope(context.Background(), ScopeExtensions(r))
}
// Script.Call 走的是共享池,以前不注入作用域扩展,脚本顶层 import 直接
// ReferenceError。现在带作用域就单造一个 VM。
func TestScopePool_池化调用也能拿到扩展(t *testing.T) {
e, _ := New(WithLogger(nil))
defer e.Close()
s, err := e.Compile("s.ts", `
rec.note("顶层:" + rec.tag())
export default function () { return rec.tag() }`)
if err != nil {
t.Fatal(err)
}
r := &recorder{tag: "A"}
got, err := s.Call(scoped(t, r), "")
if err != nil {
t.Fatalf("带作用域的池化调用该能用扩展: %v", err)
}
if got != "A" {
t.Errorf("拿到 %#v", got)
}
if len(r.seen) != 1 || r.seen[0] != "顶层:A" {
t.Errorf("模块顶层没跑或没拿到扩展: %v", r.seen)
}
}
// 关键:带作用域的 VM 用完必须丢,不能回池——回池就把上一个作用域的扩展
// 漏给下一个调用了。
func TestScopePool_作用域之间不串(t *testing.T) {
e, _ := New(WithLogger(nil))
defer e.Close()
s, _ := e.Compile("s.ts", `export default function () { return rec.tag() }`)
for _, want := range []string{"A", "B", "C"} {
r := &recorder{tag: want}
got, err := s.Call(scoped(t, r), "")
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("拿到的是别的作用域的扩展: 想要 %q,拿到 %#v", want, got)
}
}
}
// 不带作用域的调用照旧走池,行为不变。
func TestScopePool_无作用域仍然走池(t *testing.T) {
e, _ := New(WithLogger(nil))
defer e.Close()
s, _ := e.Compile("s.ts", `export default function () { return 1 }`)
ctx := context.Background()
for range 5 {
if _, err := s.Call(ctx, ""); err != nil {
t.Fatal(err)
}
}
// 第一次建一个,后面四次都该命中池
if st := s.Stats(); st.Dropped != 0 {
t.Errorf("不带作用域不该丢 VMDropped = %d", st.Dropped)
}
}
// 带作用域的 VM 每次都丢,不占池子。
func TestScopePool_带作用域的VM不回池(t *testing.T) {
e, _ := New(WithLogger(nil))
defer e.Close()
s, _ := e.Compile("s.ts", `export default function () { return rec.tag() }`)
before := s.Stats().Dropped
for range 3 {
if _, err := s.Call(scoped(t, &recorder{tag: "X"}), ""); err != nil {
t.Fatal(err)
}
}
if got := s.Stats().Dropped - before; got != 3 {
t.Errorf("三次带作用域的调用该丢三个 VM,实际丢了 %d", got)
}
}
// 没有扩展、只有 key 的作用域也不能走池:脚本能读到 scope.key,
// 池化 VM 会把上一个作用域的 key 漏给下一个调用。
func TestScopePool_只有key的作用域也不走池(t *testing.T) {
e, _ := New(WithLogger(nil))
defer e.Close()
s, _ := e.Compile("s.ts", `export default function () { return scope.key }`)
for _, want := range []string{"k1", "k2", "k3"} {
ctx := WithScope(context.Background(), ScopeKey(want))
got, err := s.Call(ctx, "")
if err != nil {
t.Fatal(err)
}
if got != want {
t.Fatalf("拿到的是别的作用域的 key: 想要 %q,拿到 %#v", want, got)
}
}
}
// 并发下各作用域看到的必须是自己那份。
func TestScopePool_并发不串(t *testing.T) {
e, _ := New(WithLogger(nil))
defer e.Close()
s, _ := e.Compile("s.ts", `export default function () { return rec.tag() }`)
var wg sync.WaitGroup
errs := make(chan string, 64)
for i := range 32 {
wg.Add(1)
go func() {
defer wg.Done()
want := string(rune('a' + i%26))
got, err := s.Call(scoped(t, &recorder{tag: want}), "")
if err != nil {
errs <- err.Error()
return
}
if got != want {
errs <- "串了: 想要 " + want + ",拿到 " + got.(string)
}
}()
}
wg.Wait()
close(errs)
var bad []string
for e := range errs {
bad = append(bad, e)
}
if len(bad) > 0 {
t.Errorf("%d 次出错:\n%s", len(bad), strings.Join(bad, "\n"))
}
}
// 取 VM 的每一条路径,带作用域时都必须把 scoped 标上。
//
// scoped 决定 release 会不会把 VM 放回池子。漏标的后果是**跨调用泄漏作用域扩展**:
// 下一个不带作用域的调用会看见上一个作用域的 store。
//
// 这条用内部测试直接断言标志位,而不是测「有没有漏」——borrowStatic 那条路今天
// 靠 staticTarget.finish 永远丢弃兜底,行为上看不出漏标,等于同一个约束靠两套
// 机制守。标志位本身才是唯一的真相。
func TestVM_每条取VM的路径都标了scoped(t *testing.T) {
e, err := New(WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("c.ts", `export default class C { static S() { return 1 } Run() { return 1 } }`)
if err != nil {
t.Fatal(err)
}
scoped := WithScope(context.Background(), ScopeKey("k"))
plain := context.Background()
for _, c := range []struct {
name string
get func(ctx context.Context) (*vmHandle, error)
}{
{"borrow(池化路径)", s.borrow},
{"borrowStatic(静态路径)", s.borrowStatic},
} {
vm, err := c.get(scoped)
if err != nil {
t.Fatalf("%s: %v", c.name, err)
}
if !vm.scoped {
t.Errorf("%s 在有作用域时没标 scoped——这个 VM 会被放回池子,"+
"把作用域的扩展漏给下一个调用", c.name)
}
vm, err = c.get(plain)
if err != nil {
t.Fatalf("%s(无作用域): %v", c.name, err)
}
if vm.scoped {
t.Errorf("%s 没有作用域却标了 scoped——白白丢弃可复用的 VM", c.name)
}
}
}