f373776d4c
* feat(ipc): add retained event subscriptions * feat(plugins): expose namespaced event publishing * feat(app): wire plugin events to IPC * docs(plugins): document local event pubsub * fix(ipc): clear request deadline for subscriptions * fix(luaplugin): install event publisher before plugins load * fix(luaplugin): clear retained events after publishers stop * fix(ipc): keep event sequence gap-free when retention is rejected * fix(luaplugin): break table cycles and cap depth in Lua value conversion * docs(plugins): document conversion limits and subscription stream rules * refactor(ipc): report a missing server with a sentinel error * fix(luaplugin): fold event namespace into one topic segment * docs(site): mention plugin event publishing and subscriptions * fix(luaplugin): keep event namespaces unique across plugins * fix(luaplugin): release event namespace by installed filename * refactor(ipc): render the not-running message in the command layer * docs(plugins): clarify namespace prefixes and JSON conversion limits
221 lines
4.9 KiB
Go
221 lines
4.9 KiB
Go
package luaplugin
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
lua "github.com/yuin/gopher-lua"
|
|
)
|
|
|
|
func TestJSONEncodeDecode(t *testing.T) {
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
cliamp := L.NewTable()
|
|
registerJSONAPI(L, cliamp)
|
|
L.SetGlobal("cliamp", cliamp)
|
|
|
|
err := L.DoString(`
|
|
local encoded = cliamp.json.encode({name = "test", count = 42})
|
|
local decoded = cliamp.json.decode(encoded)
|
|
_G.name = decoded.name
|
|
_G.count = decoded.count
|
|
`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if L.GetGlobal("name").String() != "test" {
|
|
t.Fatalf("name = %q", L.GetGlobal("name").String())
|
|
}
|
|
if float64(L.GetGlobal("count").(lua.LNumber)) != 42 {
|
|
t.Fatalf("count = %v", L.GetGlobal("count"))
|
|
}
|
|
}
|
|
|
|
func TestJSONDecodeInvalid(t *testing.T) {
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
cliamp := L.NewTable()
|
|
registerJSONAPI(L, cliamp)
|
|
L.SetGlobal("cliamp", cliamp)
|
|
|
|
err := L.DoString(`
|
|
local result, errmsg = cliamp.json.decode("not json")
|
|
_G.result = result
|
|
_G.errmsg = errmsg
|
|
`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if L.GetGlobal("result") != lua.LNil {
|
|
t.Fatalf("result = %v, want nil", L.GetGlobal("result"))
|
|
}
|
|
if L.GetGlobal("errmsg") == lua.LNil {
|
|
t.Fatal("errmsg should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestJSONEncodeArray(t *testing.T) {
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
cliamp := L.NewTable()
|
|
registerJSONAPI(L, cliamp)
|
|
L.SetGlobal("cliamp", cliamp)
|
|
|
|
err := L.DoString(`
|
|
_G.result = cliamp.json.encode({1, 2, 3})
|
|
`)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if got := L.GetGlobal("result").String(); got != "[1,2,3]" {
|
|
t.Fatalf("encode([1,2,3]) = %q, want %q", got, "[1,2,3]")
|
|
}
|
|
}
|
|
|
|
func TestLuaToGoRoundtrip(t *testing.T) {
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
|
|
tests := []struct {
|
|
name string
|
|
val lua.LValue
|
|
want any
|
|
}{
|
|
{"nil", lua.LNil, nil},
|
|
{"bool", lua.LTrue, true},
|
|
{"number", lua.LNumber(3.14), 3.14},
|
|
{"string", lua.LString("hi"), "hi"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := luaToGo(tt.val)
|
|
switch w := tt.want.(type) {
|
|
case nil:
|
|
if got != nil {
|
|
t.Fatalf("got %v, want nil", got)
|
|
}
|
|
case bool:
|
|
if got != w {
|
|
t.Fatalf("got %v, want %v", got, w)
|
|
}
|
|
case float64:
|
|
if got != w {
|
|
t.Fatalf("got %v, want %v", got, w)
|
|
}
|
|
case string:
|
|
if got != w {
|
|
t.Fatalf("got %v, want %v", got, w)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLuaToGoBreaksTableCycles(t *testing.T) {
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
|
|
self := L.NewTable()
|
|
self.RawSetString("name", lua.LString("self"))
|
|
self.RawSetString("self", self)
|
|
|
|
left := L.NewTable()
|
|
right := L.NewTable()
|
|
left.RawSetString("other", right)
|
|
right.RawSetString("other", left)
|
|
self.RawSetString("left", left)
|
|
|
|
// A cycle must not recurse until the Go stack overflows, which would kill
|
|
// cliamp instead of failing inside the plugin sandbox.
|
|
got, ok := luaToGo(self).(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("luaToGo returned %T, want map", luaToGo(self))
|
|
}
|
|
if got["name"] != "self" {
|
|
t.Fatalf("name = %v, want self", got["name"])
|
|
}
|
|
if got["self"] != nil {
|
|
t.Fatalf("cyclic self reference = %v, want nil", got["self"])
|
|
}
|
|
if _, err := json.Marshal(got); err != nil {
|
|
t.Fatalf("marshal cyclic table: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestLuaToGoKeepsRepeatedReferences(t *testing.T) {
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
|
|
shared := L.NewTable()
|
|
shared.RawSetString("id", lua.LString("shared"))
|
|
root := L.NewTable()
|
|
root.RawSetString("first", shared)
|
|
root.RawSetString("second", shared)
|
|
|
|
// The same table reached twice on different paths is not a cycle.
|
|
got, ok := luaToGo(root).(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("luaToGo returned %T, want map", luaToGo(root))
|
|
}
|
|
for _, key := range []string{"first", "second"} {
|
|
child, ok := got[key].(map[string]any)
|
|
if !ok || child["id"] != "shared" {
|
|
t.Fatalf("%s = %#v, want shared table", key, got[key])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLuaToGoLimitsNestingDepth(t *testing.T) {
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
|
|
root := L.NewTable()
|
|
deepest := root
|
|
for range maxLuaConvertDepth + 10 {
|
|
next := L.NewTable()
|
|
deepest.RawSetString("next", next)
|
|
deepest = next
|
|
}
|
|
|
|
current, ok := luaToGo(root).(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("luaToGo returned %T, want map", luaToGo(root))
|
|
}
|
|
depth := 1
|
|
for {
|
|
next, isTable := current["next"].(map[string]any)
|
|
if !isTable {
|
|
break
|
|
}
|
|
current = next
|
|
depth++
|
|
}
|
|
if depth != maxLuaConvertDepth {
|
|
t.Fatalf("converted depth = %d, want %d", depth, maxLuaConvertDepth)
|
|
}
|
|
}
|
|
|
|
func TestJSONEncodeSurvivesCyclicTable(t *testing.T) {
|
|
L := lua.NewState()
|
|
defer L.Close()
|
|
cliamp := L.NewTable()
|
|
registerJSONAPI(L, cliamp)
|
|
L.SetGlobal("cliamp", cliamp)
|
|
|
|
if err := L.DoString(`
|
|
local t = {name = "loop"}
|
|
t.self = t
|
|
_G.result = cliamp.json.encode(t)
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// The cyclic reference becomes null instead of crashing the process.
|
|
if got := L.GetGlobal("result").String(); got != `{"name":"loop","self":null}` {
|
|
t.Fatalf("encode(cyclic) = %q", got)
|
|
}
|
|
}
|