655 lines
16 KiB
Go
655 lines
16 KiB
Go
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)
|
|
}
|
|
}
|