prosody/tools/modtrace.lua

159 lines
4 KiB
Lua

-- Trace module calls and method calls on created objects
--
-- Very rough and for debugging purposes only. It makes many
-- assumptions and there are many ways it could fail.
--
-- Example use:
--
-- local dbuffer = require "tools.modtrace".trace("util.dbuffer");
--
local t_pack = require "util.table".pack;
local serialize = require "util.serialization".serialize;
local unpack = table.unpack;
local set = require "util.set";
local serialize_cfg = {
preset = "oneline";
freeze = true;
fatal = false;
fallback = function (v) return "<"..tostring(v)..">" end;
};
local function stringify_value(v)
if type(v) == "string" and #v > 20 then
return ("<string(%d)>"):format(#v);
elseif type(v) == "function" then
return tostring(v);
end
return serialize(v, serialize_cfg);
end
local function stringify_params(...)
local n = select("#", ...);
local r = {};
for i = 1, n do
table.insert(r, stringify_value((select(i, ...))));
end
return table.concat(r, ", ");
end
local function stringify_result(ret)
local r = {};
for i = 1, ret.n do
table.insert(r, stringify_value(ret[i]));
end
return table.concat(r, ", ");
end
local function stringify_call(method_name, ...)
return ("%s(%s)"):format(method_name, stringify_params(...));
end
local function wrap_method(original_obj, original_method, method_name)
method_name = ("<%s>:%s"):format(getmetatable(original_obj).__name or "object", method_name);
return function (new_obj_self, ...)
local opts = new_obj_self._modtrace_opts;
local f = opts.output or io.stderr;
f:write(stringify_call(method_name, ...));
local ret = t_pack(original_method(original_obj, ...));
if ret.n > 0 then
f:write(" = ", stringify_result(ret), "\n");
else
f:write("\n");
end
return unpack(ret, 1, ret.n);
end;
end
local function wrap_function(original_function, function_name, opts)
local f = opts.output or io.stderr;
return function (...)
f:write(stringify_call(function_name, ...));
local ret = t_pack(original_function(...));
if ret.n > 0 then
f:write(" = ", stringify_result(ret), "\n");
else
f:write("\n");
end
return unpack(ret, 1, ret.n);
end;
end
local function wrap_metamethod(name, method)
if name == "__index" then
return function (new_obj, k)
local original_method;
if type(method) == "table" then
original_method = new_obj._modtrace_original_obj[k];
else
original_method = method(new_obj._modtrace_original_obj, k);
end
if original_method == nil then
return nil;
end
return wrap_method(new_obj._modtrace_original_obj, original_method, k);
end;
end
return function (new_obj, ...)
return method(new_obj._modtrace_original_obj, ...);
end;
end
local function wrap_mt(original_mt)
local new_mt = {};
for k, v in pairs(original_mt) do
new_mt[k] = wrap_metamethod(k, v);
end
return new_mt;
end
local function wrap_obj(original_obj, opts)
local new_mt = wrap_mt(getmetatable(original_obj));
return setmetatable({_modtrace_original_obj = original_obj, _modtrace_opts = opts}, new_mt);
end
local function wrap_new(original_new, function_name, opts)
local f = opts.output or io.stderr;
return function (...)
f:write(stringify_call(function_name, ...));
local ret = t_pack(original_new(...));
local obj = ret[1];
if ret.n == 1 and type(ret[1]) == "table" then
f:write(" = <", getmetatable(ret[1]).__name or "object", ">", "\n");
elseif ret.n > 0 then
f:write(" = ", stringify_result(ret), "\n");
else
f:write("\n");
end
if obj then
ret[1] = wrap_obj(obj, opts);
end
return unpack(ret, 1, ret.n);
end;
end
local function trace(module, opts)
if type(module) == "string" then
module = require(module);
end
opts = opts or {};
local new_methods = set.new(opts.new_methods or {"new"});
local fake_module = setmetatable({}, {
__index = function (_, k)
if new_methods:contains(k) then
return wrap_new(module[k], k, opts);
else
return wrap_function(module[k], k, opts);
end
end;
});
return fake_module;
end
return {
wrap = trace;
trace = trace;
}