lolly/internal/lua/lua_test.go
xfy f145a8770e refactor: modernize code with Go 1.22+ features
Apply modern Go patterns across the codebase:
- Replace `interface{}` with `any` (Go 1.18+)
- Use `for range n` instead of `for i := 0; i < n; i++` (Go 1.22+)
- Replace `sort.Slice` with `slices.Sort` from slices package
- Simplify sync.WaitGroup patterns with errgroup where appropriate
- Add Makefile targets for modernize analyzer

Total: 84 files updated, net reduction of 79 lines

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 10:37:45 +08:00

635 lines
18 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 (
"os"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
)
// TestLuaContext 测试 LuaContext 基础功能
func TestLuaContext(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := NewContext(engine, nil)
require.NotNil(t, ctx)
assert.NotNil(t, ctx.Engine)
assert.NotNil(t, ctx.Variables)
assert.Equal(t, PhaseInit, ctx.Phase)
// 测试变量操作
ctx.SetVariable("test_key", "test_value")
val, ok := ctx.GetVariable("test_key")
assert.True(t, ok)
assert.Equal(t, "test_value", val)
// 测试未存在的变量
_, ok = ctx.GetVariable("nonexistent")
assert.False(t, ok)
}
// TestLuaContextPhase 测试阶段设置
func TestLuaContextPhase(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := NewContext(engine, nil)
// 测试所有阶段
phases := []Phase{PhaseInit, PhaseRewrite, PhaseAccess, PhaseContent, PhaseLog, PhaseHeaderFilter, PhaseBodyFilter}
for _, p := range phases {
ctx.SetPhase(p)
assert.Equal(t, p, ctx.GetPhase())
}
// 测试阶段字符串
assert.Equal(t, "init", PhaseInit.String())
assert.Equal(t, "rewrite", PhaseRewrite.String())
assert.Equal(t, "access", PhaseAccess.String())
assert.Equal(t, "content", PhaseContent.String())
assert.Equal(t, "log", PhaseLog.String())
assert.Equal(t, "header_filter", PhaseHeaderFilter.String())
assert.Equal(t, "body_filter", PhaseBodyFilter.String())
}
// TestLuaContextOutput 测试输出缓冲
func TestLuaContextOutput(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := NewContext(engine, nil)
// 测试 Write
ctx.Write([]byte("hello"))
assert.Equal(t, []byte("hello"), ctx.OutputBuffer)
// 测试 Say - Say 会添加 data 然后换行
ctx.OutputBuffer = nil // 清空重新测试
ctx.Say("hello")
assert.Equal(t, []byte("hello\n"), ctx.OutputBuffer)
}
// TestLuaContextFlushOutput 测试刷新输出
func TestLuaContextFlushOutput(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 当 RequestCtx 为 nil 时FlushOutput 应该安全处理
ctx := NewContext(engine, nil)
ctx.OutputBuffer = []byte("test output")
// FlushOutput 应该不会 panicRequestCtx 为 nil
ctx.FlushOutput()
// OutputBuffer 应该保持不变(因为 RequestCtx 为 nil
assert.NotNil(t, ctx.OutputBuffer)
}
// TestLuaContextPoolStateIsolation 测试池化 context 请求间无状态污染
func TestLuaContextPoolStateIsolation(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 第一次使用:设置变量、输出、阶段、退出标记
ctx1 := NewContext(engine, nil)
ctx1.SetVariable("key1", "value1")
ctx1.SetVariable("key2", "value2")
ctx1.Write([]byte("hello"))
ctx1.SetPhase(PhaseAccess)
ctx1.Exited = true
// 释放回池
ctx1.Release()
// 第二次使用:从池中获取(可能是同一个对象)
ctx2 := NewContext(engine, nil)
// 验证无状态污染
assert.Equal(t, PhaseInit, ctx2.Phase, "Phase should be reset to PhaseInit")
assert.False(t, ctx2.Exited, "Exited should be reset to false")
assert.Empty(t, ctx2.OutputBuffer, "OutputBuffer should be empty")
assert.Empty(t, ctx2.Variables, "Variables map should be empty")
// 验证旧的 key 不存在
_, ok := ctx2.GetVariable("key1")
assert.False(t, ok, "key1 should not exist after release")
_, ok = ctx2.GetVariable("key2")
assert.False(t, ok, "key2 should not exist after release")
ctx2.Release()
}
// TestLuaContextPoolMultipleReuse 测试多次复用
func TestLuaContextPoolMultipleReuse(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 循环多次 release/acquire验证状态始终正确
for range 100 {
ctx := NewContext(engine, nil)
ctx.SetVariable("iter", "val")
ctx.Write([]byte("data"))
ctx.SetPhase(PhaseLog)
ctx.Exited = true
ctx.Release()
}
// 最后一次获取,验证状态干净
ctx := NewContext(engine, nil)
assert.Equal(t, PhaseInit, ctx.Phase)
assert.False(t, ctx.Exited)
assert.Empty(t, ctx.OutputBuffer)
assert.Empty(t, ctx.Variables)
ctx.Release()
}
// TestLuaContextExecute 测试 Lua 执行
func TestLuaContextExecute(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := NewContext(engine, nil)
// 执行简单脚本
err = ctx.Execute("local x = 1 + 1")
assert.NoError(t, err)
// Release
ctx.Release()
assert.Nil(t, ctx.Coroutine)
}
// TestLuaContextExecuteFile 测试文件执行
func TestLuaContextExecuteFile(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
ctx := NewContext(engine, nil)
// 创建临时 Lua 文件
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "test.lua")
err = os.WriteFile(scriptPath, []byte("return 42"), 0o644)
require.NoError(t, err)
// 执行文件
err = ctx.ExecuteFile(scriptPath)
assert.NoError(t, err)
ctx.Release()
}
// TestLuaCoroutineExecute 测试协程执行
func TestLuaCoroutineExecute(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)
defer coro.Close()
// 设置沙箱
err = coro.SetupSandbox()
require.NoError(t, err)
// 执行脚本
err = coro.Execute("return 42")
assert.NoError(t, err)
}
// TestLuaCoroutineExecuteWithYield 测试 yield/resume
func TestLuaCoroutineExecuteWithYield(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)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 执行带 yield 的脚本 - 验证 yield/resume 循环
// 注意:需要注册 lolly.sleep 函数才能正确处理 yield
err = coro.Execute("local x = 1; return x + 1")
assert.NoError(t, err)
}
// TestLuaCoroutineExecuteFile 测试协程文件执行
func TestLuaCoroutineExecuteFile(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer coro.Close()
// 创建临时文件
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "test.lua")
err = os.WriteFile(scriptPath, []byte("return 42"), 0o644)
require.NoError(t, err)
err = coro.ExecuteFile(scriptPath)
assert.NoError(t, err)
}
// TestLuaCoroutineExecuteError 测试执行错误
func TestLuaCoroutineExecuteError(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer coro.Close()
// 编译错误
err = coro.Execute("invalid lua syntax !!!")
assert.Error(t, err)
assert.Contains(t, err.Error(), "compile")
// 运行时错误
err = coro.Execute("error('runtime error')")
assert.Error(t, err)
}
// TestLuaCoroutineExecuteFileError 测试文件执行错误
func TestLuaCoroutineExecuteFileError(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer coro.Close()
// 不存在的文件
err = coro.ExecuteFile("/nonexistent/path.lua")
assert.Error(t, err)
}
// TestLuaCoroutineHandleYield 测试 yield 处理
func TestLuaCoroutineHandleYield(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试 unknown yield reason - 会返回错误
// 因为 handleYield 会检查 yield reason
err = coro.Execute("coroutine.yield('unknown_reason')")
assert.Error(t, err) // unknown yield reason
}
// TestLuaCoroutineHandleSleep 测试 sleep yield 处理
// 注意:需要 coroutine 库支持,当前沙箱未加载
func TestLuaCoroutineHandleSleep(t *testing.T) {
engine, err := NewEngine(&Config{
MaxConcurrentCoroutines: 1000,
MaxExecutionTime: 5 * time.Second,
})
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer coro.Close()
// 不设置沙箱,使用全局环境(包含 coroutine 库)
// 简单测试 execute 和 yield 循环的基本路径
err = coro.Execute("return 1 + 1")
assert.NoError(t, err)
// 测试错误路径 - yield 无参数
err = coro.Execute("coroutine.yield()")
// 由于 coroutine 库可能在沙箱中不可用,这个测试可能返回编译错误或运行时错误
// 重点覆盖代码路径
_ = err
}
// TestCodeCacheFile 测试文件缓存
func TestCodeCacheFile(t *testing.T) {
// 创建临时 Lua 文件
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "test.lua")
scriptContent := "return 42"
err := os.WriteFile(scriptPath, []byte(scriptContent), 0o644)
require.NoError(t, err)
cache := NewCodeCache(100, time.Hour, true)
// 第一次编译文件
proto1, err := cache.GetOrCompileFile(scriptPath)
require.NoError(t, err)
require.NotNil(t, proto1)
// 第二次应该命中缓存
proto2, err := cache.GetOrCompileFile(scriptPath)
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)
}
// TestCodeCacheEviction 测试缓存淘汰
func TestCodeCacheEviction(t *testing.T) {
cache := NewCodeCache(2, 0, false) // 只存 2 个
// 编译 3 个脚本,触发淘汰
_, err := cache.GetOrCompileInline("return 1")
require.NoError(t, err)
_, err = cache.GetOrCompileInline("return 2")
require.NoError(t, err)
_, err = cache.GetOrCompileInline("return 3")
require.NoError(t, err)
// 第一个应该被淘汰了
hits, misses, size := cache.Stats()
assert.Equal(t, uint64(0), hits)
assert.Equal(t, uint64(3), misses)
assert.LessOrEqual(t, size, 2)
}
// TestCodeCacheTTL 测试 TTL 过期后重新编译
func TestCodeCacheTTL(t *testing.T) {
cache := NewCodeCache(100, 100*time.Millisecond, false)
script := "return 1"
// 编译脚本
_, err := cache.GetOrCompileInline(script)
require.NoError(t, err)
// 等待 TTL 过期
time.Sleep(150 * time.Millisecond)
// 应该重新编译miss
_, err = cache.GetOrCompileInline(script)
require.NoError(t, err)
// 检查 stats两次 miss因为 TTL 过期后重新编译
hits, misses, _ := cache.Stats()
assert.Equal(t, uint64(0), hits) // 没有 hit
assert.Equal(t, uint64(2), misses) // 两次 miss
}
// TestCodeCacheClear 测试清空缓存
func TestCodeCacheClear(t *testing.T) {
cache := NewCodeCache(100, 0, false)
// 添加一些缓存
_, err := cache.GetOrCompileInline("return 1")
require.NoError(t, err)
_, err = cache.GetOrCompileInline("return 2")
require.NoError(t, err)
hits, misses, size := cache.Stats()
assert.Equal(t, 2, size)
_ = hits
_ = misses
// 清空
cache.Clear()
hits, misses, size = cache.Stats()
assert.Equal(t, 0, size)
_ = hits
_ = misses
}
// TestCodeCacheHitRate 测试命中率
func TestCodeCacheHitRate(t *testing.T) {
cache := NewCodeCache(100, 0, false)
script := "return 1"
// 第一次 miss
_, err := cache.GetOrCompileInline(script)
require.NoError(t, err)
// 第二次 hit
_, err = cache.GetOrCompileInline(script)
require.NoError(t, err)
// 第三次 hit
_, err = cache.GetOrCompileInline(script)
require.NoError(t, err)
hitRate := cache.HitRate()
assert.Equal(t, 2.0/3.0, hitRate)
}
// TestEngineStats 测试引擎统计
func TestEngineStats(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 初始统计应该为 0
stats := engine.Stats()
assert.Equal(t, uint64(0), stats.CoroutinesCreated)
assert.Equal(t, uint64(0), stats.CoroutinesClosed)
// 创建协程
coro, err := engine.NewCoroutine(nil)
require.NoError(t, err)
stats = engine.Stats()
assert.Equal(t, uint64(1), stats.CoroutinesCreated)
assert.Equal(t, uint64(0), stats.CoroutinesClosed)
// 关闭协程
coro.Close()
stats = engine.Stats()
assert.Equal(t, uint64(1), stats.CoroutinesCreated)
assert.Equal(t, uint64(1), stats.CoroutinesClosed)
}
// TestEngineCodeCache 测试引擎字节码缓存访问
func TestEngineCodeCache(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
cache := engine.CodeCache()
require.NotNil(t, cache)
// 通过引擎缓存编译
proto, err := cache.GetOrCompileInline("return 42")
require.NoError(t, err)
require.NotNil(t, proto)
}
// TestConfig 测试配置
func TestConfig(t *testing.T) {
config := DefaultConfig()
require.NotNil(t, config)
// 默认配置值
assert.Equal(t, 1000, config.MaxConcurrentCoroutines)
assert.Equal(t, 30*time.Second, config.MaxExecutionTime)
assert.Equal(t, 1000, config.CodeCacheSize)
// 测试自定义配置
customConfig := &Config{
MaxConcurrentCoroutines: 100,
MaxExecutionTime: time.Minute,
CodeCacheSize: 200,
}
engine, err := NewEngine(customConfig)
require.NoError(t, err)
defer engine.Close()
assert.Equal(t, 100, engine.maxCoroutines)
}
// TestNgxAPIRegistrationInSandbox 测试所有 ngx API 在沙箱中的注册
func TestNgxAPIRegistrationInSandbox(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 创建 mock RequestCtxngx.req/resp/log API 需要 RequestCtx
mockCtx := &fasthttp.RequestCtx{}
coro, err := engine.NewCoroutine(mockCtx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 验证 ngx 表存在
err = coro.Execute(`
assert(ngx ~= nil, "ngx table should exist")
assert(type(ngx) == "table", "ngx should be a table")
`)
assert.NoError(t, err)
// 验证 ngx.req API 存在
coro2, err := engine.NewCoroutine(mockCtx)
require.NoError(t, err)
defer coro2.Close()
err = coro2.SetupSandbox()
require.NoError(t, err)
err = coro2.Execute(`
assert(ngx.req ~= nil, "ngx.req should exist")
assert(type(ngx.req.get_method) == "function", "ngx.req.get_method should be a function")
assert(type(ngx.req.get_uri) == "function", "ngx.req.get_uri should be a function")
assert(type(ngx.req.set_uri) == "function", "ngx.req.set_uri should be a function")
assert(type(ngx.req.get_uri_args) == "function", "ngx.req.get_uri_args should be a function")
assert(type(ngx.req.get_headers) == "function", "ngx.req.get_headers should be a function")
assert(type(ngx.req.set_header) == "function", "ngx.req.set_header should be a function")
assert(type(ngx.req.clear_header) == "function", "ngx.req.clear_header should be a function")
assert(type(ngx.req.get_body_data) == "function", "ngx.req.get_body_data should be a function")
`)
assert.NoError(t, err)
// 验证 ngx.resp API 存在
coro3, err := engine.NewCoroutine(mockCtx)
require.NoError(t, err)
defer coro3.Close()
err = coro3.SetupSandbox()
require.NoError(t, err)
err = coro3.Execute(`
assert(ngx.resp ~= nil, "ngx.resp should exist")
assert(type(ngx.resp.get_status) == "function", "ngx.resp.get_status should be a function")
assert(type(ngx.resp.set_status) == "function", "ngx.resp.set_status should be a function")
assert(type(ngx.resp.get_headers) == "function", "ngx.resp.get_headers should be a function")
assert(type(ngx.resp.set_header) == "function", "ngx.resp.set_header should be a function")
assert(type(ngx.resp.clear_header) == "function", "ngx.resp.clear_header should be a function")
`)
assert.NoError(t, err)
// 验证 ngx.var API 存在
coro4, err := engine.NewCoroutine(mockCtx)
require.NoError(t, err)
defer coro4.Close()
err = coro4.SetupSandbox()
require.NoError(t, err)
err = coro4.Execute(`
assert(ngx.var ~= nil, "ngx.var should exist")
`)
assert.NoError(t, err)
// 验证 ngx.ctx API 存在
coro5, err := engine.NewCoroutine(mockCtx)
require.NoError(t, err)
defer coro5.Close()
err = coro5.SetupSandbox()
require.NoError(t, err)
err = coro5.Execute(`
assert(ngx.ctx ~= nil, "ngx.ctx should exist")
assert(type(ngx.ctx) == "table", "ngx.ctx should be a table")
`)
assert.NoError(t, err)
// 验证 ngx.log API 存在(日志级别常量和函数)
coro6, err := engine.NewCoroutine(mockCtx)
require.NoError(t, err)
defer coro6.Close()
err = coro6.SetupSandbox()
require.NoError(t, err)
err = coro6.Execute(`
assert(ngx.log ~= nil, "ngx.log should exist")
assert(type(ngx.log) == "function", "ngx.log should be a function")
assert(ngx.ERR ~= nil, "ngx.ERR should exist")
assert(ngx.WARN ~= nil, "ngx.WARN should exist")
assert(ngx.INFO ~= nil, "ngx.INFO should exist")
assert(ngx.DEBUG ~= nil, "ngx.DEBUG should exist")
assert(type(ngx.say) == "function", "ngx.say should be a function")
assert(type(ngx.print) == "function", "ngx.print should be a function")
assert(type(ngx.flush) == "function", "ngx.flush should be a function")
assert(type(ngx.exit) == "function", "ngx.exit should be a function")
assert(type(ngx.redirect) == "function", "ngx.redirect should be a function")
`)
assert.NoError(t, err)
// 验证 ngx.socket API 存在
coro7, err := engine.NewCoroutine(mockCtx)
require.NoError(t, err)
defer coro7.Close()
err = coro7.SetupSandbox()
require.NoError(t, err)
err = coro7.Execute(`
assert(ngx.socket ~= nil, "ngx.socket should exist")
assert(type(ngx.socket.tcp) == "function", "ngx.socket.tcp should be a function")
`)
assert.NoError(t, err)
}