summaryrefslogtreecommitdiff
path: root/internal/plugin/plugin_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/plugin/plugin_test.go')
-rw-r--r--internal/plugin/plugin_test.go391
1 files changed, 391 insertions, 0 deletions
diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go
new file mode 100644
index 0000000..d020c31
--- /dev/null
+++ b/internal/plugin/plugin_test.go
@@ -0,0 +1,391 @@
+package plugin
+
+// White-box tests. Package plugin (not plugin_test) so unexported types and
+// functions — load, loadDir, Plugin.has, Plugin.callWithBuilder,
+// Plugin.callNoReturn — are directly accessible.
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ lua "github.com/yuin/gopher-lua"
+
+ "github.com/shiv/internal/store"
+)
+
+// ── helpers ───────────────────────────────────────────────────────────────────
+
+// writeLua writes a Lua script to a temp file and returns its path.
+// The file is removed when the test ends.
+func writeLua(t *testing.T, src string) string {
+ t.Helper()
+ f, err := os.CreateTemp("", "shiv-plugin-*.lua")
+ require.NoError(t, err)
+ _, err = f.WriteString(src)
+ require.NoError(t, err)
+ require.NoError(t, f.Close())
+ t.Cleanup(func() { os.Remove(f.Name()) })
+ return f.Name()
+}
+
+// newTestStore opens a temp SQLite store for plugin tests.
+func newTestStore(t *testing.T) *store.Store {
+ t.Helper()
+ f, err := os.CreateTemp("", "shiv-plugin-store-*.shiv")
+ require.NoError(t, err)
+ f.Close()
+ t.Cleanup(func() { os.Remove(f.Name()) })
+ st, err := store.Open(f.Name())
+ require.NoError(t, err)
+ t.Cleanup(func() { st.Close() })
+ return st
+}
+
+// loadPlugin is a convenience wrapper around load that also registers cleanup.
+func loadPlugin(t *testing.T, src string, st *store.Store) *Plugin {
+ t.Helper()
+ path := writeLua(t, src)
+ p, err := load(path, st)
+ require.NoError(t, err)
+ t.Cleanup(func() { p.close() })
+ return p
+}
+
+// ── load ──────────────────────────────────────────────────────────────────────
+
+func TestLoad_ValidScript_ReturnsPlugin(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `-- empty script`, st)
+ assert.NotNil(t, p)
+}
+
+func TestLoad_InvalidScript_ReturnsError(t *testing.T) {
+ st := newTestStore(t)
+ path := writeLua(t, `this is not valid lua %%%`)
+ _, err := load(path, st)
+ assert.Error(t, err)
+}
+
+func TestLoad_NonExistentFile_ReturnsError(t *testing.T) {
+ st := newTestStore(t)
+ _, err := load("/does/not/exist.lua", st)
+ assert.Error(t, err)
+}
+
+func TestLoad_CallsOnLoad(t *testing.T) {
+ st := newTestStore(t)
+ // on_load sets a global that we can check afterwards.
+ p := loadPlugin(t, `
+ loaded = false
+ function on_load()
+ loaded = true
+ end
+ `, st)
+
+ p.mu.Lock()
+ v := p.L.GetGlobal("loaded")
+ p.mu.Unlock()
+
+ assert.Equal(t, lua.LTrue, v)
+}
+
+func TestLoad_OnLoadError_PluginStillLoaded(t *testing.T) {
+ st := newTestStore(t)
+ // on_load errors are logged but must not prevent the plugin from loading.
+ p, err := func() (*Plugin, error) {
+ path := writeLua(t, `
+ function on_load()
+ error("intentional on_load failure")
+ end
+ `)
+ return load(path, st)
+ }()
+ // load itself should succeed despite on_load failing.
+ require.NoError(t, err)
+ assert.NotNil(t, p)
+ p.close()
+}
+
+func TestLoad_SetsName(t *testing.T) {
+ st := newTestStore(t)
+ path := writeLua(t, `-- empty`)
+ p, err := load(path, st)
+ require.NoError(t, err)
+ defer p.close()
+ assert.Equal(t, filepath.Base(path), p.name)
+}
+
+// ── has ───────────────────────────────────────────────────────────────────────
+
+func TestHas_DefinedFunction_ReturnsTrue(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `function on_request(r) return r end`, st)
+ assert.True(t, p.has("on_request"))
+}
+
+func TestHas_UndefinedFunction_ReturnsFalse(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `-- empty`, st)
+ assert.False(t, p.has("on_request"))
+}
+
+func TestHas_NilGlobal_ReturnsFalse(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `on_request = nil`, st)
+ assert.False(t, p.has("on_request"))
+}
+
+// ── callNoReturn ──────────────────────────────────────────────────────────────
+
+func TestCallNoReturn_UndefinedFunction_ReturnsNil(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `-- empty`, st)
+ err := p.callNoReturn("no_such_fn", lua.LNil)
+ assert.NoError(t, err)
+}
+
+func TestCallNoReturn_FunctionExecutes(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `
+ called = false
+ function my_fn()
+ called = true
+ end
+ `, st)
+
+ err := p.callNoReturn("my_fn", lua.LNil)
+ require.NoError(t, err)
+
+ p.mu.Lock()
+ v := p.L.GetGlobal("called")
+ p.mu.Unlock()
+ assert.Equal(t, lua.LTrue, v)
+}
+
+func TestCallNoReturn_FunctionReceivesArg(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `
+ received = nil
+ function my_fn(arg)
+ received = arg
+ end
+ `, st)
+
+ p.mu.Lock()
+ arg := p.L.NewTable()
+ p.L.SetField(arg, "key", lua.LString("value"))
+ p.mu.Unlock()
+
+ err := p.callNoReturn("my_fn", arg)
+ require.NoError(t, err)
+
+ p.mu.Lock()
+ tbl, ok := p.L.GetGlobal("received").(*lua.LTable)
+ p.mu.Unlock()
+ require.True(t, ok)
+
+ p.mu.Lock()
+ v := tbl.RawGetString("key")
+ p.mu.Unlock()
+ assert.Equal(t, lua.LString("value"), v)
+}
+
+func TestCallNoReturn_LuaError_ReturnsError(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `
+ function bad_fn()
+ error("intentional error")
+ end
+ `, st)
+
+ err := p.callNoReturn("bad_fn", lua.LNil)
+ assert.Error(t, err)
+}
+
+func TestCallNoReturn_Timeout_ReturnsError(t *testing.T) {
+ st := newTestStore(t)
+ // Infinite loop — must be killed by the context timeout.
+ p := loadPlugin(t, `
+ function infinite()
+ while true do end
+ end
+ `, st)
+
+ start := time.Now()
+ err := p.callNoReturn("infinite", lua.LNil)
+ elapsed := time.Since(start)
+
+ assert.Error(t, err)
+ // Should terminate within a reasonable margin of callTimeout (500ms).
+ assert.Less(t, elapsed, callTimeout+300*time.Millisecond)
+}
+
+// ── callWithBuilder ───────────────────────────────────────────────────────────
+
+func TestCallWithBuilder_UndefinedFunction_ReturnsNilNoError(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `-- empty`, st)
+
+ ret, err := p.callWithBuilder("no_such_fn", func(L *lua.LState) *lua.LTable {
+ return L.NewTable()
+ })
+
+ assert.NoError(t, err)
+ assert.Nil(t, ret)
+}
+
+func TestCallWithBuilder_BuilderRunsUnderLock(t *testing.T) {
+ // Verify the builder is called (indirectly — if it panics the test fails).
+ st := newTestStore(t)
+ p := loadPlugin(t, `function fn(t) return t end`, st)
+
+ builderCalled := false
+ ret, err := p.callWithBuilder("fn", func(L *lua.LState) *lua.LTable {
+ builderCalled = true
+ tbl := L.NewTable()
+ L.SetField(tbl, "x", lua.LString("hello"))
+ return tbl
+ })
+
+ require.NoError(t, err)
+ assert.True(t, builderCalled)
+ assert.NotNil(t, ret)
+}
+
+func TestCallWithBuilder_FunctionCanModifyTable(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `
+ function transform(t)
+ t.result = "modified"
+ return t
+ end
+ `, st)
+
+ ret, err := p.callWithBuilder("transform", func(L *lua.LState) *lua.LTable {
+ tbl := L.NewTable()
+ L.SetField(tbl, "result", lua.LString("original"))
+ return tbl
+ })
+
+ require.NoError(t, err)
+ require.NotNil(t, ret)
+
+ p.mu.Lock()
+ v := ret.RawGetString("result")
+ p.mu.Unlock()
+ assert.Equal(t, lua.LString("modified"), v)
+}
+
+func TestCallWithBuilder_FunctionReturnsNil_ReturnsNilTable(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `function fn(t) return nil end`, st)
+
+ ret, err := p.callWithBuilder("fn", func(L *lua.LState) *lua.LTable {
+ return L.NewTable()
+ })
+
+ assert.NoError(t, err)
+ assert.Nil(t, ret)
+}
+
+func TestCallWithBuilder_LuaError_ReturnsError(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `
+ function bad(t)
+ error("builder error")
+ end
+ `, st)
+
+ _, err := p.callWithBuilder("bad", func(L *lua.LState) *lua.LTable {
+ return L.NewTable()
+ })
+
+ assert.Error(t, err)
+}
+
+func TestCallWithBuilder_Timeout_ReturnsError(t *testing.T) {
+ st := newTestStore(t)
+ p := loadPlugin(t, `
+ function slow(t)
+ while true do end
+ end
+ `, st)
+
+ start := time.Now()
+ _, err := p.callWithBuilder("slow", func(L *lua.LState) *lua.LTable {
+ return L.NewTable()
+ })
+ elapsed := time.Since(start)
+
+ assert.Error(t, err)
+ assert.Less(t, elapsed, callTimeout+300*time.Millisecond)
+}
+
+// ── loadDir ───────────────────────────────────────────────────────────────────
+
+func TestLoadDir_NonExistentDir_ReturnsNilSlice(t *testing.T) {
+ st := newTestStore(t)
+ plugins, err := loadDir("/does/not/exist", st)
+ assert.NoError(t, err)
+ assert.Nil(t, plugins)
+}
+
+func TestLoadDir_EmptyDir_ReturnsEmpty(t *testing.T) {
+ st := newTestStore(t)
+ dir := t.TempDir()
+ plugins, err := loadDir(dir, st)
+ assert.NoError(t, err)
+ assert.Empty(t, plugins)
+}
+
+func TestLoadDir_LoadsLuaFiles(t *testing.T) {
+ st := newTestStore(t)
+ dir := t.TempDir()
+
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "a.lua"), []byte(`x = 1`), 0644))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "b.lua"), []byte(`y = 2`), 0644))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "readme.txt"), []byte(`not lua`), 0644))
+
+ plugins, err := loadDir(dir, st)
+ require.NoError(t, err)
+ assert.Len(t, plugins, 2)
+ for _, p := range plugins {
+ p.close()
+ }
+}
+
+func TestLoadDir_SkipsDirectories(t *testing.T) {
+ st := newTestStore(t)
+ dir := t.TempDir()
+
+ require.NoError(t, os.Mkdir(filepath.Join(dir, "subdir.lua"), 0755))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "real.lua"), []byte(`z = 3`), 0644))
+
+ plugins, err := loadDir(dir, st)
+ require.NoError(t, err)
+ assert.Len(t, plugins, 1)
+ for _, p := range plugins {
+ p.close()
+ }
+}
+
+func TestLoadDir_InvalidLuaSkipped_OthersLoaded(t *testing.T) {
+ st := newTestStore(t)
+ dir := t.TempDir()
+
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "good.lua"), []byte(`g = 1`), 0644))
+ require.NoError(t, os.WriteFile(filepath.Join(dir, "bad.lua"), []byte(`%%% invalid`), 0644))
+
+ plugins, err := loadDir(dir, st)
+ require.NoError(t, err)
+ // bad.lua skipped, good.lua loaded
+ assert.Len(t, plugins, 1)
+ assert.Equal(t, "good.lua", plugins[0].name)
+ for _, p := range plugins {
+ p.close()
+ }
+}