Files
jscriptx/script_static_test.go
what 7c9a70712e 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
挪成外部测试,挪不动。
2026-09-10 15:39:25 +08:00

315 lines
8.5 KiB
Go
Raw Permalink 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)
}
}