Files
goa/goa_test.go
Hanzo Dev 54a40e19c1 goa: pure-Go polyglot embedding (Go, Python, TS/JS, Rust/WASM)
One Module/Engine interface with a JSON-in/JSON-out calling convention runs
four pure-Go engines behind a Pool that makes single-threaded interpreters
serve requests as parallel goroutines:

  native               plain Go handlers, zero overhead
  goja + esbuild       JavaScript and TypeScript (TS transpiled to ES2020)
  gpython              Python, no cgo and no GIL -> true goroutine parallelism
  wazero               Rust/WASM via a trivial string ABI

Services mount from a manifest (lang + source + routes), so any Go, Python,
Rust or JS backend collapses into a single static CGO_ENABLED=0 binary served
on the zip/gofiber app. A Go-backed json stdlib shim lets Python backends keep
importing the real module names while running at Go speed.

Examples and tests cover every engine; the Rust guest is a real wasm32 fixture
exercised end to end.
2026-06-28 19:14:19 -07:00

211 lines
6.0 KiB
Go

package goa
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
)
// echoWasm is the goa Rust guest (examples/echo-rust) compiled to wasm32. Build:
//
// cd examples/echo-rust && cargo build --release --target wasm32-unknown-unknown
// cp target/wasm32-unknown-unknown/release/echo_rust.wasm echo.wasm
//
//go:embed examples/echo-rust/echo.wasm
var echoWasm []byte
func mustInvoke(t *testing.T, m Module, fn, payload string) string {
t.Helper()
out, err := m.Invoke(context.Background(), fn, []byte(payload))
if err != nil {
t.Fatalf("Invoke(%s): %v", fn, err)
}
return string(out)
}
func TestNative(t *testing.T) {
m := NewNativeModule(map[string]HandlerFunc{
"echo": func(_ context.Context, p []byte) ([]byte, error) { return p, nil },
})
if got := mustInvoke(t, m, "echo", `{"a":1}`); got != `{"a":1}` {
t.Fatalf("native echo = %q", got)
}
}
func TestGoja(t *testing.T) {
src := `function greet(p){ var d = JSON.parse(p); return JSON.stringify({hello:d.name, lang:"javascript"}); }`
m, err := Load(context.Background(), "js", []byte(src), LoadOptions{Name: "g"})
if err != nil {
t.Fatal(err)
}
got := mustInvoke(t, m, "greet", `{"name":"world"}`)
assertJSONEq(t, got, `{"hello":"world","lang":"javascript"}`)
}
func TestGojaTypeScript(t *testing.T) {
// TS-only syntax (type annotations) must transpile via esbuild.
src := `export function add(p: string): string { const d: {x:number,y:number} = JSON.parse(p); return JSON.stringify({sum: d.x + d.y}); }`
m, err := Load(context.Background(), "ts", []byte(src), LoadOptions{Name: "t"})
if err != nil {
t.Fatal(err)
}
assertJSONEq(t, mustInvoke(t, m, "add", `{"x":2,"y":3}`), `{"sum":5}`)
}
func TestGpython(t *testing.T) {
src := "import json\n" +
"def greet(payload):\n" +
" d = json.loads(payload)\n" +
" return json.dumps({'hello': d['name'], 'lang': 'python'})\n"
m, err := Load(context.Background(), "python", []byte(src), LoadOptions{Name: "g"})
if err != nil {
t.Fatal(err)
}
got := mustInvoke(t, m, "greet", `{"name":"world"}`)
assertJSONEq(t, got, `{"hello":"world","lang":"python"}`)
}
func TestGpythonJSONRoundTrip(t *testing.T) {
src := "import json\n" +
"def rt(payload):\n" +
" return json.dumps(json.loads(payload))\n"
m, err := Load(context.Background(), "py", []byte(src), LoadOptions{Name: "rt"})
if err != nil {
t.Fatal(err)
}
in := `{"n":42,"f":1.5,"b":true,"s":"x","a":[1,2,3],"o":{"k":"v"},"z":null}`
assertJSONEq(t, mustInvoke(t, m, "rt", in), in)
}
func TestPoolParallel(t *testing.T) {
src := "def busy(payload):\n return payload\n"
pool, err := LoadPool(context.Background(), "python", []byte(src), 4, LoadOptions{Name: "p"})
if err != nil {
t.Fatal(err)
}
defer pool.Close()
var wg sync.WaitGroup
for i := 0; i < 64; i++ {
wg.Add(1)
go func() {
defer wg.Done()
out, err := pool.Invoke(context.Background(), "busy", []byte(`"ok"`))
if err != nil || string(out) != `"ok"` {
t.Errorf("pool invoke = %q err=%v", out, err)
}
}()
}
wg.Wait()
}
func TestServiceHTTP(t *testing.T) {
src := "import json\n" +
"def greet(payload):\n" +
" return json.dumps({'hi': json.loads(payload)['name']})\n"
pool, err := LoadPool(context.Background(), "python", []byte(src), 2, LoadOptions{Name: "greeter"})
if err != nil {
t.Fatal(err)
}
defer pool.Close()
svc := &Service{Name: "greeter", Prefix: "/v1/greeter", Pool: pool,
Routes: []Route{{Method: "POST", Path: "/greet", Func: "greet"}}}
srv := httptest.NewServer(svc.Handler())
defer srv.Close()
resp, err := http.Post(srv.URL+"/v1/greeter/greet", "application/json", strings.NewReader(`{"name":"ada"}`))
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
t.Fatalf("status %d", resp.StatusCode)
}
var got map[string]string
_ = json.NewDecoder(resp.Body).Decode(&got)
if got["hi"] != "ada" {
t.Fatalf("got %v", got)
}
}
func TestWazero(t *testing.T) {
m, err := Load(context.Background(), "rust", echoWasm, LoadOptions{Name: "echo"})
if err != nil {
t.Fatal(err)
}
defer m.Close()
assertJSONEq(t, mustInvoke(t, m, "echo", `{"name":"ada"}`), `{"echo":{"name":"ada"}}`)
// Hammer it well past the 64 KiB arena: if dealloc didn't reset, the bump
// allocator would overflow and the host's writes would trap.
for i := 0; i < 5000; i++ {
if _, err := m.Invoke(context.Background(), "echo", []byte(`{"i":1}`)); err != nil {
t.Fatalf("repeat %d: %v", i, err)
}
}
}
func TestLoadDir(t *testing.T) {
services, err := LoadDir(context.Background(), "examples")
if err != nil {
t.Fatal(err)
}
byName := map[string]*Service{}
for _, s := range services {
byName[s.Name] = s
defer s.Pool.Close()
}
cases := []struct{ svc, fn, in, want string }{
{"hello-py", "greet", `{"name":"x"}`, `{"hello":"x","lang":"python"}`},
{"hello-ts", "greet", `{"name":"x"}`, `{"hello":"x","lang":"typescript"}`},
{"echo-rust", "echo", `{"name":"x"}`, `{"echo":{"name":"x"}}`},
}
for _, c := range cases {
s := byName[c.svc]
if s == nil {
t.Errorf("service %q not loaded from manifests", c.svc)
continue
}
out, err := s.Pool.Invoke(context.Background(), c.fn, []byte(c.in))
if err != nil {
t.Errorf("%s.%s: %v", c.svc, c.fn, err)
continue
}
assertJSONEq(t, string(out), c.want)
}
}
func TestLangsRegistered(t *testing.T) {
want := map[string]bool{"javascript": false, "python": false, "wasm": false}
for _, l := range Langs() {
if _, ok := want[l]; ok {
want[l] = true
}
}
for l, found := range want {
if !found {
t.Errorf("engine %q not registered", l)
}
}
}
func assertJSONEq(t *testing.T, got, want string) {
t.Helper()
var g, w interface{}
if err := json.Unmarshal([]byte(got), &g); err != nil {
t.Fatalf("got not JSON: %q (%v)", got, err)
}
if err := json.Unmarshal([]byte(want), &w); err != nil {
t.Fatalf("want not JSON: %q", want)
}
gb, _ := json.Marshal(g)
wb, _ := json.Marshal(w)
if string(gb) != string(wb) {
t.Fatalf("JSON mismatch:\n got=%s\nwant=%s", gb, wb)
}
}