lolly/internal/lua/constraints_test.go
xfy 6c7cf73c87 refactor(lua): replace single LState with LState pool architecture
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>
2026-05-09 10:38:10 +08:00

211 lines
5.5 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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