Initial commit

master
Lars Mueller 2022-07-05 14:54:52 +02:00
commit 5d2e82b128
11 changed files with 966 additions and 0 deletions

2
.luacheckrc Normal file
View File

@ -0,0 +1,2 @@
read_globals = {"minetest"}
globals = {"dbg"}

7
License.txt Normal file
View File

@ -0,0 +1,7 @@
Copyright 2022 Lars Müller
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

128
Readme.md Normal file
View File

@ -0,0 +1,128 @@
# Debug (`dbg`)
Debugging on steroids for Minetest mods
## Motivation
Lua offers very powerful introspective facilities through its `debug` library, which unfortunately almost always go unused due to their clunky APIs.
Current state-of-the-art in Minetest mod debugging appears to be print/log/chat "debugging" using serialization or `dump`,
all of which should be rendered obsolete by `dbg`.
## API
**Optionally depend on `dbg` in `mod.conf` to ensure that it is available for load-time debugging.**
### `dbg()`
Shorter alias for `dbg.dd()`.
`debug.debug()` on steroids: Starts a REPL (read-eval-print-loop); equivalent to a breakpoint. Features include:
* Full access to locals & upvalues of the calling function
* Own local table `_L` where `local` debug vars get stored
* Ability to enter expressions (rather than statements)
* Continuation, which works the same as in the Lua REPL (+ empty lines working)
* Pretty-printing using `dbg.pp`, including syntax highlighting
Enter `cont` to exit without an error. Use `err` to throw after error debugging sessions (`dbg.error`, `dbg.assert`).
Use EOF (<kbd>Ctrl + D</kbd> on Unix) to exit & shut down Minetest.
### `dbg.error(message, [level])`
Starts a debugging session at the given (optional) level, printing the error message.
### `dbg.assert(value, [message])`
Returns `value`. Starts an error debugging session if `value` is falsey. Error `message` is optional.
### `dbg.pp(...)`
Pretty-prints the given vararg using the default parameters.
If the argument list of functions is unreliable (see `dbg.getargs_reliable` below),
a question mark (`?`) will be appended to the argument list to indicate this.
### `dbg.ppp(params, ...)`
Parameterized pretty-print. Requires a custom pretty-printer parameter table `params`:
* `write = function(str, token_type)`, where `token_type` is optional and may be one of `nil`, `boolean`, `number`, `string`, `reference`, `function` or `type`
* `upvalues = true`, whether upvalues should be written
### `dbg.vars(level)`
Returns a virtual variable table of locals & upvalues `vars` for the given stacklevel that supports the following operations:
* Getting: `vars.varname`
* Setting: `vars.varname = value`
* Looping: `for varname, value in vars() do ... end`
### `dbg.locals(level)`
Returns a virtual variable table of local values at the given stack level.
Locals include upvalues.
### `dbg.upvals(func)`
`func` may be either a function or a stack level (including `nil`, which defaults to the stack level of the calling function).
Returns a virtual variable table of upvalues at the given stack level.
### `dbg.traceback(level)`
Formats a stack trace starting at `level`. Similar to Lua's builtin `debug.stacktrace`, but shortens paths and accepts no `message` to prepend.
### `dbg.stackinfo(level)`
Returns a list of `info` by repeatedly calling `debug.getinfo` starting with `level` and working down the stack.
### `dbg.getvararg(level)`
**Only available on LuaJIT; on PUC Lua 5.1, `dbg.getvararg` will be `nil`.**
Returns the vararg at the given stack level.
### `dbg.getargs(func)`
**Function parameter list detection doesn't work properly on PUC Lua 5.1; unused params are lost and varargs are turned into `arg`.**
Use `dbg.getargs_reliable` (boolean) to check for reliability.
Returns a table containing the argument names of `func` in string form
(example: `{"x", "y", "z", "..."}` for `function(x, y, z, ...) end`).
### `dbg.shorten_path(path)`
Shortens `path`: If path is a subpath of a mod, it will be shortened to `"<modname>:<subpath>"`.
## Security
Debug deliberately exposes the unrestricted `debug` API globally, as well as the `dbg` wrapper API,
both of which can be abused to exit the mod security sandbox.
**Only use `dbg` in environments where you trust all enabled mods.**
**Adding `dbg` to `secure.trusted_mods` (recommended) or disabling mod security (not recommended) is required.**
The `/lua` chatcommand must explicitly be enabled on servers by setting `secure.dbg.lua` to `true`;
if enabled, server owners risk unprivileged users gaining access through MITM attacks.
## Usage
**Prerequisites:** LuaJIT and a terminal with decent ANSI support are highly recommended.
### `/dbg`
Calls `dbg()` to start debugging in the console.
### `/lua <code>`
Executes the code and pretty-prints the result(s) to chat.
Only available in singleplayer for security reasons (risk of MITM attacks).
---
Links: [GitHub](https://github.com/appgurueu/dbg), [ContentDB](https://content.minetest.net/packages/LMD/dbg), [Minetest Forums](https://forum.minetest.net/viewtopic.php?f=9&t=28372)
License: Written by Lars Müller and licensed under the MIT license (see `License.txt`).

46
chat_commands.lua Normal file
View File

@ -0,0 +1,46 @@
minetest.register_chatcommand("dbg", {
description = "Start debugging",
privs = { server = true },
func = function() return dbg() end,
})
if not minetest.is_singleplayer() then
return
end
local token_colors = {
["nil"] = "#ff5f00",
boolean = "#ff5f00",
number = "#0000ff",
string = "#008700",
reference = "#af0000",
["function"] = "#ff0000",
type = "#00ffff",
}
minetest.register_chatcommand("lua", {
params = "<code>",
description = "Execute Lua code",
privs = {server = true},
func = function(_, code)
local func, err = loadstring("return " .. code, "=cmd")
if not func then
func, err = loadstring(code, "=cmd")
end
if not func then
return false, minetest.colorize("red", "syntax error: ") .. err
end
local function handle(status, ...)
local rope = {status and minetest.colorize("lime", "returned: ") or minetest.colorize("red", "error: ")}
dbg.ppp({
write = function(text, token)
table.insert(rope, token and minetest.colorize(token_colors[token], text) or text)
end,
upvalues = false -- keep matters short
}, ...)
local str = table.concat(rope)
return status, #str > 16e3 and (str:sub(1, 16e3) .. " <truncated>") or str
end
-- Use pcall: No point in proper stacktraces for oneliners
return handle(pcall(func))
end,
})

288
dbg.lua Normal file
View File

@ -0,0 +1,288 @@
local debug = ...
local function vars(where, getvar, setvar)
local idx = {}
do
local i = 1
while true do
local name = getvar(where, i)
if name == nil then break end
idx[name] = i
i = i + 1
end
end
return setmetatable({}, {
__index = function(_, name)
local _, value = getvar(where, assert(idx[name], "no variable with given name"))
return value
end,
__newindex = function(_, name, value)
setvar(where, assert(idx[name], "no variable with given name"), value)
end,
__call = function()
local i = 0
local function iterate()
i = i + 1
-- Making this a tail call requires changing `where`
local name, value = getvar(where, i)
return name, value
end
return iterate
end
})
end
function dbg.locals(level)
level = level or 1
return vars(level + 1, debug.getlocal, debug.setlocal)
end
function dbg.upvals(func)
if type(func) ~= "function" then
func = debug.getinfo((func or 1) + 1, "f").func
end
return vars(func, debug.getupvalue, debug.setupvalue)
end
function dbg.vars(level)
level = (level or 1) + 1
local func = debug.getinfo(level, "f").func
local idx, is_local = {}, {}
-- Upvals
do
local i = 1
while true do
local name = debug.getupvalue(func, i)
if name == nil then break end
idx[name] = i
i = i + 1
end
end
-- Locals
do
local i = 1
while true do
local name = debug.getlocal(level, i)
if name == nil then break end
idx[name] = i
is_local[name] = true -- might shadow upval
i = i + 1
end
end
return setmetatable({}, {
__index = function(_, name)
local var_idx = assert(idx[name], "no variable with given name")
local _, value
if is_local[name] then
_, value = debug.getlocal(level, var_idx)
else
_, value = debug.getupvalue(func, var_idx)
end
return value
end,
__newindex = function(_, name, value)
local var_idx = assert(idx[name], "no variable with given name")
if is_local[name] then
debug.setlocal(level, var_idx, value)
else
debug.setupvalue(func, var_idx, value)
end
end,
__call = function()
local i, upvals = 1, true
local function iterate()
local name, value
if upvals then
repeat -- search for not-shadowed upvals
name, value = debug.getupvalue(func, i)
i = i + 1
until not is_local[name]
if name == nil then
i, upvals = 1, false
return iterate()
end
else
name, value = debug.getlocal(level, i)
i = i + 1
end
return name, value
end
return iterate
end
})
end
-- Roughly the same format as used by debug.traceback, but paths are shortened
local function fmt_callinfo(level)
local info = debug.getinfo(level, "Snlf")
if not info then
return
end
local is_path = info.source:match"^@"
local short_src = is_path and dbg.shorten_path(info.short_src) or info.short_src
local where
if (info.namewhat or "") ~= "" then
where = "in function " .. info.name
elseif info.what == "Lua" then
where = ("in function defined at line %d"):format(info.linedefined)
elseif info.what == "main" then
where = "in main chunk"
else
where = "?"
end
return short_src .. ":" .. (info.currentline > 0 and ("%d:"):format(info.currentline) or "") .. " " .. where, info.func
end
local max_top_levels, max_bottom_levels = 5, 5
function dbg._traceback(level, until_func --[[and including]])
level = (level or 1) + 1
local res = {"stack traceback:"}
local function concat() return table.concat(res, "\n\t") end
-- Write top levels
for top_level = 1, max_top_levels do
local str, func = fmt_callinfo(level + top_level)
if not str then return concat() end
table.insert(res, str)
if func == until_func then return concat() end
end
local last_written_top_level = level + max_top_levels
-- Determine stack depth
level = last_written_top_level
repeat
level = level + 1
until not debug.getinfo(level, "")
-- Write bottom levels
local first_bottom_level = level - max_bottom_levels
if last_written_top_level + 1 > first_bottom_level then
first_bottom_level = last_written_top_level + 1
else
table.insert(res, "...")
end
for bottom_level = first_bottom_level, level - 1 do
local str, func = fmt_callinfo(bottom_level)
table.insert(res, str)
if func == until_func then return concat() end
end
return concat()
end
-- Hide until_func parameter
function dbg.traceback(level)
return dbg._traceback(level)
end
function dbg.stackinfo(level)
local res = {}
while true do
local info = debug.getinfo(level)
if not info then return res end
table.insert(res, info)
end
end
--! Only available on Lua 5.2 / LuaJIT; use the `arg` local on Lua 5.1 instead
function dbg.getvararg(level)
level = (level or 1) + 1
local function _getvararg(i)
local name, value = debug.getlocal(level, i)
if not name then return end
return value, _getvararg(i - 1)
end
return _getvararg(-1)
end
-- Test dbg.getvararg to set it to `nil` if it isn't supported
(function(...) -- luacheck: ignore
local args = {dbg.getvararg()}
if #args == 3 then
for i = 1, 3 do
if args[i] ~= i then
dbg.getvararg = nil
break
end
end
else
dbg.getvararg = nil
end
end)(1, 2, 3)
local function nils(n)
if n == 1 then return nil end
return nil, nils(n - 1)
end
local function getargs(func)
-- This function must be explicitly handled
-- as otherwise the first call to it might trigger a false-positive in the hook
if func == nils then return {"n"} end
local what = debug.getinfo(func, "S").what
if what == "C" then return {"?"} end
if what == "main" then return {"..."} end
assert(what == "Lua")
local hook, mask, count = debug.gethook()
local args = {}
debug.sethook(function()
local called_func = debug.getinfo(2, "f")
if called_func.func ~= func then return end
local i = 1
while true do
local name = debug.getlocal(2, i)
if name == nil or name:match"^%(" then break end
table.insert(args, name)
i = i + 1
end
error(args)
end, "c")
local status, _args = pcall(func)
assert(not status and args == _args)
-- Check for vararg by supplying one extraneous param
debug.sethook(function()
local called_func = debug.getinfo(2, "f")
if called_func.func ~= func then return end
if debug.getlocal(2, -1) then -- vararg
table.insert(args, "...")
end
error(args)
end, "c")
status, _args = pcall(func, nils(#args + 1))
assert(not status and args == _args)
debug.sethook(hook, mask, count) -- restore previous hook
return args
end
local function shallowequals(t1, t2)
for k, v in pairs(t1) do
if t2[k] ~= v then return false end
end
return true
end
-- Tests to check for PUC Lua 5.1 unreliability
-- Ignore "unused argument" warnings
-- luacheck: push ignore 212
local function test_getargs()
for func, expected_arg_list in pairs{
[function(x, y, z)end] = {"x", "y", "z"}, -- unused arguments
[function(...)end] = {"..."}, -- unused vararg
[function(x, y, z, ...)end] = {"x", "y", "z", "..."}, -- both
[nils] = {"n"}
} do
if not shallowequals(getargs(func), expected_arg_list) then
return false
end
end
return true
end
-- luacheck: pop
dbg.getargs = getargs
dbg.getargs_reliable = test_getargs()

175
dd.lua Normal file
View File

@ -0,0 +1,175 @@
local debug = ...
local nil_placeholder = {}
local function dd(level, caught_err)
level = (level or 1) + 1 -- add one stack level for this func (dd)
print(dbg.traceback(level))
local func = debug.getinfo(level, "f").func
local func_env = getfenv(level)
level = level + 3 -- __[new]index + loaded chunk + xpcall
local mt = {
__index = function(env, varname)
-- Debug locals
local val = env._L[varname]
if val ~= nil then
if val == nil_placeholder then
return nil
end
return val
end
-- Locals of the caller
local i = 1
while true do
local name, value = debug.getlocal(level, i)
if name == nil then break end
if varname == name then return value end
i = i + 1
end
-- Upvalues of the caller
i = 1
while true do
local name, value = debug.getupvalue(func, i)
if name == nil then break end
if varname == name then return value end
i = i + 1
end
return func_env[varname]
end,
__newindex = function(env, varname, value)
-- Debug locals
if env._L[varname] ~= nil then
if value == nil then value = nil_placeholder end
env._L[varname] = value
return
end
-- Locals
local i = 1
while true do
local name = debug.getlocal(level, i)
if name == nil then break end
if varname == name then
debug.setlocal(level, i, value)
return
end
i = i + 1
end
-- Upvalues
i = 1
while true do
local name = debug.getupvalue(func, i)
if name == nil then break end
if varname == name then
debug.setupvalue(func, i, value)
return
end
i = i + 1
end
func_env[varname] = value -- if local by default - how to access parent env then? _ENV?
end
}
-- TODO (?) special debug & dbg wrappers with stack level offsets
-- Functions: debug.(getinfo|[gs]et(local|upvalue)|traceback), [gs]etfenv, dbg.(* \ getargs)
local env = setmetatable({_L = {}, _G = _G, _ENV = func_env}, mt)
-- Source buffer
local buf, buf_i = {}, 0
local function getbuf()
buf_i = buf_i + 1
if not buf[buf_i] then return end
return buf[buf_i] .. "\n"
end
local function loadbuf()
buf_i = 0
return load(getbuf, "=stdin")
end
while true do
io.write(#buf == 0 and "dbg> " or ("[%d]> "):format(#buf + 1))
local line = io.read()
if not line then -- EOF
print()
minetest.request_shutdown("debugging", true, 0)
break
end
if line == "cont" or (caught_err and line == "err") then
return line == "err"
end
local chunk, err, continuation
if line:match"^%s+$" then -- skip spacing-only lines
continuation = #buf ~= 0
else
if line:match"^=" then
line = "return " .. line:sub(2)
end
buf[#buf + 1] = line
chunk, err = loadbuf()
if #buf == 1 and not chunk and not line:match"^return " then
-- Try implicit return
buf = {"return " .. line}
chunk, err = loadbuf()
end
continuation = err and err:find"<eof>" -- same hack as used in the Lua REPL
end
if chunk then
setfenv(chunk, env)
local hook, mask, count = debug.gethook()
local function restore_hook()
return debug.sethook(hook, mask, count)
end
debug.sethook(function()
if debug.getinfo(2, "f").func ~= chunk then return end
local i = 1
while true do
local name, value = debug.getlocal(2, i)
if name == nil then break end
if value == nil then value = nil_placeholder end
env._L[name] = value
i = i + 1
end
end, "r");
(function(status, ...)
if status then
restore_hook()
dbg.pp(...)
end
end)(xpcall(chunk, function(error)
restore_hook()
print(dbg._traceback(3, chunk)) -- this handler + [C]: function error -> 2 levels to skip
io.write"error: "; dbg.pp(error)
end))
buf = {} -- clear buffer
elseif continuation then
if #buf == 1 then -- overwrite first line
io.write("\27[F[1]> ", buf[1], "\n")
end
else
io.write("syntax error: ", err, "\n")
buf = {} -- clear buffer
end
end
end
function dbg.dd(level)
return dd(level)
end
local error = error -- localize error to allow overriding _G.error = dbg.error
function dbg.error(msg, level)
print("caught error: "); dbg.pp(msg)
if dd(level, true) then
return error(msg, level)
end
end
function dbg.assert(value, msg)
if not value then dbg.error(msg or "assertion failed!") end
return value
end

23
init.lua Normal file
View File

@ -0,0 +1,23 @@
dbg = {}
local debug = assert(minetest.request_insecure_environment(), "add dbg to secure.trusted_mods").debug
local function load(filename)
return assert(loadfile(minetest.get_modpath(minetest.get_current_modname()) .. ("/%s.lua"):format(filename)))(debug)
end
load"shorten_path"
load"pp"
load"dbg"
load"dd"
setmetatable(dbg, {__call = function(_, ...) return dbg.dd(...) end})
load"chat_commands"
load"test"
_G.debug = debug -- deliberately expose the insecure debug library
-- TODO (...) hook call events to intercept actual assert/error; set nil debug metatable to intercept attempts
-- TODO (?) "inf" loop "detection" through (line or instr?) hook?

3
mod.conf Normal file
View File

@ -0,0 +1,3 @@
name = dbg
title = Debug
description = Debugging on steroids

216
pp.lua Normal file
View File

@ -0,0 +1,216 @@
local debug = ...
local default_params = {upvalues = true}
do
local write = io.write
-- See https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit
local color_codes = {
["nil"] = 202,
boolean = 202,
number = 21,
string = 28,
reference = 124,
["function"] = 196,
type = 14,
}
function default_params.write(str, token)
if token then
write(("\27[38;5;%dm"):format(color_codes[token])) -- set appropriate FG color
write(str)
write("\27[0m") -- reset FG color
else
write(str)
end
end
end
local keywords = {}
for _, keyword in pairs{
"and", "break", "do", "else", "elseif", "end",
"false", "for", "function", "goto", "if", "in",
"local", "nil", "not", "or", "repeat", "return",
"then", "true", "until", "while"
} do
keywords[keyword] = true
end
-- Single quotes don't need to be escaped
local escapes = {}
for char in ("abfrtv\n\\\""):gmatch"." do
local escaped = "\\" .. char
escapes[loadstring(('return "%s"'):format(escaped))()] = escaped
end
-- ("%q"):format(str) doesn't deal with tabs etc. gracefully so we must roll our own
local function escape_str(str)
return str:gsub(".", escapes):gsub("([^\32-\126])()", function(char, after_pos)
if escapes[char] then return end
return (str:sub(after_pos, after_pos):match"%d" and "\\%03d" or "\\%d"):format(char:byte())
end)
end
-- TODO (?) cross/back reference distinction
-- TODO (???) index _G to produce more sensible names (very ugly performance implications; only do at load-time?)
local pp = function(params, ...)
local varg_len = select("#", ...)
local write = params.write or default_params.write
local upvalues = params.upvalues
if upvalues == nil then
upvalues = default_params.upvalues
end
-- Count references
local refs = {}
local function count_refs(val)
local typ = type(val)
if val == nil or typ == "boolean" or typ == "number" or typ == "string" then return end
if refs[val] then
refs[val].count = refs[val].count + 1
return
end
refs[val] = {count = 1}
if typ == "function" then
if upvalues then
local i = 1
while true do
local name, upval = debug.getupvalue(val, i)
if name == nil then break end
count_refs(upval)
i = i + 1
end
end
elseif typ == "table" then
for k, v in pairs(val) do
count_refs(k)
count_refs(v)
end
end
end
for i = 1, varg_len do
count_refs(select(i, ...))
end
local ref_id = 1
local function pp(val, indent)
local function newline()
write"\n"
for _ = 1, indent do
write"\t"
end
end
local typ = type(val)
if val == nil then
write("nil", "nil")
elseif typ == "boolean" then
write(val and "true" or "false", "boolean")
elseif typ == "number" then
write(("%.17g"):format(val), "number")
elseif typ == "string" then
write('"' .. escape_str(val) .. '"', "string")
else -- reference type
local ref_info = refs[val]
if ref_info.count > 1 then
write(("*%d"):format(ref_info.id or ref_id), "reference")
if ref_info.id then
return -- ID assigned => already written
end
-- Assign ID
ref_info.id = ref_id
ref_id = ref_id + 1
write" "
end
if typ == "function" then
local info = debug.getinfo(val, "Su")
local write_upvals = upvalues and info.nups > 0
if write_upvals then
write"("; write("function", "function"); write"()"
for i = 1, info.nups do
local name, value = debug.getupvalue(val, i)
newline()
write("local", "function"); write" "; write(name); write" = "; pp(value, indent+1)
end
newline()
end
write((write_upvals and "return " or "") .. "function", "function")
write"("; write(table.concat(dbg.getargs(val), ", ")); write") "
local line = ""
if info.linedefined > 0 then
if info.linedefined == info.lastlinedefined then
line = (":%d"):format(info.linedefined)
else
line = (":%d-%d"):format(info.linedefined, info.lastlinedefined)
end
end
write(dbg.shorten_path(info.short_src)); write(line)
write" "; write("end", "function")
if write_upvals then
indent = indent - 1; newline()
write("end", "function"); write")"
end
elseif typ == "table" then
write"{"
local len = 0
local first = true
for _, v in ipairs(val) do
if not first then write"," end
newline()
pp(v, indent+1)
len = len + 1
first = false
end
local hash_keys = {}
local traversal_order = {}
local i = 1
for k in pairs(val) do
if not (type(k) == "number" and k % 1 == 0 and k >= 1 and k <= len) then
table.insert(hash_keys, k)
traversal_order[k] = i
i = i + 1
end
end
table.sort(hash_keys, function(a, b)
local t_a, t_b = type(a), type(b)
if t_a ~= t_b then
return t_a < t_b
end
if t_a == "string" or t_a == "number" then
return a < b
end
return traversal_order[a] < traversal_order[b]
end)
for _, k in ipairs(hash_keys) do
if not first then write"," end
local v = val[k]
newline()
if type(k) == "string" and not keywords[k] and k:match"^[A-Za-z_][A-Za-z%d_]*$" then
write(k); write" = "
else
write"["; pp(k, indent + 1); write"] = "
end
pp(v, indent + 1)
first = false
end
if next(val) ~= nil then indent = indent - 1; newline() end
write"}"
else
write(("<%s>"):format(typ), "type")
end
end
end
for i = 1, varg_len do
pp(select(i, ...), 1)
if i < varg_len then write",\n" end
end
if varg_len > 0 then write"\n" end
end
function dbg.pp(...)
return pp(default_params, ...)
end
function dbg.ppp(params, ...)
return pp(params, ...)
end

25
shorten_path.lua Normal file
View File

@ -0,0 +1,25 @@
-- Build a trie (prefix tree) with all mod paths
local modpath_trie = {}
for _, modname in pairs(minetest.get_modnames()) do
local path = minetest.get_modpath(modname)
local subtrie = modpath_trie
for char in path:gmatch"." do
subtrie[char] = subtrie[char] or {}
subtrie = subtrie[char]
end
subtrie["\\"] = modname
subtrie["/"] = modname
end
function dbg.shorten_path(path)
-- Search for a prefix (paths have at most one prefix)
local subtrie = modpath_trie
for i = 1, #path do
if type(subtrie) == "string" then
return subtrie .. ":" .. path:sub(i)
end
subtrie = subtrie[path:sub(i, i)]
if not subtrie then return path end
end
return path
end

53
test.lua Normal file
View File

@ -0,0 +1,53 @@
-- Test variable utils
local a, b, c = nil, "b", "c" -- luacheck: ignore
local function assert_vars(vartype, expected)
local vars = dbg[vartype](2)
local i = 0
for name, value in vars() do
assert(vars[name] == value)
assert(expected[i + 1] == name and expected[i + 2] == value)
vars[name] = 42
assert(vars[name] == 42)
vars[name] = value
assert(vars[name] == value)
i = i + 2
end
assert(i == #expected)
end
local f = function(c, e) -- luacheck: ignore
assert(a == nil)
assert(b == "b")
assert_vars("upvals", {
"a", nil;
"b", "b";
"assert_vars", assert_vars;
})
do
local upvals = dbg.upvals()
upvals.a = "a"
assert(upvals.a == "a" and a == "a")
end
local f, g = "f", "g"
assert_vars("locals", {
"c", c;
"e", e;
"f", f;
"g", g;
})
do
local locals = dbg.locals()
assert(locals.f == "f" and locals.g == "g")
locals.c, locals.e = "c", "e"
assert(locals.c == "c" and locals.e == "e" and c == "c" and e == "e")
end
assert_vars("vars", {
"a", a;
"b", b;
"assert_vars", assert_vars;
"c", c;
"e", e;
"f", f;
"g", g;
})
end
f()