chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,654 @@
|
||||
package luaplugin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
lua "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
// newTestManager returns a Manager ready for testing (no disk I/O).
|
||||
func newTestManager() *Manager {
|
||||
return &Manager{
|
||||
hooks: make(map[string][]*luaHook),
|
||||
keyBinds: make(map[string][]*luaHook),
|
||||
keyBindDescs: make(map[string]KeyBinding),
|
||||
commands: make(map[string]map[string]*luaHook),
|
||||
visMap: make(map[string]*luaVis),
|
||||
timers: newTimerManager(),
|
||||
execs: newExecManager(defaultAllowedBinaries),
|
||||
}
|
||||
}
|
||||
|
||||
// loadTestPlugin writes a Lua script to a temp file, loads it into the manager,
|
||||
// and appends it to m.plugins if registration succeeded.
|
||||
func loadTestPlugin(t *testing.T, m *Manager, name, code string) *Plugin {
|
||||
return loadTestPluginWithConfig(t, m, name, code, nil)
|
||||
}
|
||||
|
||||
func loadTestPluginWithConfig(t *testing.T, m *Manager, name, code string, cfg map[string]string) *Plugin {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, name+".lua")
|
||||
if err := os.WriteFile(path, []byte(code), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p, err := m.loadPlugin(path, name, cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("loadPlugin(%s): %v", name, err)
|
||||
}
|
||||
if p != nil {
|
||||
m.plugins = append(m.plugins, p)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func loadTestPluginExpectError(t *testing.T, m *Manager, name, code string) {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, name+".lua")
|
||||
if err := os.WriteFile(path, []byte(code), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := m.loadPlugin(path, name, nil); err == nil {
|
||||
t.Fatalf("expected error for %s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginRegistersHookPlugin(t *testing.T) {
|
||||
m := newTestManager()
|
||||
p := loadTestPlugin(t, m, "test-hook", `
|
||||
local p = plugin.register({
|
||||
name = "test-hook",
|
||||
type = "hook",
|
||||
version = "1.0",
|
||||
description = "a test plugin",
|
||||
})
|
||||
p:on("track.change", function(data) end)
|
||||
`)
|
||||
|
||||
if p == nil {
|
||||
t.Fatal("plugin is nil")
|
||||
}
|
||||
if p.Name != "test-hook" {
|
||||
t.Fatalf("Name = %q, want %q", p.Name, "test-hook")
|
||||
}
|
||||
if p.Type != "hook" {
|
||||
t.Fatalf("Type = %q, want %q", p.Type, "hook")
|
||||
}
|
||||
if p.Version != "1.0" {
|
||||
t.Fatalf("Version = %q, want %q", p.Version, "1.0")
|
||||
}
|
||||
if len(m.hooks["track.change"]) != 1 {
|
||||
t.Fatalf("hooks[track.change] = %d, want 1", len(m.hooks["track.change"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginWithoutRegisterReturnsNil(t *testing.T) {
|
||||
m := newTestManager()
|
||||
p := loadTestPlugin(t, m, "no-register", `-- does nothing`)
|
||||
|
||||
if p != nil {
|
||||
t.Fatalf("expected nil plugin for script without register, got %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginSyntaxError(t *testing.T) {
|
||||
m := newTestManager()
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "bad.lua")
|
||||
os.WriteFile(path, []byte(`this is not valid lua!!!`), 0o644)
|
||||
|
||||
_, err := m.loadPlugin(path, "bad", nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid Lua syntax")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginCleanupStopsPendingTimers(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
expectErr bool
|
||||
code string
|
||||
}{
|
||||
{
|
||||
name: "without register",
|
||||
code: `
|
||||
cliamp.timer.after(0.01, function()
|
||||
cliamp.fs.write(%q, "fired")
|
||||
end)
|
||||
cliamp.sleep(0.05)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "every",
|
||||
code: `
|
||||
cliamp.timer.every(0.01, function()
|
||||
cliamp.fs.write(%q, "fired")
|
||||
end)
|
||||
cliamp.sleep(0.05)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "on load error",
|
||||
expectErr: true,
|
||||
code: `
|
||||
local p = plugin.register({name = "bad", type = "hook"})
|
||||
cliamp.timer.after(0.01, function()
|
||||
cliamp.fs.write(%q, "fired")
|
||||
end)
|
||||
cliamp.sleep(0.05)
|
||||
error("boom")
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
m := newTestManager()
|
||||
path := filepath.Join(t.TempDir(), "fired")
|
||||
code := fmt.Sprintf(tc.code, path)
|
||||
|
||||
if tc.expectErr {
|
||||
loadTestPluginExpectError(t, m, "bad", code)
|
||||
} else {
|
||||
p := loadTestPlugin(t, m, "no-register-expired-timer", code)
|
||||
if p != nil {
|
||||
t.Fatalf("expected nil plugin for script without register, got %+v", p)
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
if _, err := os.Stat(path); !os.IsNotExist(err) {
|
||||
t.Fatalf("timer callback wrote %q after cleanup, err=%v", path, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadPluginErrorRemovesHooks(t *testing.T) {
|
||||
m := newTestManager()
|
||||
loadTestPluginExpectError(t, m, "bad", `
|
||||
local p = plugin.register({name = "bad", type = "hook"})
|
||||
p:on("track.change", function() end)
|
||||
error("boom")
|
||||
`)
|
||||
if len(m.hooks["track.change"]) != 0 {
|
||||
t.Fatalf("hooks[track.change] = %d, want 0", len(m.hooks["track.change"]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVisualizerErrorRemovesVisualizer(t *testing.T) {
|
||||
m := newTestManager()
|
||||
loadTestPluginExpectError(t, m, "bad", `
|
||||
plugin.register({name = "bad", type = "visualizer"})
|
||||
error("boom")
|
||||
`)
|
||||
if len(m.visPlugs) != 0 {
|
||||
t.Fatalf("visualizer count = %d, want 0", len(m.visPlugs))
|
||||
}
|
||||
if _, ok := m.visMap["bad"]; ok {
|
||||
t.Fatal("expected visualizer to be removed from visMap")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginConfig(t *testing.T) {
|
||||
m := newTestManager()
|
||||
p := loadTestPluginWithConfig(t, m, "cfg", `
|
||||
local p = plugin.register({name = "cfg", type = "hook"})
|
||||
_G.got_url = p:config("url")
|
||||
_G.got_missing = p:config("missing")
|
||||
`, map[string]string{"url": "https://example.com"})
|
||||
|
||||
gotURL := p.L.GetGlobal("got_url")
|
||||
if gotURL.String() != "https://example.com" {
|
||||
t.Fatalf("config('url') = %q, want %q", gotURL.String(), "https://example.com")
|
||||
}
|
||||
gotMissing := p.L.GetGlobal("got_missing")
|
||||
if gotMissing != lua.LNil {
|
||||
t.Fatalf("config('missing') = %v, want nil", gotMissing)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginPermissions(t *testing.T) {
|
||||
m := newTestManager()
|
||||
p := loadTestPlugin(t, m, "perm", `
|
||||
plugin.register({
|
||||
name = "perm",
|
||||
type = "hook",
|
||||
permissions = {"control"},
|
||||
})
|
||||
`)
|
||||
|
||||
if !p.perms["control"] {
|
||||
t.Fatal("perms[control] = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitSync(t *testing.T) {
|
||||
m := newTestManager()
|
||||
loadTestPlugin(t, m, "sync-test", `
|
||||
local p = plugin.register({name = "sync-test", type = "hook"})
|
||||
_G.events = {}
|
||||
p:on("test.event", function(data)
|
||||
table.insert(_G.events, data.msg)
|
||||
end)
|
||||
`)
|
||||
m.EmitSync("test.event", map[string]any{"msg": "hello"})
|
||||
|
||||
p := m.plugins[0]
|
||||
events := p.L.GetGlobal("events").(*lua.LTable)
|
||||
if events.Len() != 1 {
|
||||
t.Fatalf("events length = %d, want 1", events.Len())
|
||||
}
|
||||
if events.RawGetInt(1).String() != "hello" {
|
||||
t.Fatalf("events[1] = %q, want %q", events.RawGetInt(1).String(), "hello")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitAsync(t *testing.T) {
|
||||
m := newTestManager()
|
||||
loadTestPlugin(t, m, "async-test", `
|
||||
local p = plugin.register({name = "async-test", type = "hook"})
|
||||
_G.called = false
|
||||
p:on("test.event", function(data)
|
||||
_G.called = true
|
||||
end)
|
||||
`)
|
||||
m.Emit("test.event", nil)
|
||||
|
||||
// Wait for async handler to complete.
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
p := m.plugins[0]
|
||||
p.mu.Lock()
|
||||
called := p.L.GetGlobal("called")
|
||||
p.mu.Unlock()
|
||||
|
||||
if called != lua.LTrue {
|
||||
t.Fatal("async event handler was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmitMultipleHooks(t *testing.T) {
|
||||
m := newTestManager()
|
||||
loadTestPlugin(t, m, "multi", `
|
||||
local p = plugin.register({name = "multi", type = "hook"})
|
||||
_G.count = 0
|
||||
p:on("test.event", function() _G.count = _G.count + 1 end)
|
||||
p:on("test.event", function() _G.count = _G.count + 10 end)
|
||||
`)
|
||||
m.EmitSync("test.event", nil)
|
||||
|
||||
p := m.plugins[0]
|
||||
count := p.L.GetGlobal("count")
|
||||
if count.(lua.LNumber) != 11 {
|
||||
t.Fatalf("count = %v, want 11", count)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginCountAndHasHooks(t *testing.T) {
|
||||
m := newTestManager()
|
||||
|
||||
if m.PluginCount() != 0 {
|
||||
t.Fatalf("PluginCount() = %d, want 0", m.PluginCount())
|
||||
}
|
||||
if m.HasHooks() {
|
||||
t.Fatal("HasHooks() = true, want false")
|
||||
}
|
||||
|
||||
loadTestPlugin(t, m, "counter", `
|
||||
local p = plugin.register({name = "counter", type = "hook"})
|
||||
p:on("app.start", function() end)
|
||||
`)
|
||||
|
||||
if m.PluginCount() != 1 {
|
||||
t.Fatalf("PluginCount() = %d, want 1", m.PluginCount())
|
||||
}
|
||||
if !m.HasHooks() {
|
||||
t.Fatal("HasHooks() = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClose(t *testing.T) {
|
||||
m := newTestManager()
|
||||
loadTestPlugin(t, m, "close-test", `
|
||||
local p = plugin.register({name = "close-test", type = "hook"})
|
||||
_G.quit = false
|
||||
p:on("app.quit", function() _G.quit = true end)
|
||||
`)
|
||||
|
||||
m.Close()
|
||||
|
||||
// After Close, the LState is shut down. We can't safely query it,
|
||||
// but we verified it doesn't panic.
|
||||
}
|
||||
|
||||
func TestManagerWithStateProvider(t *testing.T) {
|
||||
m := newTestManager()
|
||||
m.SetStateProvider(StateProvider{
|
||||
PlayerState: func() string { return "playing" },
|
||||
Volume: func() float64 { return -3.5 },
|
||||
TrackTitle: func() string { return "Angel" },
|
||||
TrackArtist: func() string { return "Massive Attack" },
|
||||
})
|
||||
|
||||
p := loadTestPlugin(t, m, "state-test", `
|
||||
local p = plugin.register({name = "state-test", type = "hook"})
|
||||
_G.state = cliamp.player.state()
|
||||
_G.vol = cliamp.player.volume()
|
||||
_G.title = cliamp.track.title()
|
||||
_G.artist = cliamp.track.artist()
|
||||
`)
|
||||
|
||||
if p.L.GetGlobal("state").String() != "playing" {
|
||||
t.Fatalf("state = %q, want %q", p.L.GetGlobal("state").String(), "playing")
|
||||
}
|
||||
if float64(p.L.GetGlobal("vol").(lua.LNumber)) != -3.5 {
|
||||
t.Fatalf("vol = %v, want -3.5", p.L.GetGlobal("vol"))
|
||||
}
|
||||
if p.L.GetGlobal("title").String() != "Angel" {
|
||||
t.Fatalf("title = %q", p.L.GetGlobal("title").String())
|
||||
}
|
||||
if p.L.GetGlobal("artist").String() != "Massive Attack" {
|
||||
t.Fatalf("artist = %q", p.L.GetGlobal("artist").String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestManagerWithControlProvider(t *testing.T) {
|
||||
m := newTestManager()
|
||||
var gotVol float64
|
||||
m.SetControlProvider(ControlProvider{
|
||||
SetVolume: func(db float64) { gotVol = db },
|
||||
})
|
||||
|
||||
loadTestPlugin(t, m, "ctrl-test", `
|
||||
plugin.register({
|
||||
name = "ctrl-test",
|
||||
type = "hook",
|
||||
permissions = {"control"},
|
||||
})
|
||||
cliamp.player.set_volume(-10)
|
||||
`)
|
||||
|
||||
if gotVol != -10 {
|
||||
t.Fatalf("SetVolume called with %v, want -10", gotVol)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlClampsBounds(t *testing.T) {
|
||||
m := newTestManager()
|
||||
var gotVol float64
|
||||
var gotSpeed float64
|
||||
m.SetControlProvider(ControlProvider{
|
||||
SetVolume: func(db float64) { gotVol = db },
|
||||
SetSpeed: func(r float64) { gotSpeed = r },
|
||||
})
|
||||
|
||||
loadTestPlugin(t, m, "clamp-test", `
|
||||
plugin.register({
|
||||
name = "clamp-test",
|
||||
type = "hook",
|
||||
permissions = {"control"},
|
||||
})
|
||||
cliamp.player.set_volume(100)
|
||||
cliamp.player.set_speed(10)
|
||||
`)
|
||||
|
||||
if gotVol != 6 {
|
||||
t.Fatalf("set_volume(100) clamped to %v, want 6", gotVol)
|
||||
}
|
||||
if gotSpeed != 2.0 {
|
||||
t.Fatalf("set_speed(10) clamped to %v, want 2.0", gotSpeed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestControlWithoutPermissionIsNoop(t *testing.T) {
|
||||
m := newTestManager()
|
||||
m.logger = newPluginLogger(filepath.Join(t.TempDir(), "test.log"))
|
||||
t.Cleanup(m.Close)
|
||||
called := false
|
||||
m.SetControlProvider(ControlProvider{
|
||||
SetVolume: func(db float64) { called = true },
|
||||
})
|
||||
|
||||
loadTestPlugin(t, m, "no-perm", `
|
||||
plugin.register({name = "no-perm", type = "hook"})
|
||||
cliamp.player.set_volume(-10)
|
||||
`)
|
||||
|
||||
if called {
|
||||
t.Fatal("SetVolume was called without control permission")
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateProviderDefaultsWhenNil(t *testing.T) {
|
||||
m := newTestManager()
|
||||
// No state provider set — all callbacks are nil.
|
||||
|
||||
p := loadTestPlugin(t, m, "defaults", `
|
||||
local p = plugin.register({name = "defaults", type = "hook"})
|
||||
_G.state = cliamp.player.state()
|
||||
_G.vol = cliamp.player.volume()
|
||||
_G.speed = cliamp.player.speed()
|
||||
_G.pos = cliamp.player.position()
|
||||
_G.title = cliamp.track.title()
|
||||
`)
|
||||
|
||||
if p.L.GetGlobal("state").String() != "stopped" {
|
||||
t.Fatalf("default state = %q, want 'stopped'", p.L.GetGlobal("state").String())
|
||||
}
|
||||
if float64(p.L.GetGlobal("vol").(lua.LNumber)) != 0 {
|
||||
t.Fatalf("default vol = %v, want 0", p.L.GetGlobal("vol"))
|
||||
}
|
||||
if float64(p.L.GetGlobal("speed").(lua.LNumber)) != 1 {
|
||||
t.Fatalf("default speed = %v, want 1", p.L.GetGlobal("speed"))
|
||||
}
|
||||
if p.L.GetGlobal("title").String() != "" {
|
||||
t.Fatalf("default title = %q, want empty", p.L.GetGlobal("title").String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimerAfter(t *testing.T) {
|
||||
m := newTestManager()
|
||||
p := loadTestPlugin(t, m, "timer-test", `
|
||||
local p = plugin.register({name = "timer-test", type = "hook"})
|
||||
_G.fired = false
|
||||
cliamp.timer.after(0.05, function()
|
||||
_G.fired = true
|
||||
end)
|
||||
`)
|
||||
|
||||
time.Sleep(150 * time.Millisecond)
|
||||
|
||||
p.mu.Lock()
|
||||
fired := p.L.GetGlobal("fired")
|
||||
p.mu.Unlock()
|
||||
|
||||
if fired != lua.LTrue {
|
||||
t.Fatal("timer.after callback was not fired")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimerCancel(t *testing.T) {
|
||||
m := newTestManager()
|
||||
p := loadTestPlugin(t, m, "cancel-test", `
|
||||
local p = plugin.register({name = "cancel-test", type = "hook"})
|
||||
_G.fired = false
|
||||
local id = cliamp.timer.after(0.2, function()
|
||||
_G.fired = true
|
||||
end)
|
||||
cliamp.timer.cancel(id)
|
||||
`)
|
||||
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
p.mu.Lock()
|
||||
fired := p.L.GetGlobal("fired")
|
||||
p.mu.Unlock()
|
||||
|
||||
if fired == lua.LTrue {
|
||||
t.Fatal("timer.after callback fired after cancel")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisualizerPlugin(t *testing.T) {
|
||||
m := newTestManager()
|
||||
loadTestPlugin(t, m, "test-vis", `
|
||||
local v = plugin.register({name = "test-vis", type = "visualizer"})
|
||||
_G.init_called = false
|
||||
_G.destroy_called = false
|
||||
v.init = function(self, rows, cols)
|
||||
_G.init_called = true
|
||||
_G.init_rows = rows
|
||||
end
|
||||
v.render = function(self, bands, frame, rows, cols)
|
||||
return "frame-" .. tostring(frame)
|
||||
end
|
||||
v.destroy = function(self)
|
||||
_G.destroy_called = true
|
||||
end
|
||||
`)
|
||||
m.finalizeVisualizers()
|
||||
|
||||
names := m.Visualizers()
|
||||
if len(names) != 1 || names[0] != "test-vis" {
|
||||
t.Fatalf("Visualizers() = %v, want [test-vis]", names)
|
||||
}
|
||||
|
||||
m.InitVis("test-vis", 8, 40)
|
||||
|
||||
vis := m.visMap["test-vis"]
|
||||
vis.plugin.mu.Lock()
|
||||
initCalled := vis.plugin.L.GetGlobal("init_called")
|
||||
vis.plugin.mu.Unlock()
|
||||
|
||||
if initCalled != lua.LTrue {
|
||||
t.Fatal("init callback was not called")
|
||||
}
|
||||
|
||||
got := m.RenderVis("test-vis", [10]float64{}, 8, 40, 42)
|
||||
if got != "frame-42" {
|
||||
t.Fatalf("RenderVis() = %q, want %q", got, "frame-42")
|
||||
}
|
||||
|
||||
m.DestroyVis("test-vis")
|
||||
|
||||
vis.plugin.mu.Lock()
|
||||
destroyCalled := vis.plugin.L.GetGlobal("destroy_called")
|
||||
vis.plugin.mu.Unlock()
|
||||
|
||||
if destroyCalled != lua.LTrue {
|
||||
t.Fatal("destroy callback was not called")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderVisReusesLastOnError(t *testing.T) {
|
||||
m := newTestManager()
|
||||
loadTestPlugin(t, m, "err-vis", `
|
||||
local v = plugin.register({name = "err-vis", type = "visualizer"})
|
||||
local calls = 0
|
||||
v.render = function(self, bands, frame, rows, cols)
|
||||
calls = calls + 1
|
||||
if calls == 2 then
|
||||
error("boom")
|
||||
end
|
||||
return "ok-" .. tostring(calls)
|
||||
end
|
||||
`)
|
||||
m.finalizeVisualizers()
|
||||
|
||||
got1 := m.RenderVis("err-vis", [10]float64{}, 8, 40, 1)
|
||||
if got1 != "ok-1" {
|
||||
t.Fatalf("frame 1 = %q, want %q", got1, "ok-1")
|
||||
}
|
||||
|
||||
// Frame 2 errors — should reuse last frame.
|
||||
got2 := m.RenderVis("err-vis", [10]float64{}, 8, 40, 2)
|
||||
if got2 != "ok-1" {
|
||||
t.Fatalf("frame 2 (error) = %q, want %q (reused)", got2, "ok-1")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataToTableConversion(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
tbl := dataToTable(L, map[string]any{
|
||||
"str": "hello",
|
||||
"num": 42,
|
||||
"float": 3.14,
|
||||
"bool": true,
|
||||
"nil": nil,
|
||||
})
|
||||
|
||||
if tbl.RawGetString("str").String() != "hello" {
|
||||
t.Fatalf("str = %v", tbl.RawGetString("str"))
|
||||
}
|
||||
if float64(tbl.RawGetString("num").(lua.LNumber)) != 42 {
|
||||
t.Fatalf("num = %v", tbl.RawGetString("num"))
|
||||
}
|
||||
if float64(tbl.RawGetString("float").(lua.LNumber)) != 3.14 {
|
||||
t.Fatalf("float = %v", tbl.RawGetString("float"))
|
||||
}
|
||||
if tbl.RawGetString("bool") != lua.LTrue {
|
||||
t.Fatalf("bool = %v", tbl.RawGetString("bool"))
|
||||
}
|
||||
if tbl.RawGetString("nil") != lua.LNil {
|
||||
t.Fatalf("nil = %v", tbl.RawGetString("nil"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDataToTableNested(t *testing.T) {
|
||||
L := lua.NewState()
|
||||
defer L.Close()
|
||||
|
||||
tbl := dataToTable(L, map[string]any{
|
||||
"nested": map[string]any{"key": "value"},
|
||||
"floats": []float64{1.0, 2.0, 3.0},
|
||||
})
|
||||
|
||||
nested := tbl.RawGetString("nested").(*lua.LTable)
|
||||
if nested.RawGetString("key").String() != "value" {
|
||||
t.Fatalf("nested.key = %v", nested.RawGetString("key"))
|
||||
}
|
||||
|
||||
floats := tbl.RawGetString("floats").(*lua.LTable)
|
||||
if float64(floats.RawGetInt(1).(lua.LNumber)) != 1.0 {
|
||||
t.Fatalf("floats[1] = %v", floats.RawGetInt(1))
|
||||
}
|
||||
if floats.Len() != 3 {
|
||||
t.Fatalf("floats length = %d, want 3", floats.Len())
|
||||
}
|
||||
}
|
||||
|
||||
func TestConcurrentEmitSafety(t *testing.T) {
|
||||
m := newTestManager()
|
||||
p := loadTestPlugin(t, m, "concurrent", `
|
||||
local p = plugin.register({name = "concurrent", type = "hook"})
|
||||
_G.count = 0
|
||||
p:on("inc", function() _G.count = _G.count + 1 end)
|
||||
`)
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for range 20 {
|
||||
wg.Go(func() {
|
||||
m.Emit("inc", nil)
|
||||
})
|
||||
}
|
||||
wg.Wait()
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
p.mu.Lock()
|
||||
count := float64(p.L.GetGlobal("count").(lua.LNumber))
|
||||
p.mu.Unlock()
|
||||
|
||||
if count != 20 {
|
||||
t.Fatalf("count after 20 concurrent emits = %v, want 20", count)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user