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→%d,Dropped %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) } }