lolly/internal/lua/boundary_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

570 lines
14 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 模块的边界场景:
// - 协程创建失败处理
// - 定时器句柄操作
// - 共享字典容量上限
// - getVariable 所有变量类型
//
// 作者xfy
package lua
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
glua "github.com/yuin/gopher-lua"
)
// TestBoundaryCoroutineSandbox 测试协程沙箱阻止危险操作。
func TestBoundaryCoroutineSandbox(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 创建协程
co, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer co.Close()
// 设置沙箱
err = co.SetupSandbox()
require.NoError(t, err)
// 尝试创建协程应该失败 - 使用单个脚本执行
err = co.Execute(`
local ok, err = pcall(function()
coroutine.create(function() end)
end)
if not ok then
-- 预期错误
else
error("coroutine.create should be blocked")
end
`)
assert.NoError(t, err)
}
// TestBoundaryCoroutineYieldAllowed 测试协程 yield 仍然可用。
func TestBoundaryCoroutineYieldAllowed(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
co, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer co.Close()
err = co.SetupSandbox()
require.NoError(t, err)
// coroutine.yield 和 coroutine.status 应该可用
err = co.Execute(`
-- coroutine.status 应该可用
local status = coroutine.status
if type(status) ~= "function" then
error("coroutine.status should be a function")
end
-- coroutine.yield 应该可用
local yield = coroutine.yield
if type(yield) ~= "function" then
error("coroutine.yield should be a function")
end
`)
assert.NoError(t, err)
}
// TestBoundaryTimerHandleCancel 测试定时器句柄取消。
func TestBoundaryTimerHandleCancel(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
timerMgr := engine.TimerManager()
require.NotNil(t, timerMgr)
L := glua.NewState()
defer L.Close()
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterTimerAPI(L, timerMgr, ngx)
// 创建定时器并取消 - 使用全局函数避免闭包
err = L.DoString(`
function timer_callback()
-- 空回调
end
local handle, err = ngx.timer.at(10, timer_callback)
if not handle then
error("Failed to create timer: " .. tostring(err))
end
-- 取消定时器
local ok, err = handle:cancel()
if not ok then
error("Failed to cancel timer: " .. tostring(err))
end
`)
assert.NoError(t, err)
// 验证定时器已取消
assert.Equal(t, int32(0), timerMgr.ActiveCount())
}
// TestBoundaryTimerHandleDoubleCancel 测试重复取消定时器。
func TestBoundaryTimerHandleDoubleCancel(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
timerMgr := engine.TimerManager()
L := glua.NewState()
defer L.Close()
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterTimerAPI(L, timerMgr, ngx)
err = L.DoString(`
function timer_callback() end
local handle = ngx.timer.at(10, timer_callback)
if not handle then
error("Failed to create timer")
end
-- 第一次取消
local ok1 = handle:cancel()
-- 第二次取消应该失败
local ok2, err = handle:cancel()
if ok2 then
error("Double cancel should fail")
end
`)
assert.NoError(t, err)
}
// TestBoundaryTimerRunningCount 测试定时器运行计数。
func TestBoundaryTimerRunningCount(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
timerMgr := engine.TimerManager()
L := glua.NewState()
defer L.Close()
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterTimerAPI(L, timerMgr, ngx)
// 创建多个定时器
err = L.DoString(`
local count = ngx.timer.running_count()
if count ~= 0 then
error("Initial count should be 0")
end
-- 创建定时器
local h1 = ngx.timer.at(10, function() end)
local h2 = ngx.timer.at(10, function() end)
local h3 = ngx.timer.at(10, function() end)
count = ngx.timer.running_count()
if count ~= 3 then
error("Count should be 3, got " .. tostring(count))
end
-- 取消一个
h1:cancel()
count = ngx.timer.running_count()
if count ~= 2 then
error("Count should be 2 after cancel, got " .. tostring(count))
end
-- 清理
h2:cancel()
h3:cancel()
`)
assert.NoError(t, err)
}
// TestBoundaryTimerUpvalueRejected 测试定时器拒绝闭包变量。
func TestBoundaryTimerUpvalueRejected(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
timerMgr := engine.TimerManager()
L := glua.NewState()
defer L.Close()
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterTimerAPI(L, timerMgr, ngx)
// 尝试创建捕获 upvalue 的定时器应该失败
err = L.DoString(`
local captured = "value"
local handle, err = ngx.timer.at(1, function()
print(captured) -- 捕获 upvalue
end)
if handle then
handle:cancel()
error("Timer with upvalue should be rejected")
end
if not err then
error("Expected error for upvalue capture")
end
`)
assert.NoError(t, err)
}
// TestBoundarySharedDictCapacity 测试共享字典容量上限。
func TestBoundarySharedDictCapacity(t *testing.T) {
dict := NewSharedDict("test", 3) // 只允许 3 个条目
// 添加 3 个条目
ok, err := dict.Set("a", "1", 0)
assert.True(t, ok)
assert.NoError(t, err)
ok, err = dict.Set("b", "2", 0)
assert.True(t, ok)
assert.NoError(t, err)
ok, err = dict.Set("c", "3", 0)
assert.True(t, ok)
assert.NoError(t, err)
// 添加第 4 个应该淘汰 LRU
ok, err = dict.Set("d", "4", 0)
assert.True(t, ok)
assert.NoError(t, err)
// 验证大小仍然为 3
assert.Equal(t, 3, dict.Size())
// "a" 应该被淘汰 - Get 返回空值
val, _, _ := dict.Get("a")
assert.Equal(t, "", val) // 不存在返回空
// "d" 应该存在
val, expired, _ := dict.Get("d")
assert.False(t, expired)
assert.Equal(t, "4", val)
}
// TestBoundarySharedDictIncrNonNumeric 测试非数值自增。
func TestBoundarySharedDictIncrNonNumeric(t *testing.T) {
dict := NewSharedDict("test", 10)
// 设置非数值字符串
ok, _ := dict.Set("key", "not-a-number", 0)
assert.True(t, ok)
// 尝试自增应该失败
_, err := dict.Incr("key", 1)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not a number")
}
// TestBoundarySharedDictIncrNegative 测试负数自增。
func TestBoundarySharedDictIncrNegative(t *testing.T) {
dict := NewSharedDict("test", 10)
// 设置初始值
ok, _ := dict.Set("counter", "10", 0)
assert.True(t, ok)
// 负数自增
val, err := dict.Incr("counter", -3)
assert.NoError(t, err)
assert.Equal(t, 7, val)
// 再次负数自增到负数
val, err = dict.Incr("counter", -20)
assert.NoError(t, err)
assert.Equal(t, -13, val)
}
// TestBoundarySharedDictTTLExpiration 测试 TTL 过期。
func TestBoundarySharedDictTTLExpiration(t *testing.T) {
dict := NewSharedDict("test", 10)
// 设置短 TTL
ok, _ := dict.Set("ephemeral", "value", 100*time.Millisecond)
assert.True(t, ok)
// 立即读取应该存在
val, expired, _ := dict.Get("ephemeral")
assert.False(t, expired)
assert.Equal(t, "value", val)
// 等待过期
time.Sleep(150 * time.Millisecond)
// 再次读取应该过期
val, expired, _ = dict.Get("ephemeral")
assert.True(t, expired)
assert.Equal(t, "", val)
}
// TestBoundarySharedDictConcurrentAccess 测试并发访问。
func TestBoundarySharedDictConcurrentAccess(t *testing.T) {
dict := NewSharedDict("test", 100)
var wg sync.WaitGroup
concurrency := 10
iterations := 100
for i := range concurrency {
wg.Add(1)
go func(id int) {
defer wg.Done()
for range iterations {
key := "key-" + string(rune('0'+id%10))
_, _ = dict.Set(key, "value", 0)
_, _, _ = dict.Get(key)
}
}(i)
}
wg.Wait()
// 如果没有竞态条件,测试通过
}
// TestBoundaryGetVariableAllTypes 测试所有变量类型。
func TestBoundaryGetVariableAllTypes(t *testing.T) {
// 创建模拟请求上下文
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("http://example.com/path?arg1=value1&arg2=value2")
ctx.Request.Header.SetMethod("POST")
ctx.Request.Header.Set("User-Agent", "test-agent")
ctx.Request.Header.Set("Content-Type", "application/json")
ctx.Request.Header.Set("X-Custom", "custom-value")
api := newNgxVarAPI(ctx)
tests := []struct {
name string
expected string
}{
{"request_method", "POST"},
{"request_uri", "http://example.com/path?arg1=value1&arg2=value2"},
{"uri", "/path"},
{"query_string", "arg1=value1&arg2=value2"},
{"args", "arg1=value1&arg2=value2"},
{"http_host", "example.com"},
{"http_user_agent", "test-agent"},
{"http_content_type", "application/json"},
{"http_x-custom", "custom-value"},
{"arg_arg1", "value1"},
{"arg_arg2", "value2"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val := api.getVariable(tt.name)
assert.Equal(t, tt.expected, val)
})
}
}
// TestBoundaryGetVariableLuaTypes 测试 Lua 类型返回。
func TestBoundaryGetVariableLuaTypes(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
ctx.Request.SetRequestURI("http://example.com/path")
ctx.Request.Header.SetMethod("GET")
api := newNgxVarAPI(ctx)
// request_length 应该返回数值类型
lv := api.getVariableLua("request_length")
if lv == nil {
t.Log("request_length returned nil (acceptable)")
} else if _, ok := lv.(glua.LNumber); !ok {
t.Errorf("request_length should return LNumber, got %T", lv)
}
// request_method 应该返回字符串类型
lv = api.getVariableLua("request_method")
if lv == nil {
t.Error("request_method should not return nil")
} else if _, ok := lv.(glua.LString); !ok {
t.Errorf("request_method should return LString, got %T", lv)
}
}
// TestBoundaryGetVariableCustom 测试自定义变量。
func TestBoundaryGetVariableCustom(t *testing.T) {
ctx := &fasthttp.RequestCtx{}
api := newNgxVarAPI(ctx)
// 设置自定义变量
api.SetVariable("custom_var", "custom_value")
// 读取自定义变量
val, ok := api.GetVariable("custom_var")
assert.True(t, ok)
assert.Equal(t, "custom_value", val)
// 读取不存在的变量
_, ok = api.GetVariable("nonexistent")
assert.False(t, ok)
}
// TestBoundaryCoroutineExecutionTimeout 测试协程执行超时。
func TestBoundaryCoroutineExecutionTimeout(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 创建带超时的上下文
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
co, err := engine.NewCoroutine(nil)
require.NoError(t, err)
defer co.Close()
co.ExecutionContext = ctx
err = co.SetupSandbox()
require.NoError(t, err)
// 执行一个长时间运行的脚本应该被中断
err = co.Execute(`
local start = os.time()
while os.time() - start < 10 do
-- 忙等待
end
`)
// 超时错误是预期的
if err == nil {
t.Log("Script completed quickly (no timeout)")
}
}
// TestBoundaryTimerHandleString 测试定时器句柄字符串表示。
func TestBoundaryTimerHandleString(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
timerMgr := engine.TimerManager()
L := glua.NewState()
defer L.Close()
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
RegisterTimerAPI(L, timerMgr, ngx)
err = L.DoString(`
local handle = ngx.timer.at(10, function() end)
if not handle then
error("Failed to create timer")
end
local str = tostring(handle)
if not string.find(str, "ngx.timer.handle:") then
error("Invalid handle string: " .. str)
end
handle:cancel()
`)
assert.NoError(t, err)
}
// TestBoundaryTimerWaitAll 测试等待所有定时器完成。
func TestBoundaryTimerWaitAll(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
timerMgr := engine.TimerManager()
// 创建短延迟定时器
callback := func(L *glua.LState) int { return 0 }
L := glua.NewState()
defer L.Close()
fn := L.NewFunction(callback)
handle1, _ := timerMgr.At(50*time.Millisecond, fn, nil)
handle2, _ := timerMgr.At(100*time.Millisecond, fn, nil)
require.NotNil(t, handle1)
require.NotNil(t, handle2)
// 等待所有完成
completed := timerMgr.WaitAll(500 * time.Millisecond)
assert.True(t, completed)
// 活跃计数应该为 0
assert.Equal(t, int32(0), timerMgr.ActiveCount())
}
// TestBoundarySharedDictAddExisting 测试 Add 已存在的键。
func TestBoundarySharedDictAddExisting(t *testing.T) {
dict := NewSharedDict("test", 10)
// 第一次 Add 应该成功
ok, _ := dict.Add("key", "value1", 0)
assert.True(t, ok)
// 第二次 Add 应该失败
ok, _ = dict.Add("key", "value2", 0)
assert.False(t, ok)
// 值应该仍然是第一个
val, _, _ := dict.Get("key")
assert.Equal(t, "value1", val)
}
// TestBoundarySharedDictFlushExpired 测试批量清理过期条目。
func TestBoundarySharedDictFlushExpired(t *testing.T) {
dict := NewSharedDict("test", 10)
// 设置过期条目(它们会在 LRU 链表前端)
dict.Set("expire1", "value", 50*time.Millisecond)
dict.Set("expire2", "value", 50*time.Millisecond)
// 等待过期
time.Sleep(100 * time.Millisecond)
// 设置不过期条目(它们会在 LRU 链表前端,过期条目在尾部)
dict.Set("keep1", "value", time.Hour)
dict.Set("keep2", "value", time.Hour)
// 清理过期条目 - evictExpired 从 LRU 尾部开始检查
_ = dict.FlushExpired()
// 验证过期条目被清理
assert.Equal(t, 2, dict.Size())
}
// TestBoundarySharedDictFlushAll 测试清空字典。
func TestBoundarySharedDictFlushAll(t *testing.T) {
dict := NewSharedDict("test", 10)
// 添加多个条目
dict.Set("a", "1", 0)
dict.Set("b", "2", 0)
dict.Set("c", "3", 0)
assert.Equal(t, 3, dict.Size())
// 清空
_ = dict.FlushAll()
assert.Equal(t, 0, dict.Size())
assert.Equal(t, 10, dict.FreeSlots())
}