lolly/internal/lua/api_ctx_test.go
xfy f123018f2d feat(lua): 增强变量 API 支持数值类型返回和测试覆盖
- 新增 getVariableLua 方法返回 Lua 类型而非字符串
- request_length 等变量返回数值类型而非字符串
- luaVarNewIndex 支持 nil 值转换为空字符串
- 添加 api_var 全面的单元测试覆盖
- 添加 api_ctx 上下文 API 测试
- 添加 api_socket_tcp TCP socket 测试

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 18:12:16 +08:00

701 lines
16 KiB
Go
Raw 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 提供 ngx.ctx API 测试
package lua
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
glua "github.com/yuin/gopher-lua"
)
// TestNgxCtxAPI 测试 ngx.ctx API 基础功能
func TestNgxCtxAPI(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试 ngx.ctx 存在且是一个 table
err = coro.Execute(`
if type(ngx) ~= "table" then
error("ngx is not a table")
end
if type(ngx.ctx) ~= "table" then
error("ngx.ctx is not a table")
end
`)
assert.NoError(t, err)
}
// TestNgxCtxStringValue 测试存储字符串值
func TestNgxCtxStringValue(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试字符串存储
err = coro.Execute(`
ngx.ctx.message = "hello world"
local msg = ngx.ctx.message
if msg ~= "hello world" then
error("string value mismatch: " .. tostring(msg))
end
`)
assert.NoError(t, err)
}
// TestNgxCtxNumberValue 测试存储数字值
func TestNgxCtxNumberValue(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试数字存储
err = coro.Execute(`
ngx.ctx.count = 42
ngx.ctx.pi = 3.14159
local count = ngx.ctx.count
local pi = ngx.ctx.pi
if count ~= 42 then
error("count should be 42, got: " .. tostring(count))
end
if math.abs(pi - 3.14159) > 0.00001 then
error("pi should be 3.14159, got: " .. tostring(pi))
end
`)
assert.NoError(t, err)
}
// TestNgxCtxTableValue 测试存储 table 值
func TestNgxCtxTableValue(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试 table 存储
err = coro.Execute(`
ngx.ctx.data = {
name = "test",
items = {1, 2, 3, 4, 5}
}
local data = ngx.ctx.data
if type(data) ~= "table" then
error("data should be a table")
end
if data.name ~= "test" then
error("data.name should be 'test'")
end
if type(data.items) ~= "table" then
error("data.items should be a table")
end
if data.items[1] ~= 1 then
error("data.items[1] should be 1")
end
if data.items[5] ~= 5 then
error("data.items[5] should be 5")
end
`)
assert.NoError(t, err)
}
// TestNgxCtxFunctionValue 测试存储函数值
func TestNgxCtxFunctionValue(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试函数存储
err = coro.Execute(`
ngx.ctx.handler = function(x)
return x * 2
end
local handler = ngx.ctx.handler
if type(handler) ~= "function" then
error("handler should be a function")
end
local result = handler(21)
if result ~= 42 then
error("handler(21) should return 42, got: " .. tostring(result))
end
`)
assert.NoError(t, err)
}
// TestNgxCtxBooleanValue 测试存储布尔值
func TestNgxCtxBooleanValue(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试布尔值存储
err = coro.Execute(`
ngx.ctx.enabled = true
ngx.ctx.disabled = false
if ngx.ctx.enabled ~= true then
error("enabled should be true")
end
if ngx.ctx.disabled ~= false then
error("disabled should be false")
end
`)
assert.NoError(t, err)
}
// TestNgxCtxNilValue 测试存储和读取 nil 值
func TestNgxCtxNilValue(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试 nil 值
err = coro.Execute(`
ngx.ctx.nothing = nil
local val = ngx.ctx.nothing
if val ~= nil then
error("nothing should be nil")
end
-- 读取不存在的键
local missing = ngx.ctx.missing_key
if missing ~= nil then
error("missing_key should be nil")
end
`)
assert.NoError(t, err)
}
// TestNgxCtxMultipleScripts 测试在同一个脚本中读写 ngx.ctx
// 注意:协程在执行后变成 dead 状态,不能多次执行
func TestNgxCtxMultipleScripts(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 在同一个脚本中设置和读取值(协程只能执行一次)
err = coro.Execute(`
-- 设置值
ngx.ctx.shared_value = "shared"
ngx.ctx.counter = 1
-- 读取并验证值
local val = ngx.ctx.shared_value
if val ~= "shared" then
error("shared_value should be 'shared'")
end
-- 修改值
ngx.ctx.counter = ngx.ctx.counter + 1
if ngx.ctx.counter ~= 2 then
error("counter should be 2")
end
`)
assert.NoError(t, err)
}
// TestNgxCtxNestedTable 测试嵌套 table
func TestNgxCtxNestedTable(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试嵌套 table
err = coro.Execute(`
ngx.ctx.config = {
database = {
host = "localhost",
port = 5432
},
cache = {
ttl = 3600
}
}
local host = ngx.ctx.config.database.host
if host ~= "localhost" then
error("config.database.host should be 'localhost'")
end
local port = ngx.ctx.config.database.port
if port ~= 5432 then
error("config.database.port should be 5432")
end
ngx.ctx.config.database.port = 3306
if ngx.ctx.config.database.port ~= 3306 then
error("config.database.port should be updated to 3306")
end
`)
assert.NoError(t, err)
}
// TestNgxCtxRequestIsolation 测试请求间上下文隔离
func TestNgxCtxRequestIsolation(t *testing.T) {
var req1, req2 fasthttp.Request
req1.Header.SetMethod("GET")
req1.Header.SetRequestURI("/request1")
req2.Header.SetMethod("GET")
req2.Header.SetRequestURI("/request2")
ctx1 := &fasthttp.RequestCtx{}
ctx1.Init(&req1, nil, nil)
ctx2 := &fasthttp.RequestCtx{}
ctx2.Init(&req2, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 第一个请求:设置 ctx 值
coro1, err := engine.NewCoroutine(ctx1)
require.NoError(t, err)
err = coro1.SetupSandbox()
require.NoError(t, err)
err = coro1.Execute(`
ngx.ctx.request_id = 1
ngx.ctx.message = "request1_data"
`)
assert.NoError(t, err)
coro1.Close()
// 第二个请求:验证 ctx 与其他请求隔离
coro2, err := engine.NewCoroutine(ctx2)
require.NoError(t, err)
err = coro2.SetupSandbox()
require.NoError(t, err)
err = coro2.Execute(`
-- 第一个请求的值不应该影响第二个请求
if ngx.ctx.request_id ~= nil then
error("ctx from another request should be isolated")
end
if ngx.ctx.message ~= nil then
error("ctx from another request should be isolated")
end
-- 设置自己的值
ngx.ctx.request_id = 2
ngx.ctx.message = "request2_data"
if ngx.ctx.request_id ~= 2 then
error("request_id should be 2")
end
if ngx.ctx.message ~= "request2_data" then
error("message should be 'request2_data'")
end
`)
assert.NoError(t, err)
coro2.Close()
}
// TestNgxCtxGoAPIAccess 测试 Go 层 API 访问
func TestNgxCtxGoAPIAccess(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 通过 Go 层 API 设置值
api := coro.GetNgxVarAPI()
require.NotNil(t, api)
api.SetVariable("go_key", "go_value")
// 验证 Lua 层可以读取,且 Lua 层设置的值 Go 层可见
// 注意:在一个脚本中完成所有操作,因为协程执行后变为 dead 状态
err = coro.Execute(`
-- 验证 Go 层设置的值
if ngx.var.go_key ~= "go_value" then
error("value from Go layer should be accessible in Lua")
end
-- 从 Lua 层设置值
ngx.var.lua_key = "lua_value"
`)
assert.NoError(t, err)
// 验证 Lua 层设置的值 Go 层可见
val, ok := api.GetVariable("lua_key")
assert.True(t, ok)
assert.Equal(t, "lua_value", val)
}
// TestNgxCtxScheduleUnsafeAPI 测试调度器上下文中的不安全 API
func TestNgxCtxScheduleUnsafeAPI(t *testing.T) {
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
// 为 Scheduler LState 创建不安全的 ctx API
L := glua.NewState()
defer L.Close()
ngx := L.NewTable()
L.SetGlobal("ngx", ngx)
// 注册调度器不安全的 ctx API
RegisterSchedulerUnsafeCtxAPI(L, ngx)
// 尝试访问 ctx 应该返回错误
err = L.DoString(`
local ok, msg = pcall(function()
ngx.ctx.key = "value"
end)
if ok then
error("writing to ngx.ctx in scheduler should fail")
end
if not string.match(msg, "not available in timer callback") then
error("wrong error message: " .. msg)
end
`)
assert.NoError(t, err)
// 尝试读取 ctx 也应该返回错误
err = L.DoString(`
local ok, msg = pcall(function()
local x = ngx.ctx.key
end)
if ok then
error("reading from ngx.ctx in scheduler should fail")
end
`)
assert.NoError(t, err)
}
// TestNgxCtxTableAPI 测试 table API 操作
func TestNgxCtxTableAPI(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试 table 操作函数
err = coro.Execute(`
-- 测试 pairs 遍历
ngx.ctx.items = {a = 1, b = 2, c = 3}
local count = 0
for k, v in pairs(ngx.ctx.items) do
count = count + 1
end
if count ~= 3 then
error("items table should have 3 elements")
end
-- 测试 table.insert
ngx.ctx.list = {}
table.insert(ngx.ctx.list, 1)
table.insert(ngx.ctx.list, 2)
table.insert(ngx.ctx.list, 3)
if ngx.ctx.list[1] ~= 1 or ngx.ctx.list[2] ~= 2 or ngx.ctx.list[3] ~= 3 then
error("table.insert failed")
end
-- 测试 table.remove
table.remove(ngx.ctx.list, 2)
if #ngx.ctx.list ~= 2 or ngx.ctx.list[1] ~= 1 or ngx.ctx.list[2] ~= 3 then
error("table.remove failed")
end
-- 测试 table.concat
ngx.ctx.strlist = {"hello", "world", "test"}
local joined = table.concat(ngx.ctx.strlist, ", ")
if joined ~= "hello, world, test" then
error("table.concat failed: " .. joined)
end
`)
assert.NoError(t, err)
}
// TestNgxCtxLargeValues 测试大值存储
func TestNgxCtxLargeValues(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试大字符串和大 table在一个脚本中完成所有操作
largeString := string(make([]byte, 10000)) // 10KB 字符串
err = coro.Execute(`
-- 测试大字符串
ngx.ctx.large = "` + largeString + `"
local val = ngx.ctx.large
if type(val) ~= "string" then
error("large value should be string")
end
-- 测试大 table
ngx.ctx.bigtable = {}
for i = 1, 1000 do
ngx.ctx.bigtable[i] = i * 2
end
if #ngx.ctx.bigtable ~= 1000 then
error("bigtable should have 1000 elements")
end
`)
assert.NoError(t, err)
}
// TestNgxCtxTypeCoercion 测试类型转换
func TestNgxCtxTypeCoercion(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
// 测试数字字符串自动转换
err = coro.Execute(`
ngx.ctx.num = 42
ngx.ctx.str = "123"
-- 数字加字符串
local result = ngx.ctx.num + tonumber(ngx.ctx.str)
if result ~= 165 then
error("type coercion failed: " .. tostring(result))
end
-- 字符串连接
local concatenated = "value: " .. ngx.ctx.num
if concatenated ~= "value: 42" then
error("string concatenation failed: " .. concatenated)
end
`)
assert.NoError(t, err)
}
// TestNgxCtxBooleanLogic 测试布尔逻辑
func TestNgxCtxBooleanLogic(t *testing.T) {
var req fasthttp.Request
req.Header.SetMethod("GET")
req.Header.SetRequestURI("/test")
ctx := &fasthttp.RequestCtx{}
ctx.Init(&req, nil, nil)
engine, err := NewEngine(DefaultConfig())
require.NoError(t, err)
defer engine.Close()
coro, err := engine.NewCoroutine(ctx)
require.NoError(t, err)
defer coro.Close()
err = coro.SetupSandbox()
require.NoError(t, err)
err = coro.Execute(`
ngx.ctx.a = true
ngx.ctx.b = false
-- and 操作
if (ngx.ctx.a and ngx.ctx.b) ~= false then
error("a and b should be false")
end
-- or 操作
if (ngx.ctx.a or ngx.ctx.b) ~= true then
error("a or b should be true")
end
-- not 操作
if (not ngx.ctx.a) ~= false then
error("not a should be false")
end
`)
assert.NoError(t, err)
}