Replace the single LState + coroutine model with an LState pool to eliminate concurrent map read/write issues in gopher-lua. Each request now gets a completely independent LState with its own Global table. Key changes: - Add LStatePool for managing pooled LState instances - Remove shared Engine.L and coroutine-based execution - Simplify coroutine.go: remove yield handling, use direct PCall - Remove ngxRegisterMu lock (no longer needed with isolated LStates) - Update config.go: add LStatePoolInitialSize/MaxSize settings - Update tests to work with the new architecture Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
211 lines
5.5 KiB
Go
211 lines
5.5 KiB
Go
// Package lua 提供 Lua 脚本嵌入能力
|
||
package lua
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
glua "github.com/yuin/gopher-lua"
|
||
)
|
||
|
||
// TestNewEngine 测试引擎创建
|
||
func TestNewEngine(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
require.NotNil(t, engine)
|
||
defer engine.Close()
|
||
|
||
assert.NotNil(t, engine.lstatePool)
|
||
assert.NotNil(t, engine.codeCache)
|
||
assert.Equal(t, int32(0), engine.ActiveCoroutines())
|
||
}
|
||
|
||
// TestNewCoroutine 测试协程创建
|
||
func TestNewCoroutine(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
coro, err := engine.NewCoroutine(nil)
|
||
require.NoError(t, err)
|
||
require.NotNil(t, coro)
|
||
require.NotNil(t, coro.Co)
|
||
|
||
defer coro.Close()
|
||
|
||
assert.Equal(t, int32(1), engine.ActiveCoroutines())
|
||
}
|
||
|
||
// TestCoroutineDeadAfterResumeOK 验证协程 ResumeOK 后变成 dead 不能复用
|
||
// 注意:gopher-lua 的 Resume 对 dead coroutine 会 panic,无法安全测试
|
||
// 此测试验证 ResumeOK 正常完成,证明协程生命周期正确
|
||
func TestCoroutineDeadAfterResumeOK(t *testing.T) {
|
||
L := glua.NewState()
|
||
defer L.Close()
|
||
|
||
// 创建协程
|
||
co, cocancel := L.NewThread()
|
||
require.NotNil(t, co)
|
||
if cocancel != nil {
|
||
defer cocancel()
|
||
}
|
||
|
||
// 编译简单脚本
|
||
proto, err := engineCodeToProto("return 42")
|
||
require.NoError(t, err)
|
||
|
||
fn := L.NewFunctionFromProto(proto)
|
||
|
||
// 执行协程,应该正常完成
|
||
st, err, values := L.Resume(co, fn)
|
||
assert.Equal(t, glua.ResumeOK, st)
|
||
assert.NoError(t, err)
|
||
assert.Len(t, values, 1)
|
||
assert.Equal(t, glua.LNumber(42), values[0])
|
||
|
||
// 协程完成后变成 dead 状态
|
||
// 注意:再次调用 Resume(co, fn) 会 panic
|
||
// 实际使用中必须确保每个协程只使用一次
|
||
}
|
||
|
||
// TestLFunctionCannotCrossLState 验证 LFunction 不能跨 LState 使用
|
||
// 注意:FunctionProto 可以跨 LState 使用,但 LFunction 绑定到特定 LState
|
||
// 这个测试验证的是 FunctionProto 共享的正确性
|
||
func TestLFunctionCannotCrossLState(t *testing.T) {
|
||
L1 := glua.NewState()
|
||
defer L1.Close()
|
||
|
||
// 在 L1 中编译脚本并执行
|
||
proto, err := engineCodeToProto("return 42")
|
||
require.NoError(t, err)
|
||
|
||
fn := L1.NewFunctionFromProto(proto)
|
||
L1.Push(fn)
|
||
err = L1.PCall(0, 1, nil)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, glua.LNumber(42), L1.Get(-1))
|
||
L1.Pop(1)
|
||
|
||
// FunctionProto 可以在不同 LState 使用(这是缓存的核心假设)
|
||
L2 := glua.NewState()
|
||
defer L2.Close()
|
||
|
||
fn2 := L2.NewFunctionFromProto(proto) // 从同一个 proto 创建新的函数
|
||
L2.Push(fn2)
|
||
err = L2.PCall(0, 1, nil)
|
||
require.NoError(t, err)
|
||
assert.Equal(t, glua.LNumber(42), L2.Get(-1))
|
||
L2.Pop(1)
|
||
}
|
||
|
||
// TestNewThreadInheritsGlobals 验证 NewThread 继承全局环境
|
||
func TestNewThreadInheritsGlobals(t *testing.T) {
|
||
L := glua.NewState()
|
||
defer L.Close()
|
||
|
||
// 在主 LState 设置全局变量
|
||
L.SetGlobal("test_global", glua.LString("shared_value"))
|
||
|
||
// 创建协程
|
||
co, cocancel := L.NewThread()
|
||
require.NotNil(t, co)
|
||
if cocancel != nil {
|
||
defer cocancel()
|
||
}
|
||
|
||
// 协程应该能访问主 LState 的全局变量
|
||
proto, err := engineCodeToProto("return test_global")
|
||
require.NoError(t, err)
|
||
|
||
fn := L.NewFunctionFromProto(proto)
|
||
st, err, values := L.Resume(co, fn)
|
||
assert.Equal(t, glua.ResumeOK, st)
|
||
assert.NoError(t, err)
|
||
assert.Len(t, values, 1)
|
||
assert.Equal(t, glua.LString("shared_value"), values[0])
|
||
}
|
||
|
||
// TestPerRequestEnvSandbox 验证 _ENV 沙箱隔离
|
||
func TestPerRequestEnvSandbox(t *testing.T) {
|
||
engine, err := NewEngine(DefaultConfig())
|
||
require.NoError(t, err)
|
||
defer engine.Close()
|
||
|
||
// 创建第一个协程并设置沙箱
|
||
coro1, err := engine.NewCoroutine(nil)
|
||
require.NoError(t, err)
|
||
require.NotNil(t, coro1)
|
||
|
||
err = coro1.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// 在沙箱中设置变量
|
||
err = coro1.Execute("local x = 1")
|
||
assert.NoError(t, err)
|
||
|
||
// 创建第二个协程
|
||
coro2, err := engine.NewCoroutine(nil)
|
||
require.NoError(t, err)
|
||
require.NotNil(t, coro2)
|
||
|
||
err = coro2.SetupSandbox()
|
||
require.NoError(t, err)
|
||
|
||
// 第二个协程不应该看到第一个协程的变量
|
||
// 由于我们使用了 _ENV 沙箱,局部变量是隔离的
|
||
coro1.Close()
|
||
coro2.Close()
|
||
}
|
||
|
||
// TestCodeCache 测试字节码缓存
|
||
func TestCodeCache(t *testing.T) {
|
||
cache := NewCodeCache(100, 0, false)
|
||
|
||
script := "return 1 + 1"
|
||
|
||
// 第一次编译
|
||
proto1, err := cache.GetOrCompileInline(script)
|
||
require.NoError(t, err)
|
||
require.NotNil(t, proto1)
|
||
|
||
// 第二次应该命中缓存
|
||
proto2, err := cache.GetOrCompileInline(script)
|
||
require.NoError(t, err)
|
||
require.NotNil(t, proto2)
|
||
|
||
// 相同的脚本应该返回相同的字节码
|
||
assert.Equal(t, proto1, proto2)
|
||
|
||
// 检查命中率
|
||
hits, misses, _ := cache.Stats()
|
||
assert.Equal(t, uint64(1), hits)
|
||
assert.Equal(t, uint64(1), misses)
|
||
}
|
||
|
||
// TestCodeCacheDifferentScripts 测试不同脚本的缓存
|
||
func TestCodeCacheDifferentScripts(t *testing.T) {
|
||
cache := NewCodeCache(100, 0, false)
|
||
|
||
proto1, err := cache.GetOrCompileInline("return 1")
|
||
require.NoError(t, err)
|
||
|
||
proto2, err := cache.GetOrCompileInline("return 2")
|
||
require.NoError(t, err)
|
||
|
||
// 不同脚本应该产生不同的字节码
|
||
assert.NotEqual(t, proto1, proto2)
|
||
|
||
hits, misses, _ := cache.Stats()
|
||
assert.Equal(t, uint64(0), hits) // 都是 miss
|
||
assert.Equal(t, uint64(2), misses)
|
||
}
|
||
|
||
// Helper function: compile Lua code to FunctionProto
|
||
func engineCodeToProto(src string) (*glua.FunctionProto, error) {
|
||
return cacheGetOrCompile(src)
|
||
}
|
||
|
||
// Package-level helper for testing
|
||
var cacheGetOrCompile = NewCodeCache(100, 0, false).GetOrCompileInline
|