252 lines
9.0 KiB
Lua
252 lines
9.0 KiB
Lua
-- Unit tests for mod_debug_traceback.lua
|
|
-- Run with busted from resources/prosody-plugins/:
|
|
-- busted spec/lua/
|
|
--
|
|
-- No Prosody installation required — all dependencies are stubbed.
|
|
-- The module exposes a global `dump_traceback` function that is called directly.
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Package preloads (must be registered before any dofile() call)
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
package.preload['util.debug'] = function()
|
|
return { traceback = function() return "fake traceback\nframe 1\nframe 2" end }
|
|
end
|
|
|
|
package.preload['util.pposix'] = function()
|
|
return { getpid = function() return 99 end }
|
|
end
|
|
|
|
-- Minimal interpolation stub: substitutes {key} and {key.subkey} tokens.
|
|
-- Filters (yyyymmdd, hhmmss) are invoked when the key matches.
|
|
package.preload['util.interpolation'] = function()
|
|
return {
|
|
new = function(_, _, filters)
|
|
return function(template, vars)
|
|
return (template:gsub('{([^}]+)}', function(key)
|
|
-- Try filters first (yyyymmdd, hhmmss, ...)
|
|
if filters and filters[key] and vars.time then
|
|
return tostring(filters[key](vars.time))
|
|
end
|
|
-- Nested key lookup (e.g. paths.data)
|
|
local val = vars
|
|
for part in key:gmatch('[^%.]+') do
|
|
if type(val) == 'table' then val = val[part]
|
|
else val = nil; break end
|
|
end
|
|
return val ~= nil and tostring(val) or ('{' .. key .. '}')
|
|
end))
|
|
end
|
|
end,
|
|
}
|
|
end
|
|
|
|
-- util.signal is required when mod_posix lacks signal_events support
|
|
package.preload['util.signal'] = function()
|
|
return { signal = function() end }
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Prosody global required by get_filename (prosody.paths.data)
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
_G.prosody = { paths = { data = "/tmp/prosody-test" } }
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Helper: load module with given option overrides
|
|
--
|
|
-- Returns: dump_traceback fn, log_calls list, fired_events list
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
local function load_module(options)
|
|
options = options or {}
|
|
local log_calls = {}
|
|
local fired_events = {}
|
|
|
|
local mod = {
|
|
set_global = function() end,
|
|
get_option_string = function(_, key, default)
|
|
local v = options[key]
|
|
return v ~= nil and v or default
|
|
end,
|
|
log = function(_, level, fmt, ...)
|
|
local ok, msg = pcall(string.format, fmt, ...)
|
|
table.insert(log_calls, { level = level, msg = ok and msg or fmt })
|
|
end,
|
|
fire_event = function(_, name, data)
|
|
table.insert(fired_events, { name = name, data = data })
|
|
end,
|
|
depends = function() return {} end, -- no features.signal_events → util.signal path
|
|
hook = function() end,
|
|
wrap_event = function() end,
|
|
}
|
|
|
|
_G.module = mod
|
|
_G.dump_traceback = nil
|
|
|
|
local ok, err = pcall(dofile, "mod_debug_traceback.lua")
|
|
assert(ok, "module load failed: " .. tostring(err))
|
|
|
|
return _G.dump_traceback, log_calls, fired_events
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- io.open helpers
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
-- Replace io.open for the duration of fn(); collect written content per path.
|
|
local function with_mock_io(files_written, fn)
|
|
local orig = io.open
|
|
io.open = function(path, _mode)
|
|
local buf = {}
|
|
files_written[path] = buf
|
|
return {
|
|
write = function(_, s) buf[#buf + 1] = s end,
|
|
close = function() end,
|
|
}
|
|
end
|
|
fn()
|
|
io.open = orig
|
|
end
|
|
|
|
-- Replace io.open with one that always fails.
|
|
local function with_failing_io(fn)
|
|
local orig = io.open
|
|
io.open = function() return nil, "permission denied" end
|
|
fn()
|
|
io.open = orig
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------------
|
|
-- Tests
|
|
-- ---------------------------------------------------------------------------
|
|
|
|
describe("mod_debug_traceback", function()
|
|
|
|
-- -----------------------------------------------------------------------
|
|
describe("dump_traceback()", function()
|
|
|
|
it("writes traceback content to file", function()
|
|
local dump = load_module()
|
|
local files = {}
|
|
with_mock_io(files, function() dump() end)
|
|
|
|
local path = next(files)
|
|
local content = table.concat(files[path])
|
|
assert.is_truthy(content:find("fake traceback", 1, true))
|
|
end)
|
|
|
|
it("surrounds traceback with header and footer lines", function()
|
|
local dump = load_module()
|
|
local files = {}
|
|
with_mock_io(files, function() dump() end)
|
|
|
|
local content = table.concat(files[next(files)])
|
|
assert.is_truthy(content:find("-- Traceback generated at", 1, true))
|
|
assert.is_truthy(content:find("-- End of traceback --", 1, true))
|
|
end)
|
|
|
|
it("fires debug_traceback/triggered event with a traceback string", function()
|
|
local dump, _, events = load_module()
|
|
local files = {}
|
|
with_mock_io(files, function() dump() end)
|
|
|
|
assert.equal(1, #events)
|
|
assert.equal("debug_traceback/triggered", events[1].name)
|
|
assert.is_string(events[1].data.traceback)
|
|
end)
|
|
|
|
it("fires event before attempting file write (even on io failure)", function()
|
|
local dump, _, events = load_module()
|
|
with_failing_io(function() dump() end)
|
|
|
|
assert.equal(1, #events)
|
|
assert.equal("debug_traceback/triggered", events[1].name)
|
|
end)
|
|
|
|
it("logs an error when the file cannot be opened", function()
|
|
local dump, log_calls = load_module()
|
|
with_failing_io(function() dump() end)
|
|
|
|
local found = false
|
|
for _, e in ipairs(log_calls) do
|
|
if e.level == "error" and e.msg:find("permission denied", 1, true) then
|
|
found = true
|
|
end
|
|
end
|
|
assert.is_true(found, "expected error log mentioning 'permission denied'")
|
|
end)
|
|
|
|
it("does not write a file when io.open fails", function()
|
|
local dump = load_module()
|
|
local files = {}
|
|
with_failing_io(function() dump() end)
|
|
-- files table should remain empty (no successful open)
|
|
assert.is_nil(next(files))
|
|
end)
|
|
|
|
it("default filename contains pid and starts with data path", function()
|
|
local dump = load_module()
|
|
local files = {}
|
|
with_mock_io(files, function() dump() end)
|
|
|
|
local path = next(files)
|
|
assert.is_truthy(path:find("/tmp/prosody-test", 1, true))
|
|
assert.is_truthy(path:find("99", 1, true)) -- pid = 99
|
|
end)
|
|
|
|
it("default filename embeds count (starting at 0)", function()
|
|
local dump = load_module()
|
|
local files = {}
|
|
with_mock_io(files, function() dump() end)
|
|
|
|
local path = next(files)
|
|
-- default template: traceback-{pid}-{count}.log → count=0 on first call
|
|
assert.is_truthy(path:find("%-0%.log$"))
|
|
end)
|
|
|
|
it("increments count between calls producing distinct filenames", function()
|
|
local dump = load_module()
|
|
local paths = {}
|
|
local orig = io.open
|
|
io.open = function(path, _)
|
|
paths[#paths + 1] = path
|
|
return { write = function() end, close = function() end }
|
|
end
|
|
dump()
|
|
dump()
|
|
io.open = orig
|
|
|
|
assert.equal(2, #paths)
|
|
assert.not_equal(paths[1], paths[2])
|
|
end)
|
|
|
|
it("uses custom signal name in info log message", function()
|
|
local dump, log_calls = load_module({ debug_traceback_signal = "SIGUSR2" })
|
|
local files = {}
|
|
with_mock_io(files, function() dump() end)
|
|
|
|
local found = false
|
|
for _, e in ipairs(log_calls) do
|
|
if e.level == "info" and e.msg:find("SIGUSR2", 1, true) then
|
|
found = true
|
|
end
|
|
end
|
|
assert.is_true(found, "expected info log mentioning 'SIGUSR2'")
|
|
end)
|
|
|
|
it("uses custom filename template from config", function()
|
|
local dump = load_module({
|
|
debug_traceback_filename = "/custom/tb-{pid}.log",
|
|
})
|
|
local files = {}
|
|
with_mock_io(files, function() dump() end)
|
|
|
|
local path = next(files)
|
|
assert.equal("/custom/tb-99.log", path)
|
|
end)
|
|
|
|
end) -- dump_traceback()
|
|
|
|
end)
|