feat(plugins): add debug.traceback (#6652)
Reviewed-by: Mm2PL <mm2pl+gh@kotmisia.pl>
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
- Minor: Added broadcaster-only `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605)
|
||||
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, `/lockprediction`, and `/completeprediction` commands. (#6583, #6612, #6632)
|
||||
- Minor: Added support for BetterTTV Pro subscriber badges. (#6625)
|
||||
- Minor: Added `debug.traceback` for plugins. (#6652)
|
||||
- Minor: Added title and duration options for `/clip` command. (#6669)
|
||||
- Minor: Added Markdown support to user notes. (#6490)
|
||||
- Bugfix: Moderation checks now include the lead moderator badge. (#6642)
|
||||
|
||||
@@ -850,6 +850,31 @@ and [`file:write()`](https://www.lua.org/manual/5.4/manual.html#pdf-file:write).
|
||||
|
||||
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-io.write)
|
||||
|
||||
### Debug API
|
||||
|
||||
To aid debugging, Chatterino provides Lua's `debug.traceback` function.
|
||||
Other functions from the `debug` library are not exposed.
|
||||
|
||||
#### `traceback([thread,] [message [, level]])`
|
||||
|
||||
Returns a stack trace of `thread` or the current thread with an optional `message` prepended.
|
||||
`level` can be used to skip some frames. By default, it's 1 (skipping the current function).
|
||||
|
||||
This can be used as a message handler in `xpcall`:
|
||||
|
||||
```lua
|
||||
local function main()
|
||||
c2.ThisDoesNotExistAndWillError()
|
||||
end
|
||||
|
||||
local ok, result = xpcall(main, debug.traceback)
|
||||
if not ok then
|
||||
print(result)
|
||||
end
|
||||
```
|
||||
|
||||
See [official documentation](https://www.lua.org/manual/5.4/manual.html#pdf-debug.traceback)
|
||||
|
||||
### Changed globals
|
||||
|
||||
#### `load(chunk [, chunkname [, mode [, env]]])`
|
||||
|
||||
@@ -254,6 +254,8 @@ set(SOURCE_FILES
|
||||
controllers/plugins/api/Accounts.hpp
|
||||
controllers/plugins/api/ChannelRef.cpp
|
||||
controllers/plugins/api/ChannelRef.hpp
|
||||
controllers/plugins/api/DebugLibrary.cpp
|
||||
controllers/plugins/api/DebugLibrary.hpp
|
||||
controllers/plugins/api/EventType.hpp
|
||||
controllers/plugins/api/HTTPRequest.cpp
|
||||
controllers/plugins/api/HTTPRequest.hpp
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# include "controllers/commands/CommandController.hpp"
|
||||
# include "controllers/plugins/api/Accounts.hpp"
|
||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||
# include "controllers/plugins/api/DebugLibrary.hpp"
|
||||
# include "controllers/plugins/api/HTTPRequest.hpp"
|
||||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||
@@ -207,6 +208,13 @@ void PluginController::openLibrariesFor(Plugin *plugin)
|
||||
r["_IO_input"] = sol::nil;
|
||||
r["_IO_output"] = sol::nil;
|
||||
}
|
||||
// set up debug lib
|
||||
{
|
||||
auto debuglib = lua.create_table();
|
||||
g["debug"] = debuglib;
|
||||
|
||||
debuglib.set_function("traceback", lua::api::debugTraceback);
|
||||
}
|
||||
PluginController::initSol(lua, plugin);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
#include "controllers/plugins/api/DebugLibrary.hpp"
|
||||
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
|
||||
# include <sol/sol.hpp>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
|
||||
/// Signature in Lua: ([thread,] [message [, level]])
|
||||
int debugTraceback(lua_State *L)
|
||||
{
|
||||
int argOffset = 1;
|
||||
lua_State *targetThread = L;
|
||||
|
||||
// If the first argument is a thread, take that as our target
|
||||
if (lua_isthread(L, argOffset))
|
||||
{
|
||||
targetThread = lua_tothread(L, argOffset);
|
||||
argOffset += 1;
|
||||
}
|
||||
|
||||
// The message is an optional string. If it's not a string/number/nil, we
|
||||
// return the message as-is and don't create a traceback.
|
||||
const char *msg = lua_tostring(L, argOffset);
|
||||
if (!msg && !lua_isnoneornil(L, argOffset))
|
||||
{
|
||||
lua_pushvalue(L, argOffset);
|
||||
return 1;
|
||||
}
|
||||
argOffset += 1;
|
||||
|
||||
int defaultLevel = 0;
|
||||
if (L == targetThread)
|
||||
{
|
||||
defaultLevel = 1;
|
||||
}
|
||||
|
||||
int level = static_cast<int>(luaL_optinteger(L, argOffset, defaultLevel));
|
||||
// push a traceback with `msg` at the start
|
||||
luaL_traceback(L, targetThread, msg, level);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#ifdef CHATTERINO_HAVE_PLUGINS
|
||||
# include <sol/forward.hpp>
|
||||
|
||||
namespace chatterino::lua::api {
|
||||
|
||||
/// A reimplementation of Lua's debug.traceback (ldblib.c).
|
||||
/// Creates a traceback of a thread.
|
||||
/// It's intended to be used as a message handler in `xpcall`.
|
||||
/// See https://www.lua.org/manual/5.4/manual.html#pdf-debug.traceback
|
||||
int debugTraceback(lua_State *L);
|
||||
|
||||
} // namespace chatterino::lua::api
|
||||
#endif
|
||||
+82
-22
@@ -157,6 +157,24 @@ std::string luaTestPath(const QString &category, const QString &entry)
|
||||
return luaTestBaseDir(category).filePath(entry + ".lua").toStdString();
|
||||
}
|
||||
|
||||
bool runLuaTest(const QString &category, const QString &entry,
|
||||
sol::state_view lua)
|
||||
{
|
||||
auto loadResult = lua.load_file(luaTestPath(category, entry));
|
||||
auto pfn = loadResult.get<sol::protected_function>();
|
||||
pfn.set_error_handler(lua["debug"]["traceback"]);
|
||||
auto pfr = pfn.call();
|
||||
EXPECT_TRUE(pfr.valid());
|
||||
if (!pfr.valid())
|
||||
{
|
||||
qDebug().noquote() << "Test" << entry << "failed:";
|
||||
sol::error err = pfr;
|
||||
qDebug().noquote() << err.what();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace chatterino {
|
||||
@@ -1528,23 +1546,15 @@ TEST_P(PluginJsonTest, Run)
|
||||
auto reg = lua->registry().size();
|
||||
auto globals = lua->globals().size();
|
||||
|
||||
auto pfr = lua->safe_script_file(luaTestPath("json", GetParam()));
|
||||
EXPECT_TRUE(pfr.valid());
|
||||
if (!pfr.valid())
|
||||
{
|
||||
qDebug() << "Test" << GetParam() << "failed:";
|
||||
sol::error err = pfr;
|
||||
qDebug() << err.what();
|
||||
return;
|
||||
}
|
||||
runLuaTest("json", GetParam(), *this->lua);
|
||||
|
||||
for (size_t i = 0; i < 5; i++)
|
||||
{
|
||||
lua->collect_garbage();
|
||||
}
|
||||
// make sure we don't leak anything to globals or the registry
|
||||
// but give the registry some room of 1 slot
|
||||
EXPECT_LE(lua->registry().size(), reg + 1);
|
||||
// but give the registry some room of 3 slots (two from getting the debug library)
|
||||
EXPECT_LE(lua->registry().size(), reg + 3);
|
||||
EXPECT_EQ(lua->globals().size(), globals);
|
||||
}
|
||||
|
||||
@@ -1557,17 +1567,8 @@ class PluginMessageTest : public PluginTest,
|
||||
};
|
||||
TEST_P(PluginMessageTest, Run)
|
||||
{
|
||||
configure();
|
||||
|
||||
auto pfr = lua->safe_script_file(luaTestPath("message", GetParam()));
|
||||
EXPECT_TRUE(pfr.valid());
|
||||
if (!pfr.valid())
|
||||
{
|
||||
qDebug() << "Test" << GetParam() << "failed:";
|
||||
sol::error err = pfr;
|
||||
qDebug() << err.what();
|
||||
return;
|
||||
}
|
||||
this->configure();
|
||||
runLuaTest("message", GetParam(), *this->lua);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(PluginMessage, PluginMessageTest,
|
||||
@@ -1593,4 +1594,63 @@ TEST_F(PluginTest, testAccounts)
|
||||
ASSERT_TRUE(res.valid()) << res.get<sol::error>().what();
|
||||
}
|
||||
|
||||
TEST_F(PluginTest, debugTraceback)
|
||||
{
|
||||
configure();
|
||||
|
||||
QString traceback = lua->script(R"lua(
|
||||
local function other()
|
||||
error("oh no")
|
||||
end
|
||||
local function main()
|
||||
local function inner()
|
||||
other()
|
||||
end
|
||||
inner()
|
||||
end
|
||||
|
||||
local ok, res = xpcall(main, debug.traceback)
|
||||
return res
|
||||
)lua")
|
||||
.get<QString>();
|
||||
ASSERT_TRUE(traceback.contains("[C]: in function 'error'"));
|
||||
ASSERT_TRUE(traceback.contains("[string \"...\"]:3: in upvalue 'other'"));
|
||||
ASSERT_TRUE(traceback.contains("[string \"...\"]:7: in local 'inner'"));
|
||||
ASSERT_TRUE(traceback.contains(
|
||||
"[string \"...\"]:9: in function <[string \"...\"]:5>"));
|
||||
ASSERT_TRUE(traceback.contains("[C]: in function 'xpcall'"));
|
||||
ASSERT_TRUE(traceback.contains("[string \"...\"]:12: in main chunk"));
|
||||
|
||||
traceback = lua->script(R"lua(
|
||||
return debug.traceback()
|
||||
)lua")
|
||||
.get<QString>();
|
||||
ASSERT_TRUE(traceback.contains("[string \"...\"]:2: in main chunk"));
|
||||
|
||||
traceback = lua->script(R"lua(
|
||||
return debug.traceback("my message")
|
||||
)lua")
|
||||
.get<QString>();
|
||||
ASSERT_TRUE(traceback.contains("my message"));
|
||||
ASSERT_TRUE(traceback.contains("[string \"...\"]:2: in main chunk"));
|
||||
|
||||
traceback = lua->script(R"lua(
|
||||
local coro = coroutine.create(function ()
|
||||
coroutine.yield()
|
||||
end)
|
||||
coroutine.resume(coro)
|
||||
return debug.traceback(coro)
|
||||
)lua")
|
||||
.get<QString>();
|
||||
ASSERT_TRUE(traceback.contains("[C]: in function 'coroutine.yield'"));
|
||||
ASSERT_TRUE(traceback.contains(
|
||||
"[string \"...\"]:3: in function <[string \"...\"]:2>"));
|
||||
|
||||
auto msg = lua->script(R"lua(
|
||||
return debug.traceback(c2.Message.new({id = "who would do this"}))
|
||||
)lua")
|
||||
.get<std::shared_ptr<Message>>();
|
||||
ASSERT_EQ(msg->id, "who would do this");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
Reference in New Issue
Block a user