Files
jscriptx/static_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

315 lines
8.5 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"
"strings"
"testing"
"git.fsdpf.net/go/jscriptx"
"git.fsdpf.net/go/jscriptx/internal/testext"
)
// ctorSpy 记录 constructor 有没有被跑过。
type ctorSpy struct{ built, startups int }
func (s *ctorSpy) Name() string { return "spy" }
func (s *ctorSpy) Module() (string, string) { return "", "" }
func (s *ctorSpy) Bindings() map[string]any {
return map[string]any{
"built": func() { s.built++ },
"startup": func() { s.startups++ },
}
}
const staticScript = `
export default class C {
constructor() { spy.built() }
static Startup() { spy.startup(); return "started" }
Execute() { return "executed" }
}`
func newStaticScript(t *testing.T) (*jscriptx.Engine, *jscriptx.Script, *ctorSpy, context.Context) {
t.Helper()
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
t.Cleanup(e.Close)
s, err := e.Compile("c.ts", staticScript)
if err != nil {
t.Fatal(err)
}
spy := &ctorSpy{}
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(spy))
return e, s, spy, ctx
}
// 静态方法调得到,而且**不跑 constructor**。
func TestStatic_调静态方法不构造实例(t *testing.T) {
_, s, spy, ctx := newStaticScript(t)
got, err := s.CallStatic(ctx, "Startup")
if err != nil {
t.Fatal(err)
}
if got != "started" {
t.Errorf("返回值 %#v", got)
}
if spy.startups != 1 {
t.Errorf("Startup 该跑一次,跑了 %d 次", spy.startups)
}
if spy.built != 0 {
t.Errorf("不该构造实例,constructor 却跑了 %d 次", spy.built)
}
}
// 静态方法里能用作用域扩展——这正是它的用武之地(配置 Go 侧的东西)。
func TestStatic_能用作用域扩展(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("c.ts", `
export default class C {
static Startup() { return spy.tag() }
}`)
if err != nil {
t.Fatal(err)
}
ext := tagExt{"来自扩展"}
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(ext))
got, err := s.CallStatic(ctx, "Startup")
if err != nil {
t.Fatal(err)
}
if got != "来自扩展" {
t.Errorf("拿到 %#v", got)
}
}
type tagExt struct{ v string }
func (tagExt) Name() string { return "spy" }
func (tagExt) Module() (string, string) { return "", "" }
func (e tagExt) Bindings() map[string]any {
return map[string]any{"tag": func() string { return e.v }}
}
// 没写这个静态方法时要能被 ErrFuncNotFound 认出来——生命周期钩子多半是可选的。
func TestStatic_没写时报ErrFuncNotFound(t *testing.T) {
_, s, _, ctx := newStaticScript(t)
_, err := s.CallStatic(ctx, "Shutdown")
if !errors.Is(err, jscriptx.ErrFuncNotFound) {
t.Fatalf("该能被 ErrFuncNotFound 匹配上: %v", err)
}
}
// 实例方法不算静态方法,别把两者搞混。
func TestStatic_实例方法不算静态(t *testing.T) {
_, s, spy, ctx := newStaticScript(t)
_, err := s.CallStatic(ctx, "Execute")
if !errors.Is(err, jscriptx.ErrFuncNotFound) {
t.Fatalf("Execute 是实例方法,不该被静态调用找到: %v", err)
}
if spy.built != 0 {
t.Error("找不到也不该构造实例")
}
}
// 静态方法抛异常时要把错误报出来,不能跟"没写"混为一谈。
func TestStatic_抛异常时报错(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("c.ts", `
export default class C {
static Startup() { throw new Error("配置缺失") }
}`)
if err != nil {
t.Fatal(err)
}
_, err = s.CallStatic(context.Background(), "Startup")
if err == nil {
t.Fatal("该报错")
}
if errors.Is(err, jscriptx.ErrFuncNotFound) {
t.Error("这是执行出错,不是没写")
}
if !strings.Contains(err.Error(), "配置缺失") {
t.Errorf("错误里该带上脚本抛的内容: %v", err)
}
}
// 静态方法建的 JS 对象活不下来:VM 用完就丢,后续调用看不到。
func TestStatic_建的对象活不下来(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("c.ts", `
let prepared = "没跑过 Startup"
export default class C {
static Startup() { prepared = "Startup 跑过了"; return prepared }
Read() { return prepared }
}`)
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
if got, err := s.CallStatic(ctx, "Startup"); err != nil || got != "Startup 跑过了" {
t.Fatalf("got=%#v err=%v", got, err)
}
// 换一个 VM 读,看不到 Startup 改的东西
obj, err := s.New(ctx)
if err != nil {
t.Fatal(err)
}
defer obj.Close()
got, err := obj.Call(ctx, "Read")
if err != nil {
t.Fatal(err)
}
if got != "没跑过 Startup" {
t.Errorf("静态方法改的顶层变量不该跨 VM 存活,读到 %#v", got)
}
}
// HasStatic 只认静态方法。
func TestStatic_HasStatic(t *testing.T) {
_, s, spy, _ := newStaticScript(t)
if !s.HasStatic("Startup") {
t.Error("Startup 是静态方法,该认出来")
}
if s.HasStatic("Execute") {
t.Error("Execute 是实例方法,不该算静态")
}
if s.HasStatic("Nope") {
t.Error("不存在的名字不该算")
}
if spy.built != 0 {
t.Error("HasStatic 不该构造实例")
}
}
// 脚本导出的不是 class 时,静态调用要老实说找不到。
func TestStatic_非class脚本(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("c.ts", `export default function () { return 1 }`)
if err != nil {
t.Fatal(err)
}
if s.HasStatic("Startup") {
t.Error("函数导出没有静态方法")
}
if _, err := s.CallStatic(context.Background(), "Startup"); !errors.Is(err, jscriptx.ErrFuncNotFound) {
t.Errorf("该报 ErrFuncNotFound: %v", err)
}
}
// HasStatic 建了 VM 就得记一笔丢弃,否则 Stats 会越用越偏。
//
// Created 在 load 里加(所有建 VM 的路径都走它),Dropped 在 staticTarget.finish
// 里加。HasStatic 不走 finish,漏掉的话每调一次 Created-Dropped 就永久 +1——
// Stats 是导出的观测结构,这个偏差会直接体现在监控上。
func TestStatic_HasStatic不让Stats跑偏(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("c.ts", `
export default class C {
static Startup() { return 1 }
Run() { return 2 }
}`)
if err != nil {
t.Fatal(err)
}
before := s.Stats()
inFlight := before.Created - before.Dropped - int64(before.Pooled)
for range 5 {
if !s.HasStatic("Startup") {
t.Fatal("该找得到静态方法")
}
}
after := s.Stats()
if got := after.Created - after.Dropped - int64(after.Pooled); got != inFlight {
t.Errorf("调了 5 次 HasStatic 之后在飞的 VM 数变成 %d,本该还是 %d\n"+
"Created %d→%dDropped %d→%d)——HasStatic 建了 VM 却没记丢弃",
got, inFlight, before.Created, after.Created, before.Dropped, after.Dropped)
}
}
// 带作用域时,静态调用拿到的 VM 必须标成 scoped。
//
// scoped 的 VM 带着某个作用域的扩展,回池就会漏给下一个调用。borrow 那条路径
// 设了这个标志,borrowStatic 同样注入作用域扩展却没设——今天不出事只是因为
// staticTarget.finish 永远丢弃,等于同一个约束靠两套机制守。
func TestStatic_作用域VM要标记为不可回池(t *testing.T) {
e, err := jscriptx.New(jscriptx.WithLogger(nil))
if err != nil {
t.Fatal(err)
}
defer e.Close()
s, err := e.Compile("c.ts", `
export default class C {
static Startup() { return typeof store }
}`)
if err != nil {
t.Fatal(err)
}
st := testext.New()
ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st))
// 先在作用域下跑一次静态方法——这次会建一个带扩展的 VM
got, err := s.CallStatic(ctx, "Startup")
if err != nil {
t.Fatal(err)
}
if got != "object" {
t.Fatalf("作用域扩展没进去: %#v", got)
}
// 那个 VM 绝不能回池:池里但凡有一个,下一个**无作用域**的调用就会看见 store
if p := s.Stats().Pooled; p != 0 {
t.Fatalf("作用域 VM 回池了(池里有 %d 个),下一个调用会漏到别人的扩展", p)
}
leak, err := s.CallStatic(context.Background(), "Startup")
if err != nil {
t.Fatal(err)
}
if leak != "undefined" {
t.Errorf("无作用域的调用看见了 store(%#v)——上一次的作用域漏过来了", leak)
}
}