diff --git a/internal/lua/boundary_test.go b/internal/lua/boundary_test.go new file mode 100644 index 0000000..adb99ea --- /dev/null +++ b/internal/lua/boundary_test.go @@ -0,0 +1,569 @@ +// 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 := 0; i < concurrency; i++ { + wg.Add(1) + go func(id int) { + defer wg.Done() + for j := 0; j < iterations; j++ { + 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()) +} diff --git a/internal/lua/scheduler_test.go b/internal/lua/scheduler_test.go new file mode 100644 index 0000000..68bac3f --- /dev/null +++ b/internal/lua/scheduler_test.go @@ -0,0 +1,483 @@ +// Package lua 提供 Scheduler 模式测试。 +// +// 该文件测试 Lua Scheduler 模式的功能: +// - setupSchedulerNgxAPI +// - setSchedulerMode / IsSchedulerMode +// - SchedulerUnsafeLocationAPI +// - SchedulerUnsafeVarAPI +// - SchedulerUnsafeCtxAPI +// +// 作者:xfy +package lua + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + glua "github.com/yuin/gopher-lua" +) + +// TestSchedulerModeFlag 测试 Scheduler 模式标志。 +func TestSchedulerModeFlag(t *testing.T) { + L := glua.NewState() + defer L.Close() + + // 初始状态不是 scheduler 模式 + if IsSchedulerMode(L) { + t.Error("Initial state should not be scheduler mode") + } + + // 设置 scheduler 模式 + setSchedulerMode(L, true) + if !IsSchedulerMode(L) { + t.Error("Should be in scheduler mode after setting") + } + + // 关闭 scheduler 模式 + setSchedulerMode(L, false) + if IsSchedulerMode(L) { + t.Error("Should not be in scheduler mode after unsetting") + } +} + +// TestSetupSchedulerNgxAPI 测试 setupSchedulerNgxAPI 创建安全的 ngx API。 +func TestSetupSchedulerNgxAPI(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + L := glua.NewState() + defer L.Close() + + // 调用 setupSchedulerNgxAPI + setupSchedulerNgxAPI(L, engine) + + // 验证 ngx 表存在 + ngx := L.GetGlobal("ngx") + if ngx == glua.LNil { + t.Fatal("ngx table should exist") + } + + ngxTable, ok := ngx.(*glua.LTable) + if !ok { + t.Fatal("ngx should be a table") + } + + // 验证 scheduler 模式已设置 + if !IsSchedulerMode(L) { + t.Error("Should be in scheduler mode after setupSchedulerNgxAPI") + } + + // 验证 ngx.shared 存在 + shared := ngxTable.RawGetString("shared") + if shared == glua.LNil { + t.Error("ngx.shared should exist") + } + + // 验证 ngx.log 存在 + log := ngxTable.RawGetString("log") + if log == glua.LNil { + t.Error("ngx.log should exist") + } + + // 验证 ngx.timer 存在 + timer := ngxTable.RawGetString("timer") + if timer == glua.LNil { + t.Error("ngx.timer should exist") + } +} + +// TestSchedulerUnsafeLocationAPI 测试 Scheduler 模式下 ngx.location 不可用。 +func TestSchedulerUnsafeLocationAPI(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + // 注册不安全的 location API + RegisterSchedulerUnsafeLocationAPI(L, ngx) + + // 验证 ngx.location 存在 + location := ngx.RawGetString("location") + if location == glua.LNil { + t.Fatal("ngx.location should exist") + } + + // 尝试调用 ngx.location.capture 应该返回错误 + err := L.DoString(` + local ok, err = pcall(function() + ngx.location.capture("/test") + end) + if ok then + error("ngx.location.capture should fail in scheduler mode") + end + `) + assert.NoError(t, err) +} + +// TestSchedulerUnsafeVarAPI 测试 Scheduler 模式下 ngx.var 不可用。 +func TestSchedulerUnsafeVarAPI(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + // 注册不安全的 var API + RegisterSchedulerUnsafeVarAPI(L, ngx) + + // 验证 ngx.var 存在 + v := ngx.RawGetString("var") + if v == glua.LNil { + t.Fatal("ngx.var should exist") + } + + // 尝试读取 ngx.var 应该返回错误 + err := L.DoString(` + local ok, err = pcall(function() + local x = ngx.var.some_var + end) + if ok then + error("reading ngx.var should fail in scheduler mode") + end + `) + assert.NoError(t, err) + + // 尝试写入 ngx.var 应该返回错误 + err = L.DoString(` + local ok, err = pcall(function() + ngx.var.some_var = "value" + end) + if ok then + error("writing ngx.var should fail in scheduler mode") + end + `) + assert.NoError(t, err) +} + +// TestSchedulerUnsafeCtxAPI 测试 Scheduler 模式下 ngx.ctx 不可用。 +func TestSchedulerUnsafeCtxAPISeparate(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + // 注册不安全的 ctx API + RegisterSchedulerUnsafeCtxAPI(L, ngx) + + // 验证 ngx.ctx 存在 + ctx := ngx.RawGetString("ctx") + if ctx == glua.LNil { + t.Fatal("ngx.ctx should exist") + } + + // 尝试读取 ngx.ctx 应该返回错误 + err := L.DoString(` + local ok, err = pcall(function() + local x = ngx.ctx.some_key + end) + if ok then + error("reading ngx.ctx should fail in scheduler mode") + end + `) + assert.NoError(t, err) + + // 尝试写入 ngx.ctx 应该返回错误 + err = L.DoString(` + local ok, err = pcall(function() + ngx.ctx.some_key = "value" + end) + if ok then + error("writing ngx.ctx should fail in scheduler mode") + end + `) + assert.NoError(t, err) +} + +// TestSchedulerUnsafeLogAPI 测试 Scheduler 模式下 ngx.log 可用。 +func TestSchedulerUnsafeLogAPI(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + // 注册 Scheduler 日志 API + RegisterSchedulerLogAPI(L, ngx) + + // 验证 ngx.log 存在 + log := ngx.RawGetString("log") + if log == glua.LNil { + t.Fatal("ngx.log should exist") + } + + // ngx.log 应该可以调用(不会报错) + err := L.DoString(` + ngx.log(ngx.ERR, "test log message") + `) + assert.NoError(t, err) +} + +// TestSchedulerUnsafeReqAPI 测试 Scheduler 模式下 ngx.req 不可用。 +func TestSchedulerUnsafeReqAPISeparate(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + // 注册不安全的 req API + RegisterSchedulerUnsafeReqAPI(L, ngx) + + // 验证 ngx["ngx.req"] 存在(RegisterUnsafeAPI 使用完整名称作为键) + req := ngx.RawGetString("ngx.req") + if req == glua.LNil { + t.Fatal("ngx[\"ngx.req\"] should exist") + } + + // 尝试调用 ngx.req.get_method 应该返回错误 + err := L.DoString(` + local ok, err = pcall(function() + ngx["ngx.req"].get_method() + end) + if ok then + error("ngx.req.get_method should fail in scheduler mode") + end + `) + assert.NoError(t, err) +} + +// TestSchedulerUnsafeRespAPI 测试 Scheduler 模式下 ngx.resp 不可用。 +func TestSchedulerUnsafeRespAPISeparate(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + // 注册不安全的 resp API + RegisterSchedulerUnsafeRespAPI(L, ngx) + + // 验证 ngx["ngx.resp"] 存在(RegisterUnsafeAPI 使用完整名称作为键) + resp := ngx.RawGetString("ngx.resp") + if resp == glua.LNil { + t.Fatal("ngx[\"ngx.resp\"] should exist") + } + + // 尝试调用 ngx.resp.get_headers 应该返回错误 + err := L.DoString(` + local ok, err = pcall(function() + ngx["ngx.resp"].get_headers() + end) + if ok then + error("ngx.resp.get_headers should fail in scheduler mode") + end + `) + assert.NoError(t, err) +} + +// TestSchedulerSharedDictAvailable 测试 Scheduler 模式下 ngx.shared 可用。 +func TestSchedulerSharedDictAvailable(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + // 创建共享字典 + engine.CreateSharedDict("test_dict", 100) + + L := glua.NewState() + defer L.Close() + + // 调用 setupSchedulerNgxAPI + setupSchedulerNgxAPI(L, engine) + + // 验证 ngx.shared.DICT 存在 + err = L.DoString(` + if ngx.shared == nil then + error("ngx.shared should exist") + end + local dict = ngx.shared.DICT("test_dict") + if dict == nil then + error("ngx.shared.DICT('test_dict') should return dict") + end + `) + assert.NoError(t, err) +} + +// TestSchedulerTimerAvailable 测试 Scheduler 模式下 ngx.timer 可用。 +func TestSchedulerTimerAvailable(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + L := glua.NewState() + defer L.Close() + + // 调用 setupSchedulerNgxAPI + setupSchedulerNgxAPI(L, engine) + + // 验证 ngx.timer 存在 + err = L.DoString(` + if ngx.timer == nil then + error("ngx.timer should exist") + end + if ngx.timer.at == nil then + error("ngx.timer.at should exist") + end + `) + assert.NoError(t, err) +} + +// TestSchedulerModeIsolation 测试 Scheduler 模式与普通模式隔离。 +func TestSchedulerModeIsolation(t *testing.T) { + // 创建两个 LState,一个在 scheduler 模式,一个不在 + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + // 普通 LState + normalL := glua.NewState() + defer normalL.Close() + + // Scheduler LState + schedulerL := glua.NewState() + defer schedulerL.Close() + + setupSchedulerNgxAPI(schedulerL, engine) + + // 验证普通 LState 不是 scheduler 模式 + if IsSchedulerMode(normalL) { + t.Error("Normal LState should not be in scheduler mode") + } + + // 验证 Scheduler LState 是 scheduler 模式 + if !IsSchedulerMode(schedulerL) { + t.Error("Scheduler LState should be in scheduler mode") + } +} + +// TestSchedulerErrorMessages 测试 Scheduler 模式下错误消息格式。 +func TestSchedulerErrorMessages(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + RegisterSchedulerUnsafeLocationAPI(L, ngx) + RegisterSchedulerUnsafeVarAPI(L, ngx) + RegisterSchedulerUnsafeCtxAPI(L, ngx) + + // 测试 location 错误消息 + err := L.DoString(` + local ok, err = pcall(function() + ngx.location.capture("/test") + end) + if not string.find(err, "not available") then + error("Error message should mention 'not available': " .. tostring(err)) + end + `) + assert.NoError(t, err) + + // 测试 var 错误消息 + err = L.DoString(` + local ok, err = pcall(function() + local x = ngx.var.test + end) + if not string.find(err, "not available") then + error("Error message should mention 'not available': " .. tostring(err)) + end + `) + assert.NoError(t, err) + + // 测试 ctx 错误消息 + err = L.DoString(` + local ok, err = pcall(function() + local x = ngx.ctx.test + end) + if not string.find(err, "not available") then + error("Error message should mention 'not available': " .. tostring(err)) + end + `) + assert.NoError(t, err) +} + +// TestSchedulerMultipleUnsafeCalls 测试多次调用不安全 API。 +func TestSchedulerMultipleUnsafeCalls(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + RegisterSchedulerUnsafeVarAPI(L, ngx) + + // 多次调用都应该失败 + err := L.DoString(` + for i = 1, 5 do + local ok, err = pcall(function() + ngx.var["key" .. i] = "value" .. i + end) + if ok then + error("Call " .. i .. " should have failed") + end + end + `) + assert.NoError(t, err) +} + +// TestSchedulerNilValueHandling 测试 Scheduler 模式下处理 nil 值。 +func TestSchedulerNilValueHandling(t *testing.T) { + L := glua.NewState() + defer L.Close() + + ngx := L.NewTable() + L.SetGlobal("ngx", ngx) + + RegisterSchedulerUnsafeVarAPI(L, ngx) + + // 尝试读取 nil 键 + err := L.DoString(` + local ok, err = pcall(function() + local x = ngx.var[nil] + end) + if ok then + error("Reading nil key should fail") + end + `) + assert.NoError(t, err) +} + +// TestSchedulerConcurrentAccess 测试并发访问 Scheduler API。 +func TestSchedulerConcurrentAccess(t *testing.T) { + engine, err := NewEngine(DefaultConfig()) + require.NoError(t, err) + defer engine.Close() + + // 创建多个 LState 并发访问 + done := make(chan bool, 10) + + for i := 0; i < 10; i++ { + go func() { + L := glua.NewState() + defer L.Close() + + setupSchedulerNgxAPI(L, engine) + + // 验证 scheduler 模式 + if !IsSchedulerMode(L) { + t.Error("Should be in scheduler mode") + } + + done <- true + }() + } + + // 等待所有 goroutine 完成 + for i := 0; i < 10; i++ { + <-done + } +}