package jscriptx // 这是**内部测试**(package jscriptx,不是 jscriptx_test):它要直接断言 // vmHandle.scoped 这个未导出字段。行为测试盖不住那条——borrowStatic 那条路 // 靠 finish 永远丢弃兜底,漏标 scoped 从外面看不出来,标志位本身才是真相。 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) } } }