Files
kong--kong/spec/helpers/perf/utils.lua
T
2026-07-13 12:32:21 +08:00

251 lines
6.9 KiB
Lua

local ngx_pipe = require("ngx.pipe")
local ffi = require("ffi")
local cjson_safe = require("cjson.safe")
local logger = require("spec.helpers.perf.logger")
local log = logger.new_logger("[controller]")
local DISABLE_EXEC_OUTPUT = os.getenv("PERF_TEST_DISABLE_EXEC_OUTPUT") or false
string.startswith = function(s, start) -- luacheck: ignore
return s and start and start ~= "" and s:sub(1, #start) == start
end
string.endswith = function(s, e) -- luacheck: ignore
return s and e and e ~= "" and s:sub(#s-#e+1, #s) == e
end
--- Spawns a child process and get its exit code and outputs
-- @param opts.stdin string the stdin buffer
-- @param opts.logger function(lvl, _, line) stdout+stderr writer; if not defined, whole
-- stdout and stderr is returned
-- @param opts.stop_signal function return true to abort execution
-- @return stdout+stderr string, err|nil
local function execute(cmd, opts)
local log_output = opts and opts.logger
-- skip if PERF_TEST_DISABLE_EXEC_OUTPUT is set
if not DISABLE_EXEC_OUTPUT then
-- fallback to default logger if not defined
log_output = log_output or log.debug
log_output("[exec]: ", cmd)
end
local proc, err = ngx_pipe.spawn(cmd, {
merge_stderr = true,
})
if not proc then
return "", "failed to start process: " .. err
end
-- set stdout/stderr read timeout to 1s for faster noticing process exit
-- proc:set_timeouts(write_timeout?, stdout_read_timeout?, stderr_read_timeout?, wait_timeout?)
proc:set_timeouts(nil, 1000, 1000, nil)
if opts and opts.stdin then
proc:write(opts.stdin)
end
proc:shutdown("stdin")
local ret = {}
while true do
-- is it alive?
local ok = proc:kill(0)
if not ok then
break
end
local l, err = proc:stdout_read_line()
if l then
if log_output then
log_output(l)
end
-- always store output
table.insert(ret, l)
end
if err == "closed" then
break
end
local sig = opts and opts.stop_signal and opts.stop_signal()
if sig then
proc:kill(sig)
break
end
end
local ok, msg, code = proc:wait()
ok = ok and code == 0
ret = table.concat(ret, "\n")
if ok then
return ret
end
return ret, ("process exited with code %s: %s"):format(code, msg)
end
--- Execute a command and return until pattern is found in its output
-- @function wait_output
-- @param cmd string the command the execute
-- @param pattern string the pattern to find in stdout and stderr
-- @param timeout number time in seconds to wait for the pattern
-- @return bool whether the pattern is found
local function wait_output(cmd, pattern, timeout)
timeout = timeout or 5
local found
local co = coroutine.create(function()
while not found do
local line = coroutine.yield("yield")
if line:match(pattern) then
found = true
end
end
end)
-- start
coroutine.resume(co)
-- don't kill it, it me finish by itself
ngx.thread.spawn(function()
execute(cmd, {
logger = function(line)
return coroutine.running(co) and coroutine.resume(co, line)
end,
stop_signal = function() if found then return 9 end end,
})
end)
ngx.update_time()
local s = ngx.now()
while not found and ngx.now() - s <= timeout do
ngx.update_time()
ngx.sleep(0.1)
end
return found
end
ffi.cdef [[
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);
]]
--- Set an environment variable
-- @function setenv
-- @param env (string) name of the environment variable
-- @param value the value to set
-- @return true on success, false otherwise
local function setenv(env, value)
return ffi.C.setenv(env, value, 1) == 0
end
--- Unset an environment variable
-- @function setenv
-- @param env (string) name of the environment variable
-- @return true on success, false otherwise
local function unsetenv(env)
return ffi.C.unsetenv(env) == 0
end
local handler = require("busted.outputHandlers.base")()
local current_test_element
local function register_busted_hook()
local busted = require("busted")
handler.testStart = function(element, parent)
current_test_element = element
end
busted.subscribe({'test', 'start'}, handler.testStart)
end
local function get_test_descriptor(sanitized, element_override)
local elem = current_test_element or element_override
if elem then
local msg = handler.getFullName(elem)
local common_prefix = "perf test for Kong "
if msg:startswith(common_prefix) then
msg = msg:sub(#common_prefix+1)
end
if sanitized then
msg = msg:gsub("[:/]", "#"):gsub("[ ,]", "_"):gsub("__", "_")
end
return msg
end
end
local function get_test_output_filename()
return get_test_descriptor(true)
end
local function parse_docker_image_labels(docker_inspect_output)
local m, err = cjson_safe.decode(docker_inspect_output)
if err then
return nil, err
end
local labels = m[1].Config.Labels or {}
labels.version = labels["org.opencontainers.image.version"] or "unknown_version"
labels.revision = labels["org.opencontainers.image.revision"] or "unknown_revision"
labels.created = labels["org.opencontainers.image.created"] or "unknown_created"
return labels
end
local original_lua_package_paths = package.path
local function add_lua_package_paths(d)
d = d or "."
local pp = d .. "/?.lua;" ..
d .. "/?/init.lua;"
local pl_dir = require("pl.dir")
local pl_path = require("pl.path")
if pl_path.isdir(d .. "/plugins-ee") then
for _, p in ipairs(pl_dir.getdirectories(d .. "/plugins-ee")) do
pp = pp.. p .. "/?.lua;"..
p .. "/?/init.lua;"
end
end
package.path = pp .. ";" .. original_lua_package_paths
end
local function restore_lua_package_paths()
package.path = original_lua_package_paths
end
-- clear certain packages to allow spec.helpers to be re-imported
-- those modules are only needed to run migrations in the "controller"
-- and won't affect kong instances performing tests
local function clear_loaded_package()
for _, p in ipairs({
"spec.helpers", "kong.cluster_events",
"kong.global", "kong.constants",
"kong.cache", "kong.db", "kong.plugins", "kong.pdk", "kong.enterprise_edition.pdk",
}) do
package.loaded[p] = nil
end
end
local function print_and_save(s, path)
local shell = require "resty.shell"
shell.run("mkdir -p output", nil, 0)
print(s)
local f = io.open(path or "output/result.txt", "a")
f:write(s)
f:write("\n")
f:close()
end
return {
execute = execute,
wait_output = wait_output,
setenv = setenv,
unsetenv = unsetenv,
register_busted_hook = register_busted_hook,
get_test_descriptor = get_test_descriptor,
get_test_output_filename = get_test_output_filename,
parse_docker_image_labels = parse_docker_image_labels,
add_lua_package_paths = add_lua_package_paths,
restore_lua_package_paths = restore_lua_package_paths,
clear_loaded_package = clear_loaded_package,
print_and_save = print_and_save,
}