package jscriptx_test import ( "context" "errors" "fmt" "strings" "sync" "testing" "git.fsdpf.net/go/jscriptx" ) // testLoader 是测试用的 Loader 实现:本库只定义接口,实现由调用方提供。 type testLoader struct { mu sync.Mutex sources map[string]string version map[string]string calls int } func newTestLoader() *testLoader { return &testLoader{sources: map[string]string{}, version: map[string]string{}} } func (l *testLoader) put(name, src, version string) { l.mu.Lock() defer l.mu.Unlock() l.sources[name] = src l.version[name] = version } func (l *testLoader) Load(name string) (string, string, error) { l.mu.Lock() defer l.mu.Unlock() l.calls++ src, ok := l.sources[name] if !ok { return "", "", fmt.Errorf("%w: %s", jscriptx.ErrScriptNotFound, name) } return src, l.version[name], nil } func (l *testLoader) callCount() int { l.mu.Lock() defer l.mu.Unlock() return l.calls } func TestLoader_按名字加载并缓存(t *testing.T) { loader := newTestLoader() loader.put("greet", `export function hi() { return "v1" }`, "v1") e := newEngine(t, jscriptx.WithLoader(loader)) for i := 0; i < 3; i++ { s, err := e.Script("greet") if err != nil { t.Fatal(err) } got, err := s.Call(context.Background(), "hi") if err != nil { t.Fatal(err) } if got != "v1" { t.Fatalf("got %#v", got) } } if loader.callCount() != 1 { t.Errorf("没开自动重载时应该只加载一次,实际 %d 次", loader.callCount()) } } func TestLoader_脚本不存在(t *testing.T) { e := newEngine(t, jscriptx.WithLoader(newTestLoader())) _, err := e.Script("nope") if !errors.Is(err, jscriptx.ErrScriptNotFound) { t.Fatalf("want ErrScriptNotFound, got %v", err) } } func TestLoader_没配Loader也没注册(t *testing.T) { e := newEngine(t) if _, err := e.Script("nope"); !errors.Is(err, jscriptx.ErrScriptNotFound) { t.Fatalf("want ErrScriptNotFound, got %v", err) } } // 热更新:版本号变了才重新编译,没变就复用编译产物和 VM 池。 func TestLoader_自动热更新(t *testing.T) { loader := newTestLoader() loader.put("greet", `export function hi() { return "v1" }`, "v1") e := newEngine(t, jscriptx.WithLoader(loader), jscriptx.WithAutoReload(true)) s1, err := e.Script("greet") if err != nil { t.Fatal(err) } got, _ := s1.Call(context.Background(), "hi") if got != "v1" { t.Fatalf("got %#v", got) } // 版本号没变:应该拿到同一个 Script s2, err := e.Script("greet") if err != nil { t.Fatal(err) } if s1 != s2 { t.Error("版本号没变时不该重新编译") } // 版本号变了:重新编译,新调用走新逻辑 loader.put("greet", `export function hi() { return "v2" }`, "v2") s3, err := e.Script("greet") if err != nil { t.Fatal(err) } if s3 == s1 { t.Fatal("版本号变了应该重新编译") } if got, _ := s3.Call(context.Background(), "hi"); got != "v2" { t.Errorf("热更新后 got %#v, want v2", got) } if s3.Version() != "v2" { t.Errorf("Version() = %q", s3.Version()) } } // Loader 不提供版本号时,退化成按源码哈希判断有没有变。 func TestLoader_不提供版本号时按源码哈希(t *testing.T) { loader := newTestLoader() loader.put("x", `export function hi() { return 1 }`, "") e := newEngine(t, jscriptx.WithLoader(loader), jscriptx.WithAutoReload(true)) s1, err := e.Script("x") if err != nil { t.Fatal(err) } s2, err := e.Script("x") if err != nil { t.Fatal(err) } if s1 != s2 { t.Error("源码没变不该重新编译") } loader.put("x", `export function hi() { return 2 }`, "") s3, err := e.Script("x") if err != nil { t.Fatal(err) } if s3 == s1 { t.Error("源码变了应该重新编译") } } // LoaderFunc:让普通函数直接当 Loader 用。 func TestLoaderFunc(t *testing.T) { scripts := map[string]string{ "greet": `export function hi() { return "来自 LoaderFunc" }`, } loader := jscriptx.LoaderFunc(func(name string) (string, string, error) { src, ok := scripts[name] if !ok { return "", "", errors.New("没有这个脚本: " + jscriptx.ErrScriptNotFound.Error() + " " + name) } return src, "v1", nil }) e := newEngine(t, jscriptx.WithLoader(loader)) s, err := e.Script("greet") if err != nil { t.Fatalf("LoaderFunc 没生效: %v", err) } got, err := s.Call(context.Background(), "hi") if err != nil { t.Fatal(err) } if got != "来自 LoaderFunc" { t.Errorf("got = %#v", got) } if _, err := e.Script("nope"); err == nil || !strings.Contains(err.Error(), "nope") { t.Errorf("找不到脚本时该把 Loader 的错误带出来: %v", err) } } // preparedLoader 声称源码已经打过包了。 type preparedLoader struct{ src string } func (l preparedLoader) Load(string) (string, string, error) { return l.src, "v1", nil } func (l preparedLoader) Prepared() bool { return true } // plainLoader 不实现 Prepared,源码交给 Engine 打包。 type plainLoader struct{ src string } func (l plainLoader) Load(string) (string, string, error) { return l.src, "v1", nil } // Prepared 决定 Engine 打不打包这个 Loader 给的源码。 // // 用一段原始 ESM 源码就能把两条路分辨开:Engine 打包的话 export 会被抹平, // 不打包直接丢给 goja 的话,export 是保留字,编译当场失败。 func TestLoader_Prepared决定是否打包(t *testing.T) { const esmSource = `export default class H { hi() { return "ok" } }` // 不实现 Prepared:Engine 负责打包,ESM 源码能用 e1 := newEngine(t, jscriptx.WithLoader(plainLoader{esmSource})) s, err := e1.Script("x") if err != nil { t.Fatalf("Engine 应该打包这段源码: %v", err) } obj, err := s.New(context.Background()) if err != nil { t.Fatal(err) } defer obj.Close() if got, _ := obj.Call(context.Background(), "hi"); got != "ok" { t.Errorf("got = %#v", got) } // 实现了 Prepared:Engine 跳过打包,原始 ESM 直接交给 goja 就会语法错误 e2 := newEngine(t, jscriptx.WithLoader(preparedLoader{esmSource})) if _, err := e2.Script("x"); err == nil { t.Fatal("声称已打包却给原始 ESM,应该编译失败——说明 Prepared 没被识别,源码被重复打包了") } }