package jscriptx_test import ( "context" "errors" "testing" "time" "git.fsdpf.net/go/jscriptx" "git.fsdpf.net/go/jscriptx/internal/testext" ) const deviceScript = ` export default class DeviceHandler { constructor(deviceId, model) { this.deviceId = deviceId this.model = model this.count = 0 } onMessage(payload) { this.count++ return this.deviceId + "#" + this.count + ":" + payload } spin() { while (true) {} } } ` // 作用域的核心用途:同一个 ctx 下的多个脚本共享扩展。 func TestScope_多脚本共享扩展(t *testing.T) { e := newEngine(t) writer := mustCompile(t, e, "writer.ts", ` export default class Writer { put(k, v) { store.Set(k, v) } } `) reader := mustCompile(t, e, "reader.ts", ` export default class Reader { take(k) { return store.Get(k) } count() { return store.Len() } } `) st := testext.New() ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st)) w, err := writer.New(ctx) if err != nil { t.Fatal(err) } defer w.Close() r, err := reader.New(ctx) if err != nil { t.Fatal(err) } defer r.Close() if _, err := w.Call(ctx, "put", "k", "写进去的"); err != nil { t.Fatal(err) } got, err := r.Call(ctx, "take", "k") if err != nil { t.Fatal(err) } if got != "写进去的" { t.Errorf("另一个脚本读不到: %#v", got) } // Go 侧拿的是同一份 if st.Get("k") != "写进去的" { t.Errorf("Go 侧读不到: %#v", st.Get("k")) } st.Set("fromGo", 1) if got, _ := r.Call(ctx, "count"); toNumber(t, got) != 2 { t.Errorf("脚本看不到 Go 侧写的: %#v", got) } } // 不同作用域之间互不干扰。 func TestScope_作用域之间隔离(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "counter.ts", ` export default class C { bump() { return store.Incr("n", 1) } } `) st1, st2 := testext.New(), testext.New() ctx1 := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st1)) ctx2 := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st2)) o1, _ := s.New(ctx1) defer o1.Close() o2, _ := s.New(ctx2) defer o2.Close() o1.Call(ctx1, "bump") o1.Call(ctx1, "bump") got, err := o2.Call(ctx2, "bump") if err != nil { t.Fatal(err) } if toNumber(t, got) != 1 { t.Errorf("第二个作用域被污染: %#v", got) } } // 用已有的 SharedState 建 store,把 Go 侧正在用的状态直接交给脚本。 func TestScope_复用已有状态(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "s.ts", ` export default class S { read(k) { return store.Get(k) } } `) state := testext.New() state.Set("preset", "Go 侧预置的") ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(state)) // Store 本身就是状态,直接当扩展用 obj, err := s.New(ctx) if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(ctx, "read", "preset"); got != "Go 侧预置的" { t.Errorf("got = %#v", got) } } // 改扩展的全局名。 func TestScope_扩展改名(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "s.ts", ` export default class S { put() { $state.Set("k", "v"); return typeof globalThis.store } } `) st := testext.Named("$state") ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st)) obj, err := s.New(ctx) if err != nil { t.Fatal(err) } defer obj.Close() got, err := obj.Call(ctx, "put") if err != nil { t.Fatalf("改名后没生效: %v", err) } if got != "undefined" { t.Errorf("改名后默认名不该还在: %#v", got) } if st.Get("k") != "v" { t.Error("写入没落到同一份") } } // 没有作用域时,扩展的全局不存在。 func TestScope_没有作用域就没有扩展(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "s.ts", ` export default class S { probe() { return typeof globalThis.store } } `) obj, err := s.New(context.Background()) if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(context.Background(), "probe"); got != "undefined" { t.Errorf("没有作用域不该有 store: %#v", got) } } // ScopeGlobals 注入作用域专属的全局,脚本能读到会话标识。 func TestScope_全局与标识(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "s.ts", ` export default class S { who() { return currentUser.name + "@" + scope.key } } `) ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeKey("req-7"), jscriptx.ScopeGlobals(map[string]any{ "currentUser": map[string]any{"name": "张三"}, })) if key, ok := jscriptx.ScopeKeyOf(ctx); !ok || key != "req-7" { t.Errorf("ScopeKeyOf = %q, %v", key, ok) } obj, err := s.New(ctx) if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(ctx, "who"); got != "张三@req-7" { t.Errorf("got = %#v", got) } } // 不给 key 时自动生成一个,两次 WithScope 拿到的不一样。 func TestScope_自动生成key(t *testing.T) { c1 := jscriptx.WithScope(context.Background()) c2 := jscriptx.WithScope(context.Background()) k1, ok1 := jscriptx.ScopeKeyOf(c1) k2, ok2 := jscriptx.ScopeKeyOf(c2) if !ok1 || !ok2 || k1 == "" || k2 == "" { t.Fatalf("没生成 key: %q %q", k1, k2) } if k1 == k2 { t.Error("两次应该生成不同的 key") } if _, ok := jscriptx.ScopeKeyOf(context.Background()); ok { t.Error("没有作用域时不该返回 key") } } // ScopeExtensionOf 在手上没有原对象时把扩展取回来。 func TestScope_取回扩展(t *testing.T) { st := testext.New() ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st)) got := jscriptx.ScopeExtensionOf(ctx, "store") if got != jscriptx.Extension(st) { t.Errorf("取回来的不是同一个: %#v", got) } if jscriptx.ScopeExtensionOf(ctx, "nope") != nil { t.Error("不存在的扩展该返回 nil") } if jscriptx.ScopeExtensionOf(context.Background(), "store") != nil { t.Error("没有作用域时该返回 nil") } } // 作用域实例重建 VM 时,要回到它**创建时**那个作用域。 // // Instance 的 VM 超时后会被丢弃,下次调用重建(见 Resets)。重建走的是 i.scope, // 不是当次调用 ctx 里的——对一个作用域实例调 Call(context.Background()) 不该把 // 它的扩展弄丢。 // // 这条守的是 newVM 的 scope 参数为什么是显式传的:改成从 ctx 嗅探就会破坏它。 func TestScope_实例重建VM后仍在原作用域(t *testing.T) { e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond)) s := mustCompile(t, e, "s.ts", ` export default class S { spin() { while (true) {} } seeStore() { return typeof store } } `) st := testext.New() ctx := jscriptx.WithScope(context.Background(), jscriptx.ScopeExtensions(st)) obj, err := s.New(ctx) if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(ctx, "seeStore"); got != "object" { t.Fatalf("一开始就没看见扩展: %#v", got) } // 超时会让实例丢掉 VM,下次调用重建 if _, err := obj.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) { t.Fatalf("该超时: %v", err) } if obj.Resets() != 1 { t.Fatalf("VM 没被丢弃,这条测不到重建(Resets=%d)", obj.Resets()) } // 用**不带作用域**的 ctx 触发重建:实例仍属于原来那个作用域 got, err := obj.Call(context.Background(), "seeStore") if err != nil { t.Fatal(err) } if got != "object" { t.Errorf("重建之后扩展丢了(%#v)——实例被当成了无作用域的", got) } }