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 `/poll`, `/cancelpoll`, and `/endpoll` commands. (#6583, #6605)
|
||||||
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, `/lockprediction`, and `/completeprediction` commands. (#6583, #6612, #6632)
|
- Minor: Added broadcaster-only `/prediction`, `/cancelprediction`, `/lockprediction`, and `/completeprediction` commands. (#6583, #6612, #6632)
|
||||||
- Minor: Added support for BetterTTV Pro subscriber badges. (#6625)
|
- 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 title and duration options for `/clip` command. (#6669)
|
||||||
- Minor: Added Markdown support to user notes. (#6490)
|
- Minor: Added Markdown support to user notes. (#6490)
|
||||||
- Bugfix: Moderation checks now include the lead moderator badge. (#6642)
|
- 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)
|
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
|
### Changed globals
|
||||||
|
|
||||||
#### `load(chunk [, chunkname [, mode [, env]]])`
|
#### `load(chunk [, chunkname [, mode [, env]]])`
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ set(SOURCE_FILES
|
|||||||
controllers/plugins/api/Accounts.hpp
|
controllers/plugins/api/Accounts.hpp
|
||||||
controllers/plugins/api/ChannelRef.cpp
|
controllers/plugins/api/ChannelRef.cpp
|
||||||
controllers/plugins/api/ChannelRef.hpp
|
controllers/plugins/api/ChannelRef.hpp
|
||||||
|
controllers/plugins/api/DebugLibrary.cpp
|
||||||
|
controllers/plugins/api/DebugLibrary.hpp
|
||||||
controllers/plugins/api/EventType.hpp
|
controllers/plugins/api/EventType.hpp
|
||||||
controllers/plugins/api/HTTPRequest.cpp
|
controllers/plugins/api/HTTPRequest.cpp
|
||||||
controllers/plugins/api/HTTPRequest.hpp
|
controllers/plugins/api/HTTPRequest.hpp
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
# include "controllers/commands/CommandController.hpp"
|
# include "controllers/commands/CommandController.hpp"
|
||||||
# include "controllers/plugins/api/Accounts.hpp"
|
# include "controllers/plugins/api/Accounts.hpp"
|
||||||
# include "controllers/plugins/api/ChannelRef.hpp"
|
# include "controllers/plugins/api/ChannelRef.hpp"
|
||||||
|
# include "controllers/plugins/api/DebugLibrary.hpp"
|
||||||
# include "controllers/plugins/api/HTTPRequest.hpp"
|
# include "controllers/plugins/api/HTTPRequest.hpp"
|
||||||
# include "controllers/plugins/api/HTTPResponse.hpp"
|
# include "controllers/plugins/api/HTTPResponse.hpp"
|
||||||
# include "controllers/plugins/api/IOWrapper.hpp"
|
# include "controllers/plugins/api/IOWrapper.hpp"
|
||||||
@@ -207,6 +208,13 @@ void PluginController::openLibrariesFor(Plugin *plugin)
|
|||||||
r["_IO_input"] = sol::nil;
|
r["_IO_input"] = sol::nil;
|
||||||
r["_IO_output"] = 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);
|
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();
|
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
|
||||||
|
|
||||||
namespace chatterino {
|
namespace chatterino {
|
||||||
@@ -1528,23 +1546,15 @@ TEST_P(PluginJsonTest, Run)
|
|||||||
auto reg = lua->registry().size();
|
auto reg = lua->registry().size();
|
||||||
auto globals = lua->globals().size();
|
auto globals = lua->globals().size();
|
||||||
|
|
||||||
auto pfr = lua->safe_script_file(luaTestPath("json", GetParam()));
|
runLuaTest("json", GetParam(), *this->lua);
|
||||||
EXPECT_TRUE(pfr.valid());
|
|
||||||
if (!pfr.valid())
|
|
||||||
{
|
|
||||||
qDebug() << "Test" << GetParam() << "failed:";
|
|
||||||
sol::error err = pfr;
|
|
||||||
qDebug() << err.what();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (size_t i = 0; i < 5; i++)
|
for (size_t i = 0; i < 5; i++)
|
||||||
{
|
{
|
||||||
lua->collect_garbage();
|
lua->collect_garbage();
|
||||||
}
|
}
|
||||||
// make sure we don't leak anything to globals or the registry
|
// make sure we don't leak anything to globals or the registry
|
||||||
// but give the registry some room of 1 slot
|
// but give the registry some room of 3 slots (two from getting the debug library)
|
||||||
EXPECT_LE(lua->registry().size(), reg + 1);
|
EXPECT_LE(lua->registry().size(), reg + 3);
|
||||||
EXPECT_EQ(lua->globals().size(), globals);
|
EXPECT_EQ(lua->globals().size(), globals);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1557,17 +1567,8 @@ class PluginMessageTest : public PluginTest,
|
|||||||
};
|
};
|
||||||
TEST_P(PluginMessageTest, Run)
|
TEST_P(PluginMessageTest, Run)
|
||||||
{
|
{
|
||||||
configure();
|
this->configure();
|
||||||
|
runLuaTest("message", GetParam(), *this->lua);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
INSTANTIATE_TEST_SUITE_P(PluginMessage, PluginMessageTest,
|
INSTANTIATE_TEST_SUITE_P(PluginMessage, PluginMessageTest,
|
||||||
@@ -1593,4 +1594,63 @@ TEST_F(PluginTest, testAccounts)
|
|||||||
ASSERT_TRUE(res.valid()) << res.get<sol::error>().what();
|
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
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user