package jscriptx_test import ( "context" "errors" "fmt" "strings" "sync" "testing" "time" "git.fsdpf.net/go/jscriptx" "git.fsdpf.net/go/jscriptx/internal/testext" ) const objScript = ` export default class DeviceHandler { constructor(deviceId, model) { this.deviceId = deviceId this.model = model this.count = 0 } info() { return this.deviceId + "/" + this.model } onMessage(payload) { this.count++; return this.count } spin() { while (true) {} } } ` // constructor 抛异常要在 New 就报出来,不用等到第一次调用。 func TestInstance_构造失败当场报错(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "bad.js", ` export default class H { constructor(cfg) { if (!cfg) { throw new Error("缺少配置") } } run() { return 1 } } `) _, err := s.New(context.Background()) // 故意不传构造参数 if err == nil { t.Fatal("New 就该报错") } var jsErr *jscriptx.Error if !errors.As(err, &jsErr) { t.Fatalf("错误类型不对: %T", err) } if jsErr.Func != "constructor" || !strings.Contains(jsErr.Msg, "缺少配置") { t.Errorf("错误信息不对: %+v", jsErr) } t.Logf("%v", err) } func TestInstance_Close后拒绝调用(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "device.js", objScript) ctx := context.Background() obj, _ := s.New(ctx, "dev-1", "温控器") if _, err := obj.Call(ctx, "info"); err != nil { t.Fatal(err) } obj.Close() if _, err := obj.Call(ctx, "info"); !errors.Is(err, jscriptx.ErrClosed) { t.Errorf("want ErrClosed, got %v", err) } obj.Close() // 重复关闭不该 panic } func TestInstance_Has(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "device.js", objScript) obj, _ := s.New(context.Background(), "d", "m") defer obj.Close() if !obj.Has("onMessage") || !obj.Has("info") { t.Error("Has 认不出实例方法") } if obj.Has("nope") { t.Error("Has 不该认出不存在的方法") } } // 实例独占 VM,this.xxx 在它活着期间跨调用保持。 func TestInstance_状态跨调用保持(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "device.ts", deviceScript) ctx := context.Background() obj, err := s.New(ctx, "device-A", "温控器") if err != nil { t.Fatal(err) } defer obj.Close() for i, want := range []string{"device-A#1:m1", "device-A#2:m2", "device-A#3:m3"} { got, err := obj.Call(ctx, "onMessage", fmt.Sprintf("m%d", i+1)) if err != nil { t.Fatalf("第 %d 条失败: %v", i+1, err) } if got != want { t.Errorf("第 %d 条 = %#v, want %q", i+1, got, want) } } } // 不同实例互不相干,这是并发的基础。 func TestInstance_互相隔离(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "device.ts", deviceScript) ctx := context.Background() a, _ := s.New(ctx, "device-A", "型号A") defer a.Close() b, _ := s.New(ctx, "device-B", "型号B") defer b.Close() a.Call(ctx, "onMessage", "x") a.Call(ctx, "onMessage", "x") got, err := b.Call(ctx, "onMessage", "x") if err != nil { t.Fatal(err) } if got != "device-B#1:x" { t.Errorf("B 被 A 污染: %#v", got) } } // 同一个实例的调用串行,保护 this.xxx 不错乱。 func TestInstance_调用串行(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "counter.ts", ` export default class C { constructor() { this.n = 0 } bump() { this.n++; return this.n } total() { return this.n } } `) obj, err := s.New(context.Background()) if err != nil { t.Fatal(err) } defer obj.Close() const goroutines, per = 8, 100 var wg sync.WaitGroup for g := 0; g < goroutines; g++ { wg.Add(1) go func() { defer wg.Done() for i := 0; i < per; i++ { if _, err := obj.Call(context.Background(), "bump"); err != nil { t.Errorf("%v", err) return } } }() } wg.Wait() got, _ := obj.Call(context.Background(), "total") if got.(int64) != goroutines*per { t.Errorf("串行保证失效, total = %#v, want %d", got, goroutines*per) } } // 多个实例并发跑,各自的状态正确。 func TestInstance_多实例并发(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "device.ts", deviceScript) const devices, messages = 16, 30 var wg sync.WaitGroup for d := 0; d < devices; d++ { id := fmt.Sprintf("device-%d", d) wg.Add(1) go func(id string) { defer wg.Done() obj, err := s.New(context.Background(), id, "型号") if err != nil { t.Errorf("%v", err) return } defer obj.Close() for m := 0; m < messages; m++ { got, err := obj.Call(context.Background(), "onMessage", "m") if err != nil { t.Errorf("%s: %v", id, err) return } want := fmt.Sprintf("%s#%d:m", id, m+1) if got != want { t.Errorf("%s 第 %d 条 = %#v, want %q", id, m+1, got, want) return } } }(id) } wg.Wait() } // 脚本出错会丢 this 上的状态,但扩展里的东西还在。 func TestInstance_出错后状态归零(t *testing.T) { e := newEngine(t, jscriptx.WithTimeout(80*time.Millisecond)) s := mustCompile(t, e, "mix.ts", ` export default class M { constructor() { this.n = 0 } bump() { store.Incr("kept", 1); return ++this.n } spin() { while (true) {} } } `) 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() obj.Call(ctx, "bump") obj.Call(ctx, "bump") if _, err := obj.Call(ctx, "spin"); !errors.Is(err, jscriptx.ErrTimeout) { t.Fatalf("want ErrTimeout, got %v", err) } if obj.Resets() != 1 { t.Errorf("Resets = %d, want 1", obj.Resets()) } got, err := obj.Call(ctx, "bump") if err != nil { t.Fatal(err) } if got.(int64) != 1 { t.Errorf("this 上的状态应该归零, got %#v", got) } // 扩展在 Go 侧,不受 VM 重建影响 if n := toNumber(t, st.Get("kept")); n != 3 { t.Errorf("扩展里的状态不该丢, got %v want 3", n) } } // 脚本关闭后实例失效。 func TestInstance_脚本关闭后失效(t *testing.T) { e := newEngine(t) s := mustCompile(t, e, "device.ts", deviceScript) obj, _ := s.New(context.Background(), "d", "m") s.Close() if _, err := obj.Call(context.Background(), "onMessage", "x"); !errors.Is(err, jscriptx.ErrClosed) { t.Errorf("want ErrClosed, got %v", err) } }