test(lua): 添加 api_shared_dict 和 engine 测试覆盖
- api_shared_dict_test.go: 测试 shared_dict API 功能 - engine_test.go: 测试 Lua 引擎初始化和执行 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
28be9e7e66
commit
7ef16f2be1
527
internal/lua/api_shared_dict_test.go
Normal file
527
internal/lua/api_shared_dict_test.go
Normal file
@ -0,0 +1,527 @@
|
|||||||
|
// Package lua 提供 ngx.shared API 扩展测试,覆盖 Lua API 函数的更多场景
|
||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSharedDictLuaReplace 测试 dict:replace
|
||||||
|
func TestSharedDictLuaReplace(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("testdict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
// 测试 replace 存在的 key
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("testdict")
|
||||||
|
|
||||||
|
-- 先设置一个值
|
||||||
|
dict:set("mykey", "old_value")
|
||||||
|
|
||||||
|
-- replace 存在的 key 应该成功
|
||||||
|
local ok, err = dict:replace("mykey", "new_value")
|
||||||
|
assert(ok == true, "replace existing key should succeed: " .. tostring(err))
|
||||||
|
|
||||||
|
-- 验证值已更新
|
||||||
|
local val, _ = dict:get("mykey")
|
||||||
|
assert(val == "new_value", "value should be updated")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 测试 replace 不存在的 key
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("testdict")
|
||||||
|
|
||||||
|
-- replace 不存在的 key 应该失败
|
||||||
|
local ok, err = dict:replace("nonexistent", "value")
|
||||||
|
assert(ok == false, "replace nonexistent key should fail")
|
||||||
|
assert(err == "not found", "error should be 'not found', got: " .. tostring(err))
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaReplaceWithTTL 测试 dict:replace 带 TTL
|
||||||
|
func TestSharedDictLuaReplaceWithTTL(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("ttldict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("ttldict")
|
||||||
|
dict:set("ttlkey", "original")
|
||||||
|
|
||||||
|
-- replace 带 TTL(0.1 秒)
|
||||||
|
local ok, err = dict:replace("ttlkey", "replaced", 0.1)
|
||||||
|
assert(ok == true, "replace with TTL should succeed: " .. tostring(err))
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 等待过期
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
|
||||||
|
// 验证过期
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("ttldict")
|
||||||
|
local val, err = dict:get("ttlkey")
|
||||||
|
assert(val == nil, "expired key should return nil")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaIndexAccess 测试 dict["key"] 索引访问
|
||||||
|
func TestSharedDictLuaIndexAccess(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("idxdict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("idxdict")
|
||||||
|
|
||||||
|
-- 通过方法设置值
|
||||||
|
dict:set("foo", "bar")
|
||||||
|
|
||||||
|
-- 通过索引方式读取
|
||||||
|
local val = dict["foo"]
|
||||||
|
assert(val == "bar", "index access should work")
|
||||||
|
|
||||||
|
-- 通过 __newindex 设置值
|
||||||
|
dict["newkey"] = "newvalue"
|
||||||
|
local v = dict:get("newkey")
|
||||||
|
assert(v == "newvalue", "__newindex should work")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaIndexNotFound 测试索引访问不存在的 key
|
||||||
|
func TestSharedDictLuaIndexNotFound(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("idxdict2", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("idxdict2")
|
||||||
|
|
||||||
|
-- 读取不存在的 key 应该返回 nil
|
||||||
|
local val = dict["nonexistent"]
|
||||||
|
assert(val == nil, "nonexistent key should return nil")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaIndexMethodAccess 测试索引访问方法名
|
||||||
|
func TestSharedDictLuaIndexMethodAccess(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("metdict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("metdict")
|
||||||
|
|
||||||
|
-- 通过索引访问方法
|
||||||
|
local getFn = dict["get"]
|
||||||
|
assert(type(getFn) == "function", "get should be a function")
|
||||||
|
|
||||||
|
local setFn = dict["set"]
|
||||||
|
assert(type(setFn) == "function", "set should be a function")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaFlushExpired 测试 dict:flush_expired
|
||||||
|
func TestSharedDictLuaFlushExpired(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("flushdict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
// 设置一些带 TTL 的条目和一个永久的
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("flushdict")
|
||||||
|
dict:set("exp1", "v1", 0.05)
|
||||||
|
dict:set("exp2", "v2", 0.05)
|
||||||
|
dict:set("perm", "permanent")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 立即清除应该没有过期条目
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("flushdict")
|
||||||
|
local count = dict:flush_expired()
|
||||||
|
assert(count == 0, "should have 0 expired immediately")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 等待过期
|
||||||
|
time.Sleep(100 * time.Millisecond)
|
||||||
|
|
||||||
|
// 现在应该有 2 个过期条目
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("flushdict")
|
||||||
|
local count = dict:flush_expired()
|
||||||
|
assert(count == 2, "should have 2 expired after wait")
|
||||||
|
|
||||||
|
-- 永久条目应该还在
|
||||||
|
local val, _ = dict:get("perm")
|
||||||
|
assert(val == "permanent", "permanent key should still exist")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaGetKeys 测试 dict:get_keys
|
||||||
|
func TestSharedDictLuaGetKeys(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("keysdict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("keysdict")
|
||||||
|
dict:set("k1", "v1")
|
||||||
|
dict:set("k2", "v2")
|
||||||
|
dict:set("k3", "v3")
|
||||||
|
|
||||||
|
local keys = dict:get_keys()
|
||||||
|
assert(type(keys) == "table", "get_keys should return a table")
|
||||||
|
-- 当前实现返回空表
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaSize 测试 dict:size
|
||||||
|
func TestSharedDictLuaSize(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("sizedict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("sizedict")
|
||||||
|
|
||||||
|
-- 初始为 0
|
||||||
|
assert(dict:size() == 0, "initial size should be 0")
|
||||||
|
|
||||||
|
dict:set("a", "1")
|
||||||
|
dict:set("b", "2")
|
||||||
|
dict:set("c", "3")
|
||||||
|
|
||||||
|
assert(dict:size() >= 3, "size should be at least 3")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaFreeSpace 测试 dict:free_space
|
||||||
|
func TestSharedDictLuaFreeSpace(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("freedict", 50)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("freedict")
|
||||||
|
|
||||||
|
-- 初始 free_space 应该等于 maxItems
|
||||||
|
local free = dict:free_space()
|
||||||
|
assert(free == 50, "initial free_space should be 50, got: " .. tostring(free))
|
||||||
|
|
||||||
|
-- 添加条目后 free_space 减少
|
||||||
|
dict:set("key1", "value1")
|
||||||
|
local free2 = dict:free_space()
|
||||||
|
assert(free2 == 49, "free_space should be 49 after 1 item, got: " .. tostring(free2))
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaFlushAll 测试 dict:flush_all
|
||||||
|
func TestSharedDictLuaFlushAll(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("flushalldict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("flushalldict")
|
||||||
|
dict:set("a", "1")
|
||||||
|
dict:set("b", "2")
|
||||||
|
|
||||||
|
assert(dict:size() >= 2)
|
||||||
|
|
||||||
|
-- flush_all 清空所有
|
||||||
|
dict:flush_all()
|
||||||
|
assert(dict:size() == 0, "size should be 0 after flush_all")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaDictNotFound 测试请求不存在的 shared dict
|
||||||
|
func TestSharedDictLuaDictNotFound(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict, err = ngx.shared.DICT("nonexistent_dict")
|
||||||
|
assert(dict == nil, "nonexistent dict should return nil")
|
||||||
|
assert(err ~= nil, "should return error message")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaToString 测试 dict 的 tostring
|
||||||
|
func TestSharedDictLuaToString(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("tostringdict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("tostringdict")
|
||||||
|
local s = tostring(dict)
|
||||||
|
assert(type(s) == "string", "tostring should return a string")
|
||||||
|
assert(string.match(s, "ngx.shared.dict"), "should contain 'ngx.shared.dict'")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaIncrNonNumber 测试 incr 对非数值
|
||||||
|
func TestSharedDictLuaIncrNonNumber(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("incrdict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
// 设置一个非数值
|
||||||
|
dict := engine.SharedDictManager().GetDict("incrdict")
|
||||||
|
dict.Set("notnum", "hello", 0)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("incrdict")
|
||||||
|
local val, err = dict:incr("notnum", 1)
|
||||||
|
-- 非数值 incr 返回 nil
|
||||||
|
assert(val == nil, "incr non-number should return nil")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaAddMultipleKeys 测试批量添加
|
||||||
|
func TestSharedDictLuaAddMultipleKeys(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("multiadd", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("multiadd")
|
||||||
|
|
||||||
|
-- 批量添加
|
||||||
|
for i = 1, 10 do
|
||||||
|
local ok, err = dict:add("key_" .. i, "val_" .. i)
|
||||||
|
assert(ok == true, "add should succeed: " .. tostring(err))
|
||||||
|
end
|
||||||
|
|
||||||
|
-- 验证所有 key 都存在
|
||||||
|
for i = 1, 10 do
|
||||||
|
local val, _ = dict:get("key_" .. i)
|
||||||
|
assert(val == "val_" .. i, "key_" .. i .. " should have correct value")
|
||||||
|
end
|
||||||
|
|
||||||
|
assert(dict:size() == 10, "size should be 10")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictEngineAPI 测试通过 Engine API 创建共享字典
|
||||||
|
func TestSharedDictEngineAPI(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 通过 Engine 的 CreateSharedDict 方法创建
|
||||||
|
dict := engine.CreateSharedDict("enginedict", 50)
|
||||||
|
require.NotNil(t, dict)
|
||||||
|
|
||||||
|
// 验证可以通过 SharedDictManager 获取
|
||||||
|
dict2 := engine.SharedDictManager().GetDict("enginedict")
|
||||||
|
assert.Equal(t, dict, dict2, "should return same dict")
|
||||||
|
|
||||||
|
// 再次调用 CreateSharedDict 应返回已存在的
|
||||||
|
dict3 := engine.CreateSharedDict("enginedict", 100)
|
||||||
|
assert.Equal(t, dict, dict3, "should return existing dict regardless of maxItems")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaSetWithTTL 测试 set 带 TTL
|
||||||
|
func TestSharedDictLuaSetWithTTL(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("setttldict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("setttldict")
|
||||||
|
|
||||||
|
-- set 带 TTL(0.1 秒)
|
||||||
|
local ok, err = dict:set("ttlkey", "temp_value", 0.1)
|
||||||
|
assert(ok == true, "set with TTL should succeed: " .. tostring(err))
|
||||||
|
|
||||||
|
-- 立即获取应该成功
|
||||||
|
local val, exp = dict:get("ttlkey")
|
||||||
|
assert(val == "temp_value", "value should be correct")
|
||||||
|
assert(exp == 0 or exp == nil, "should not be expired yet")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 等待过期
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("setttldict")
|
||||||
|
local val, err = dict:get("ttlkey")
|
||||||
|
assert(val == nil, "expired key should return nil")
|
||||||
|
assert(err == "expired", "error should be 'expired', got: " .. tostring(err))
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictLuaAddWithTTL 测试 add 带 TTL
|
||||||
|
func TestSharedDictLuaAddWithTTL(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("addttldict", 100)
|
||||||
|
|
||||||
|
L := engine.L
|
||||||
|
ngx := L.NewTable()
|
||||||
|
L.SetGlobal("ngx", ngx)
|
||||||
|
RegisterSharedDictAPI(L, engine.SharedDictManager(), ngx)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("addttldict")
|
||||||
|
|
||||||
|
-- add 带 TTL
|
||||||
|
local ok, err = dict:add("addkey", "temp", 0.1)
|
||||||
|
assert(ok == true, "add with TTL should succeed: " .. tostring(err))
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
time.Sleep(150 * time.Millisecond)
|
||||||
|
|
||||||
|
err = L.DoString(`
|
||||||
|
local dict = ngx.shared.DICT("addttldict")
|
||||||
|
local val, err = dict:get("addkey")
|
||||||
|
assert(val == nil, "expired key should return nil")
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSharedDictClose 测试 SharedDictManager.Close
|
||||||
|
func TestSharedDictClose(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
_ = engine.CreateSharedDict("closedict", 100)
|
||||||
|
|
||||||
|
// Close 引擎会清理 sharedDictManager
|
||||||
|
engine.Close()
|
||||||
|
|
||||||
|
// Close 后不应该 panic(即使再次 Close)
|
||||||
|
engine.Close()
|
||||||
|
}
|
||||||
461
internal/lua/engine_test.go
Normal file
461
internal/lua/engine_test.go
Normal file
@ -0,0 +1,461 @@
|
|||||||
|
// Package lua 提供 LuaEngine 测试,覆盖协程创建和管理、调度器、回调队列
|
||||||
|
package lua
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/valyala/fasthttp"
|
||||||
|
glua "github.com/yuin/gopher-lua"
|
||||||
|
"github.com/yuin/gopher-lua/parse"
|
||||||
|
)
|
||||||
|
|
||||||
|
// engineCodeToProtoForTest 编译 Lua 代码为 FunctionProto(测试辅助函数)
|
||||||
|
func engineCodeToProtoForTest(src string) (*glua.FunctionProto, error) {
|
||||||
|
chunk, err := parse.Parse(strings.NewReader(src), "<test>")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return glua.Compile(chunk, "<test>")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewEngineNilConfig 测试 NewEngine 使用 nil config 时使用默认配置
|
||||||
|
func TestNewEngineNilConfig(t *testing.T) {
|
||||||
|
engine, err := NewEngine(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
assert.NotNil(t, engine.L)
|
||||||
|
assert.NotNil(t, engine.codeCache)
|
||||||
|
assert.NotNil(t, engine.sharedDictManager)
|
||||||
|
assert.NotNil(t, engine.timerManager)
|
||||||
|
assert.NotNil(t, engine.locationManager)
|
||||||
|
assert.NotNil(t, engine.ctx)
|
||||||
|
assert.NotNil(t, engine.cancel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineCloseMultiple 测试多次 Close 不 panic
|
||||||
|
func TestEngineCloseMultiple(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
engine.Close()
|
||||||
|
engine.Close() // 第二次不应该 panic
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineCloseScheduler 测试关闭调度器
|
||||||
|
func TestEngineCloseScheduler(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 初始化调度器
|
||||||
|
err = engine.InitSchedulerLState()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 关闭调度器
|
||||||
|
engine.CloseScheduler()
|
||||||
|
|
||||||
|
// 再次关闭不应该 panic
|
||||||
|
engine.CloseScheduler()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineNewCoroutine 测试创建协程
|
||||||
|
func TestEngineNewCoroutine(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
var req fasthttp.Request
|
||||||
|
req.Header.SetMethod("GET")
|
||||||
|
req.Header.SetRequestURI("/test")
|
||||||
|
|
||||||
|
ctx := &fasthttp.RequestCtx{}
|
||||||
|
ctx.Init(&req, nil, nil)
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, coro)
|
||||||
|
assert.NotNil(t, coro.Co)
|
||||||
|
assert.NotNil(t, coro.Engine)
|
||||||
|
assert.Equal(t, ctx, coro.RequestCtx)
|
||||||
|
|
||||||
|
coro.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineNewCoroutineNilContext 测试创建带 nil 请求上下文的协程
|
||||||
|
func TestEngineNewCoroutineNilContext(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)
|
||||||
|
assert.Nil(t, coro.RequestCtx)
|
||||||
|
|
||||||
|
coro.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineActiveCoroutines 测试活跃协程计数
|
||||||
|
func TestEngineActiveCoroutines(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 初始应该为 0
|
||||||
|
assert.Equal(t, int32(0), engine.ActiveCoroutines())
|
||||||
|
|
||||||
|
// 创建一个协程
|
||||||
|
coro, err := engine.NewCoroutine(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, int32(1), engine.ActiveCoroutines())
|
||||||
|
|
||||||
|
// 关闭协程
|
||||||
|
coro.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, int32(0), engine.ActiveCoroutines())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineCoroutinePoolWarmup 测试协程池预热
|
||||||
|
func TestEngineCoroutinePoolWarmup(t *testing.T) {
|
||||||
|
config := DefaultConfig()
|
||||||
|
config.CoroutinePoolWarmup = 10
|
||||||
|
|
||||||
|
engine, err := NewEngine(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 预热后池中应该有 10 个对象
|
||||||
|
// 直接验证 engine 创建成功即可,预热是内部实现
|
||||||
|
assert.NotNil(t, engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineStatsAfterOperations 测试引擎统计信息在操作后更新
|
||||||
|
func TestEngineStatsAfterOperations(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 创建并关闭多个协程
|
||||||
|
for range 5 {
|
||||||
|
coro, err := engine.NewCoroutine(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
coro.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := engine.Stats()
|
||||||
|
assert.Equal(t, uint64(5), stats.CoroutinesCreated)
|
||||||
|
assert.Equal(t, uint64(5), stats.CoroutinesClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineMaxCoroutinesExceeded 测试超过最大并发协程限制
|
||||||
|
func TestEngineMaxCoroutinesExceeded(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
MaxConcurrentCoroutines: 2,
|
||||||
|
MaxExecutionTime: 5 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := NewEngine(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 创建 2 个协程
|
||||||
|
coro1, err := engine.NewCoroutine(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
coro2, err := engine.NewCoroutine(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 第 3 个应该失败
|
||||||
|
coro3, err := engine.NewCoroutine(nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Nil(t, coro3)
|
||||||
|
assert.Contains(t, err.Error(), "max concurrent coroutines exceeded")
|
||||||
|
|
||||||
|
coro1.Close()
|
||||||
|
coro2.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineNewCoroutineFails 测试 NewThread 返回 nil 的情况
|
||||||
|
// 这个场景在实际中很难触发,我们验证引擎在正常情况下不会返回 nil
|
||||||
|
func TestEngineNewCoroutineSuccess(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 创建大量协程,验证稳定性
|
||||||
|
for range 100 {
|
||||||
|
coro, err := engine.NewCoroutine(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, coro.Co)
|
||||||
|
coro.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineCodeCacheAccess 测试 CodeCache 访问器
|
||||||
|
func TestEngineCodeCacheAccess(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 1 + 1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, proto)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineSharedDictManagerAccess 测试 SharedDictManager 访问器
|
||||||
|
func TestEngineSharedDictManagerAccess(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
mgr := engine.SharedDictManager()
|
||||||
|
require.NotNil(t, mgr)
|
||||||
|
|
||||||
|
// 创建共享字典
|
||||||
|
dict := engine.CreateSharedDict("test", 100)
|
||||||
|
require.NotNil(t, dict)
|
||||||
|
|
||||||
|
// 通过 manager 获取
|
||||||
|
dict2 := mgr.GetDict("test")
|
||||||
|
assert.Equal(t, dict, dict2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineTimerManagerAccess 测试 TimerManager 访问器
|
||||||
|
func TestEngineTimerManagerAccess(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
mgr := engine.TimerManager()
|
||||||
|
require.NotNil(t, mgr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineLocationManagerAccess 测试 LocationManager 访问器
|
||||||
|
func TestEngineLocationManagerAccess(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
mgr := engine.LocationManager()
|
||||||
|
require.NotNil(t, mgr)
|
||||||
|
|
||||||
|
// 注册一个 location
|
||||||
|
mgr.Register("/test", func(ctx *fasthttp.RequestCtx) {
|
||||||
|
ctx.SetStatusCode(200)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 验证已注册
|
||||||
|
_, err2 := mgr.Capture(&fasthttp.RequestCtx{}, "/test", nil)
|
||||||
|
assert.NoError(t, err2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineSchedulerLoop 测试调度器循环处理回调
|
||||||
|
func TestEngineSchedulerLoop(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 初始化调度器
|
||||||
|
err = engine.InitSchedulerLState()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 创建一个简单的回调函数并加入队列
|
||||||
|
proto, err := engineCodeToProtoForTest("return 42")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
entry := &CallbackEntry{
|
||||||
|
proto: proto,
|
||||||
|
args: []glua.LValue{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := engine.EnqueueCallback(entry)
|
||||||
|
assert.True(t, ok, "enqueue should succeed")
|
||||||
|
|
||||||
|
// 给调度器一些时间处理
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// 关闭调度器
|
||||||
|
engine.CloseScheduler()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineEnqueueCallbackFull 测试回调队列满时入队失败
|
||||||
|
func TestEngineEnqueueCallbackFull(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
err = engine.InitSchedulerLState()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
proto, err := engineCodeToProtoForTest("return 1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 填满回调队列(默认 1024 容量)
|
||||||
|
full := false
|
||||||
|
for range 1024 {
|
||||||
|
if !engine.EnqueueCallback(&CallbackEntry{proto: proto, args: []glua.LValue{}}) {
|
||||||
|
full = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在正常环境下 1024 个应该能填满队列
|
||||||
|
// 最后一个应该失败
|
||||||
|
last := engine.EnqueueCallback(&CallbackEntry{proto: proto, args: []glua.LValue{}})
|
||||||
|
// 可能为 false(队列满)或 true(如果调度器已经开始消费)
|
||||||
|
// 不强制断言,因为调度器可能在消费
|
||||||
|
_ = full
|
||||||
|
_ = last
|
||||||
|
|
||||||
|
engine.CloseScheduler()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineExecuteCallbackNilScheduler 测试 executeCallback 时 schedulerLState 为 nil
|
||||||
|
func TestEngineExecuteCallbackNilScheduler(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 不调用 InitSchedulerLState,schedulerLState 为 nil
|
||||||
|
// executeCallback 会在 schedulerLState == nil 时直接返回
|
||||||
|
engine.executeCallback(&CallbackEntry{
|
||||||
|
proto: nil,
|
||||||
|
args: []glua.LValue{},
|
||||||
|
})
|
||||||
|
// 不应 panic
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineExecuteCallbackPanicRecovery 测试 executeCallback 中 panic 的恢复
|
||||||
|
func TestEngineExecuteCallbackPanicRecovery(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
err = engine.InitSchedulerLState()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 传入 nil proto,executeCallback 内部应该不会 panic
|
||||||
|
// 因为 recover() 会捕获
|
||||||
|
engine.executeCallback(&CallbackEntry{
|
||||||
|
proto: nil,
|
||||||
|
args: []glua.LValue{},
|
||||||
|
})
|
||||||
|
|
||||||
|
engine.CloseScheduler()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineSchedulerLoopExitOnClose 测试调度器在引擎关闭时退出
|
||||||
|
func TestEngineSchedulerLoopExitOnClose(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = engine.InitSchedulerLState()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 关闭引擎(会触发 cancel 信号)
|
||||||
|
engine.Close()
|
||||||
|
|
||||||
|
// 给调度器一些时间退出
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// 再次关闭不应该 panic
|
||||||
|
engine.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineSchedulerLoopExitOnChannelClose 测试调度器在回调队列关闭时退出
|
||||||
|
func TestEngineSchedulerLoopExitOnChannelClose(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = engine.InitSchedulerLState()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 直接关闭调度器(关闭 callbackQueue channel)
|
||||||
|
engine.CloseScheduler()
|
||||||
|
|
||||||
|
// 给调度器一些时间退出
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
engine.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineCoroutineExecutionContext 测试协程的执行上下文和超时控制
|
||||||
|
func TestEngineCoroutineExecutionContext(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
MaxConcurrentCoroutines: 100,
|
||||||
|
MaxExecutionTime: 100 * time.Millisecond,
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := NewEngine(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
coro, err := engine.NewCoroutine(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// 验证执行上下文已设置
|
||||||
|
assert.NotNil(t, coro.ExecutionContext)
|
||||||
|
assert.NotNil(t, coro.Cancel)
|
||||||
|
|
||||||
|
coro.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineReleaseCoroutineNilSafety 测试 releaseCoroutine 对 nil 的安全处理
|
||||||
|
func TestEngineReleaseCoroutineNilSafety(t *testing.T) {
|
||||||
|
engine, err := NewEngine(DefaultConfig())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 释放 nil 协程不应 panic
|
||||||
|
engine.releaseCoroutine(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineCoroutinePoolReuse 测试协程池复用
|
||||||
|
func TestEngineCoroutinePoolReuse(t *testing.T) {
|
||||||
|
engine, err := NewEngine(&Config{
|
||||||
|
MaxConcurrentCoroutines: 1000,
|
||||||
|
MaxExecutionTime: 5 * time.Second,
|
||||||
|
CoroutinePoolWarmup: 5,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
// 创建并释放多次
|
||||||
|
for range 10 {
|
||||||
|
coro, err := engine.NewCoroutine(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
coro.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
stats := engine.Stats()
|
||||||
|
assert.Equal(t, uint64(10), stats.CoroutinesCreated)
|
||||||
|
assert.Equal(t, uint64(10), stats.CoroutinesClosed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEngineConfigOverride 测试配置覆盖
|
||||||
|
func TestEngineConfigOverride(t *testing.T) {
|
||||||
|
config := &Config{
|
||||||
|
MaxConcurrentCoroutines: 500,
|
||||||
|
MaxExecutionTime: 10 * time.Second,
|
||||||
|
CodeCacheSize: 2000,
|
||||||
|
CodeCacheTTL: 5 * time.Minute,
|
||||||
|
CoroutineStackSize: 64,
|
||||||
|
MinimizeStackMemory: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := NewEngine(config)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer engine.Close()
|
||||||
|
|
||||||
|
assert.Equal(t, 500, engine.maxCoroutines)
|
||||||
|
assert.Equal(t, 10*time.Second, config.MaxExecutionTime)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user