两个都是静态调用那条路上的。
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。行为不改——两段各自计时是合理的,缺的只是说明。
222 lines
5.9 KiB
Go
222 lines
5.9 KiB
Go
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("不带作用域不该丢 VM,Dropped = %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)
|
||
}
|
||
}
|
||
}
|