summaryrefslogtreecommitdiff
path: root/tests
diff options
context:
space:
mode:
authorVaxry <43317083+vaxerski@users.noreply.github.com>2026-04-26 15:16:36 +0100
committerGitHub <noreply@github.com>2026-04-26 15:16:36 +0100
commit5ba33f846129f3e6c0505b19f94634b24505e828 (patch)
treeaaacabe678006040c342935481a86607c66607fd /tests
parent329b2b356dd92ac07f4e2aa69cbce82ee9ba9d19 (diff)
config/lua: init lua config manager, use lua if available (#13817)
Adds a new lua config manager, which will automatically be used if there is a lua config present.
Diffstat (limited to 'tests')
-rw-r--r--tests/config/MonitorParser.cpp2
-rw-r--r--tests/config/lua/ConfigValueTypes.cpp347
-rw-r--r--tests/config/lua/LuaBindingsInternal.cpp256
-rw-r--r--tests/config/lua/LuaConfigUtils.cpp105
-rw-r--r--tests/config/lua/LuaEventHandler.cpp115
-rw-r--r--tests/config/lua/LuaObjectsBasic.cpp266
-rw-r--r--tests/helpers/MathTransform.cpp7
7 files changed, 1094 insertions, 4 deletions
diff --git a/tests/config/MonitorParser.cpp b/tests/config/MonitorParser.cpp
index 29bd8db1..1d8fe664 100644
--- a/tests/config/MonitorParser.cpp
+++ b/tests/config/MonitorParser.cpp
@@ -128,7 +128,7 @@ TEST(Config, monitorParserTransformValid) {
for (int i = 0; i <= 7; i++) {
CMonitorRuleParser parser("DP-1");
EXPECT_TRUE(parser.parseTransform(std::to_string(i)));
- EXPECT_EQ(parser.rule().m_transform, static_cast<wl_output_transform>(i));
+ EXPECT_EQ(parser.rule().m_transform, sc<wl_output_transform>(i));
}
}
diff --git a/tests/config/lua/ConfigValueTypes.cpp b/tests/config/lua/ConfigValueTypes.cpp
new file mode 100644
index 00000000..90e97520
--- /dev/null
+++ b/tests/config/lua/ConfigValueTypes.cpp
@@ -0,0 +1,347 @@
+#include <config/lua/types/LuaConfigBool.hpp>
+#include <config/lua/types/LuaConfigColor.hpp>
+#include <config/lua/types/LuaConfigCssGap.hpp>
+#include <config/lua/types/LuaConfigFloat.hpp>
+#include <config/lua/types/LuaConfigFontWeight.hpp>
+#include <config/lua/types/LuaConfigGradient.hpp>
+#include <config/lua/types/LuaConfigInt.hpp>
+#include <config/lua/types/LuaConfigString.hpp>
+#include <config/lua/types/LuaConfigVec2.hpp>
+
+#include <macros.hpp>
+
+#include <gtest/gtest.h>
+
+#include <numbers>
+
+extern "C" {
+#include <lualib.h>
+#include <lauxlib.h>
+}
+
+using namespace Config;
+using namespace Config::Lua;
+
+namespace {
+ class CLuaState {
+ public:
+ CLuaState() : m_lua(luaL_newstate()) {
+ luaL_openlibs(m_lua);
+ }
+
+ ~CLuaState() {
+ if (m_lua)
+ lua_close(m_lua);
+ }
+
+ lua_State* get() const {
+ return m_lua;
+ }
+
+ private:
+ lua_State* m_lua = nullptr;
+ };
+
+ void pushVec2Table(lua_State* L, double x, double y) {
+ lua_createtable(L, 2, 2);
+ lua_pushnumber(L, x);
+ lua_rawseti(L, -2, 1);
+ lua_pushnumber(L, y);
+ lua_rawseti(L, -2, 2);
+ }
+}
+
+TEST(ConfigLuaValueTypes, boolParseAndReset) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigBool value(false);
+ EXPECT_FALSE(value.parsed());
+ EXPECT_FALSE(value.setByUser());
+
+ lua_pushboolean(L, true);
+ const auto err = value.parse(L);
+ lua_pop(L, 1);
+
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_TRUE(value.parsed());
+ EXPECT_TRUE(value.setByUser());
+
+ value.reset();
+ EXPECT_FALSE(value.parsed());
+
+ value.resetSetByUser();
+ EXPECT_FALSE(value.setByUser());
+}
+
+TEST(ConfigLuaValueTypes, boolBadType) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigBool value(false);
+
+ lua_pushinteger(L, 1);
+ const auto err = value.parse(L);
+ lua_pop(L, 1);
+
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_TYPE);
+ EXPECT_NE(err.message.find("requires a bool"), std::string::npos);
+}
+
+TEST(ConfigLuaValueTypes, intBooleanCastAndRangeAndMap) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigInt ranged(0, 0, 10);
+
+ lua_pushboolean(L, true);
+ auto err = ranged.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_EQ(ranged.parsed(), 1);
+
+ lua_pushinteger(L, 11);
+ err = ranged.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OUT_OF_RANGE);
+
+ CLuaConfigInt mapped(0, std::nullopt, std::nullopt, std::unordered_map<std::string, INTEGER>{{"auto", -1}, {"on", 1}});
+
+ lua_pushstring(L, "auto");
+ err = mapped.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_EQ(mapped.parsed(), -1);
+
+ lua_pushstring(L, "missing");
+ err = mapped.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE);
+ EXPECT_NE(err.message.find("unknown string value"), std::string::npos);
+}
+
+TEST(ConfigLuaValueTypes, floatParseAndRange) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigFloat value(0.F, -1.F, 1.F);
+
+ lua_pushnumber(L, 0.5);
+ auto err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_FLOAT_EQ(value.parsed(), 0.5F);
+
+ lua_pushinteger(L, 2);
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OUT_OF_RANGE);
+}
+
+TEST(ConfigLuaValueTypes, stringValidatorAndEmptyPush) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigString validated("default", std::optional<std::function<std::expected<void, std::string>(const STRING&)>>{[](const STRING& s) -> std::expected<void, std::string> {
+ if (s.starts_with("ok:"))
+ return {};
+ return std::unexpected("must start with ok:");
+ }});
+
+ lua_pushstring(L, "ok:value");
+ auto err = validated.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_EQ(validated.parsed(), "ok:value");
+
+ lua_pushstring(L, "bad");
+ err = validated.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE);
+ EXPECT_NE(err.message.find("must start with ok:"), std::string::npos);
+
+ CLuaConfigString emptyDefault(STRVAL_EMPTY);
+ emptyDefault.push(L);
+ ASSERT_TRUE(lua_isstring(L, -1));
+ EXPECT_STREQ(lua_tostring(L, -1), "");
+ lua_pop(L, 1);
+}
+
+TEST(ConfigLuaValueTypes, colorParseAndPush) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigColor value(0);
+
+ lua_pushstring(L, "0xFF00FF00");
+ auto err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_EQ(value.parsed(), 0xFF00FF00);
+
+ value.push(L);
+ ASSERT_TRUE(lua_isstring(L, -1));
+ EXPECT_STREQ(lua_tostring(L, -1), "0xFF00FF00");
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "@@@notacolor@@@");
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE);
+}
+
+TEST(ConfigLuaValueTypes, vec2ParseValidateAndPush) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigVec2 value({0, 0}, std::optional<std::function<std::expected<void, std::string>(const VEC2&)>>{[](const VEC2& v) -> std::expected<void, std::string> {
+ if (v.x < 0 || v.y < 0)
+ return std::unexpected("must be non-negative");
+ return {};
+ }});
+
+ pushVec2Table(L, 10, 20);
+ auto err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_FLOAT_EQ(value.parsed().x, 10.F);
+ EXPECT_FLOAT_EQ(value.parsed().y, 20.F);
+
+ value.push(L);
+ ASSERT_TRUE(lua_istable(L, -1));
+ lua_rawgeti(L, -1, 1);
+ EXPECT_DOUBLE_EQ(lua_tonumber(L, -1), 10);
+ lua_pop(L, 1);
+ lua_getfield(L, -1, "y");
+ EXPECT_DOUBLE_EQ(lua_tonumber(L, -1), 20);
+ lua_pop(L, 2);
+
+ pushVec2Table(L, -1, 0);
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE);
+ EXPECT_NE(err.message.find("must be non-negative"), std::string::npos);
+
+ lua_createtable(L, 3, 0);
+ lua_pushnumber(L, 1);
+ lua_rawseti(L, -2, 1);
+ lua_pushnumber(L, 2);
+ lua_rawseti(L, -2, 2);
+ lua_pushnumber(L, 3);
+ lua_rawseti(L, -2, 3);
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE);
+}
+
+TEST(ConfigLuaValueTypes, cssGapParseFormsAndRange) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigCssGap value(1, 0, 10);
+
+ lua_pushinteger(L, 7);
+ auto err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_EQ(value.parsed().m_top, 7);
+ EXPECT_EQ(value.parsed().m_right, 7);
+ EXPECT_EQ(value.parsed().m_bottom, 7);
+ EXPECT_EQ(value.parsed().m_left, 7);
+
+ lua_createtable(L, 0, 2);
+ lua_pushinteger(L, 3);
+ lua_setfield(L, -2, "top");
+ lua_pushinteger(L, 4);
+ lua_setfield(L, -2, "left");
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_EQ(value.parsed().m_top, 3);
+ EXPECT_EQ(value.parsed().m_right, 0);
+ EXPECT_EQ(value.parsed().m_bottom, 0);
+ EXPECT_EQ(value.parsed().m_left, 4);
+
+ lua_createtable(L, 0, 1);
+ lua_pushinteger(L, 100);
+ lua_setfield(L, -2, "top");
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OUT_OF_RANGE);
+
+ lua_createtable(L, 0, 1);
+ lua_pushstring(L, "bad");
+ lua_setfield(L, -2, "top");
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_TYPE);
+}
+
+TEST(ConfigLuaValueTypes, fontWeightParse) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigFontWeight value(400);
+
+ lua_pushstring(L, "bold");
+ auto err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ EXPECT_EQ(value.parsed().m_value, 700);
+
+ lua_pushinteger(L, -1);
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OUT_OF_RANGE);
+
+ lua_pushstring(L, "definitely-not-a-weight");
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE);
+}
+
+TEST(ConfigLuaValueTypes, gradientParseAndPush) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigGradient value(CHyprColor(0xFF000000));
+
+ lua_pushstring(L, "0xFF112233");
+ auto err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ ASSERT_EQ(value.parsed().m_colors.size(), 1);
+
+ lua_createtable(L, 0, 2);
+ lua_createtable(L, 2, 0);
+ lua_pushstring(L, "0xFF0000FF");
+ lua_rawseti(L, -2, 1);
+ lua_pushstring(L, "0xFFFF0000");
+ lua_rawseti(L, -2, 2);
+ lua_setfield(L, -2, "colors");
+ lua_pushnumber(L, 90);
+ lua_setfield(L, -2, "angle");
+
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_OK);
+ ASSERT_EQ(value.parsed().m_colors.size(), 2);
+ EXPECT_NEAR(value.parsed().m_angle, std::numbers::pi_v<float> / 2.F, 0.0001F);
+
+ value.push(L);
+ ASSERT_TRUE(lua_istable(L, -1));
+ lua_getfield(L, -1, "colors");
+ ASSERT_TRUE(lua_istable(L, -1));
+ lua_rawgeti(L, -1, 1);
+ EXPECT_STREQ(lua_tostring(L, -1), "0xFF0000FF");
+ lua_pop(L, 1);
+ lua_pop(L, 1);
+ lua_getfield(L, -1, "angle");
+ EXPECT_NEAR(lua_tonumber(L, -1), 90.0, 0.001);
+ lua_pop(L, 2);
+
+ lua_createtable(L, 0, 0);
+ err = value.parse(L);
+ lua_pop(L, 1);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE);
+}
diff --git a/tests/config/lua/LuaBindingsInternal.cpp b/tests/config/lua/LuaBindingsInternal.cpp
new file mode 100644
index 00000000..d94ea2dc
--- /dev/null
+++ b/tests/config/lua/LuaBindingsInternal.cpp
@@ -0,0 +1,256 @@
+#include <config/lua/bindings/LuaBindingsInternal.hpp>
+#include <config/lua/ConfigManager.hpp>
+
+#include <Compositor.hpp>
+
+#include <config/lua/types/LuaConfigInt.hpp>
+
+#include <gtest/gtest.h>
+
+extern "C" {
+#include <lualib.h>
+#include <lauxlib.h>
+}
+
+using namespace Config::Lua;
+using namespace Config::Lua::Bindings;
+
+namespace Config::Lua {
+ class CConfigManagerPluginLuaTestAccessor {
+ public:
+ static void initializeLuaState(CConfigManager& mgr, lua_State* L) {
+ mgr.m_lua = L;
+ lua_pushlightuserdata(L, &mgr);
+ lua_setfield(L, LUA_REGISTRYINDEX, "hl_lua_manager");
+ }
+ };
+}
+
+namespace {
+ class CLuaState {
+ public:
+ CLuaState() : m_lua(luaL_newstate()) {
+ luaL_openlibs(m_lua);
+ }
+
+ ~CLuaState() {
+ if (m_lua)
+ lua_close(m_lua);
+ }
+
+ lua_State* get() const {
+ return m_lua;
+ }
+
+ private:
+ lua_State* m_lua = nullptr;
+ };
+
+ int testPluginFn(lua_State* L) {
+ lua_pushstring(L, "pong");
+ return 1;
+ }
+}
+
+TEST(ConfigLuaBindingsInternal, parseDirectionAliases) {
+ EXPECT_EQ(Internal::parseDirectionStr("left"), Math::DIRECTION_LEFT);
+ EXPECT_EQ(Internal::parseDirectionStr("l"), Math::DIRECTION_LEFT);
+ EXPECT_EQ(Internal::parseDirectionStr("right"), Math::DIRECTION_RIGHT);
+ EXPECT_EQ(Internal::parseDirectionStr("r"), Math::DIRECTION_RIGHT);
+ EXPECT_EQ(Internal::parseDirectionStr("up"), Math::DIRECTION_UP);
+ EXPECT_EQ(Internal::parseDirectionStr("t"), Math::DIRECTION_UP);
+ EXPECT_EQ(Internal::parseDirectionStr("down"), Math::DIRECTION_DOWN);
+ EXPECT_EQ(Internal::parseDirectionStr("b"), Math::DIRECTION_DOWN);
+ EXPECT_EQ(Internal::parseDirectionStr("???"), Math::DIRECTION_DEFAULT);
+}
+
+TEST(ConfigLuaBindingsInternal, parseToggleAliases) {
+ EXPECT_EQ(Internal::parseToggleStr(""), Config::Actions::TOGGLE_ACTION_TOGGLE);
+ EXPECT_EQ(Internal::parseToggleStr("toggle"), Config::Actions::TOGGLE_ACTION_TOGGLE);
+ EXPECT_EQ(Internal::parseToggleStr("enable"), Config::Actions::TOGGLE_ACTION_ENABLE);
+ EXPECT_EQ(Internal::parseToggleStr("on"), Config::Actions::TOGGLE_ACTION_ENABLE);
+ EXPECT_EQ(Internal::parseToggleStr("disable"), Config::Actions::TOGGLE_ACTION_DISABLE);
+ EXPECT_EQ(Internal::parseToggleStr("off"), Config::Actions::TOGGLE_ACTION_DISABLE);
+}
+
+TEST(ConfigLuaBindingsInternal, argStrConvertsStringsAndNumbers) {
+ CLuaState S;
+ const auto L = S.get();
+
+ lua_pushstring(L, "abc");
+ EXPECT_EQ(Internal::argStr(L, -1), "abc");
+ lua_pop(L, 1);
+
+ lua_pushnumber(L, 42);
+ EXPECT_EQ(Internal::argStr(L, -1), "42");
+ lua_pop(L, 1);
+}
+
+TEST(ConfigLuaBindingsInternal, tableOptHelpersReadOptionalFields) {
+ CLuaState S;
+ const auto L = S.get();
+
+ lua_createtable(L, 0, 5);
+ lua_pushstring(L, "value");
+ lua_setfield(L, -2, "s");
+ lua_pushnumber(L, 5.5);
+ lua_setfield(L, -2, "n");
+ lua_pushboolean(L, true);
+ lua_setfield(L, -2, "b");
+ lua_pushstring(L, "not-number");
+ lua_setfield(L, -2, "n2");
+ lua_pushnil(L);
+ lua_setfield(L, -2, "nilv");
+
+ EXPECT_EQ(Internal::tableOptStr(L, -1, "s").value_or(""), "value");
+ EXPECT_DOUBLE_EQ(Internal::tableOptNum(L, -1, "n").value_or(0), 5.5);
+ EXPECT_EQ(Internal::tableOptBool(L, -1, "b").value_or(false), true);
+ EXPECT_FALSE(Internal::tableOptNum(L, -1, "n2").has_value());
+ EXPECT_FALSE(Internal::tableOptStr(L, -1, "missing").has_value());
+ EXPECT_FALSE(Internal::tableOptBool(L, -1, "nilv").has_value());
+
+ lua_pop(L, 1);
+}
+
+TEST(ConfigLuaBindingsInternal, selectorHelpersAcceptStringAndNumberSelectors) {
+ CLuaState S;
+ const auto L = S.get();
+
+ lua_createtable(L, 0, 4);
+ lua_pushstring(L, "DP-1");
+ lua_setfield(L, -2, "monitor");
+ lua_pushnumber(L, 7);
+ lua_setfield(L, -2, "workspace");
+ lua_pushnumber(L, 1337);
+ lua_setfield(L, -2, "window");
+
+ EXPECT_EQ(Internal::tableOptMonitorSelector(L, -1, "monitor", "test.fn").value_or(""), "DP-1");
+ EXPECT_EQ(Internal::tableOptWorkspaceSelector(L, -1, "workspace", "test.fn").value_or(""), "7");
+ EXPECT_EQ(Internal::tableOptWindowSelector(L, -1, "window", "test.fn").value_or(""), "1337");
+
+ EXPECT_FALSE(Internal::tableOptMonitorSelector(L, -1, "missing", "test.fn").has_value());
+ EXPECT_FALSE(Internal::tableOptWorkspaceSelector(L, -1, "missing", "test.fn").has_value());
+ EXPECT_FALSE(Internal::tableOptWindowSelector(L, -1, "missing", "test.fn").has_value());
+
+ EXPECT_EQ(Internal::requireTableFieldMonitorSelector(L, -1, "monitor", "test.fn"), "DP-1");
+ EXPECT_EQ(Internal::requireTableFieldWorkspaceSelector(L, -1, "workspace", "test.fn"), "7");
+ EXPECT_EQ(Internal::requireTableFieldWindowSelector(L, -1, "window", "test.fn"), "1337");
+
+ lua_pop(L, 1);
+}
+
+TEST(ConfigLuaBindingsInternal, pushWindowUpvalAcceptsNumberAndStringSelectors) {
+ CLuaState S;
+ const auto L = S.get();
+
+ lua_createtable(L, 0, 1);
+ lua_pushnumber(L, 42);
+ lua_setfield(L, -2, "window");
+
+ Internal::pushWindowUpval(L, -1);
+ ASSERT_TRUE(lua_isstring(L, -1));
+ EXPECT_STREQ(lua_tostring(L, -1), "42");
+ lua_pop(L, 1);
+
+ lua_pushstring(L, "0xabc");
+ lua_setfield(L, -2, "window");
+
+ Internal::pushWindowUpval(L, -1);
+ ASSERT_TRUE(lua_isstring(L, -1));
+ EXPECT_STREQ(lua_tostring(L, -1), "0xabc");
+ lua_pop(L, 1);
+
+ lua_pushnil(L);
+ lua_setfield(L, -2, "window");
+
+ Internal::pushWindowUpval(L, -1);
+ EXPECT_TRUE(lua_isnil(L, -1));
+ lua_pop(L, 1);
+
+ lua_pop(L, 1);
+}
+
+TEST(ConfigLuaBindingsInternal, parseTableFieldMissingFieldAndPrefixedErrors) {
+ CLuaState S;
+ const auto L = S.get();
+
+ CLuaConfigInt parser(0);
+
+ lua_newtable(L);
+ auto err = Internal::parseTableField(L, -1, "required", parser);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_VALUE);
+ EXPECT_NE(err.message.find("missing required field"), std::string::npos);
+ lua_pop(L, 1);
+
+ lua_createtable(L, 0, 1);
+ lua_pushstring(L, "bad");
+ lua_setfield(L, -2, "count");
+
+ err = Internal::parseTableField(L, -1, "count", parser);
+ EXPECT_EQ(err.errorCode, PARSE_ERROR_BAD_TYPE);
+ EXPECT_NE(err.message.find("field \"count\":"), std::string::npos);
+ lua_pop(L, 1);
+}
+
+TEST(ConfigLuaBindingsInternal, pluginBindingIsTableWithLoadFunction) {
+ CLuaState S;
+ const auto L = S.get();
+
+ lua_newtable(L);
+ Internal::registerConfigRuleBindings(L, nullptr);
+
+ lua_getfield(L, -1, "plugin");
+ ASSERT_TRUE(lua_istable(L, -1));
+
+ lua_getfield(L, -1, "load");
+ EXPECT_TRUE(lua_isfunction(L, -1));
+ lua_pop(L, 1);
+
+ lua_pop(L, 2);
+}
+
+TEST(ConfigLuaBindingsInternal, pluginLuaFnIsUnloadedWithoutDanglingCall) {
+ CLuaState S;
+ const auto L = S.get();
+
+ auto PREVCOMPOSITOR = std::move(g_pCompositor);
+ g_pCompositor = makeUnique<CCompositor>(true);
+
+ CConfigManager mgr;
+ CConfigManagerPluginLuaTestAccessor::initializeLuaState(mgr, L);
+
+ lua_newtable(L);
+ Internal::registerConfigRuleBindings(L, &mgr);
+ lua_setglobal(L, "hl");
+
+ const auto HANDLE = reinterpret_cast<void*>(0x1BADB002);
+
+ const auto regResult = mgr.registerPluginLuaFunction(HANDLE, "demo", "ping", testPluginFn);
+ ASSERT_TRUE(regResult.has_value()) << regResult.error();
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ local f = hl.plugin.demo.ping
+ assert(type(f) == "function")
+ captured = f
+ local v = f()
+ assert(v == "pong")
+ )"),
+ LUA_OK);
+
+ mgr.onPluginUnload(HANDLE);
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ assert(hl.plugin.demo == nil)
+ )"),
+ LUA_OK);
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ local ok, err = pcall(captured)
+ assert(ok == false)
+ assert(type(err) == "string")
+ assert(string.find(err, "no longer available", 1, true) ~= nil)
+ )"),
+ LUA_OK);
+
+ g_pCompositor = std::move(PREVCOMPOSITOR);
+}
diff --git a/tests/config/lua/LuaConfigUtils.cpp b/tests/config/lua/LuaConfigUtils.cpp
new file mode 100644
index 00000000..a165755e
--- /dev/null
+++ b/tests/config/lua/LuaConfigUtils.cpp
@@ -0,0 +1,105 @@
+#include <config/lua/types/LuaConfigUtils.hpp>
+
+#include <config/lua/types/LuaConfigBool.hpp>
+#include <config/lua/types/LuaConfigColor.hpp>
+#include <config/lua/types/LuaConfigCssGap.hpp>
+#include <config/lua/types/LuaConfigFloat.hpp>
+#include <config/lua/types/LuaConfigFontWeight.hpp>
+#include <config/lua/types/LuaConfigGradient.hpp>
+#include <config/lua/types/LuaConfigInt.hpp>
+#include <config/lua/types/LuaConfigString.hpp>
+#include <config/lua/types/LuaConfigVec2.hpp>
+
+#include <config/values/types/BoolValue.hpp>
+#include <config/values/types/ColorValue.hpp>
+#include <config/values/types/CssGapValue.hpp>
+#include <config/values/types/FloatValue.hpp>
+#include <config/values/types/FontWeightValue.hpp>
+#include <config/values/types/GradientValue.hpp>
+#include <config/values/types/IntValue.hpp>
+#include <config/values/types/StringValue.hpp>
+#include <config/values/types/Vec2Value.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace Config;
+using namespace Config::Lua;
+
+namespace {
+ class CUnsupportedValue : public Values::IValue {
+ public:
+ CUnsupportedValue() : Values::IValue(0) {
+ m_name = "unsupported";
+ m_description = "unsupported";
+ }
+
+ const std::type_info* underlying() const override {
+ return &typeid(void);
+ }
+
+ void commence() override {
+ ;
+ }
+ };
+}
+
+TEST(ConfigLuaUtils, fromGenericValueMapsSupportedTypes) {
+ {
+ auto out = fromGenericValue(makeShared<Values::CIntValue>("a", "", 1));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigInt*>(out.get()), nullptr);
+ }
+ {
+ auto out = fromGenericValue(makeShared<Values::CFloatValue>("a", "", 1.F));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigFloat*>(out.get()), nullptr);
+ }
+ {
+ auto out = fromGenericValue(makeShared<Values::CBoolValue>("a", "", true));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigBool*>(out.get()), nullptr);
+ }
+ {
+ auto out = fromGenericValue(makeShared<Values::CStringValue>("a", "", "x"));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigString*>(out.get()), nullptr);
+ }
+ {
+ auto out = fromGenericValue(makeShared<Values::CColorValue>("a", "", 0xFF000000));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigColor*>(out.get()), nullptr);
+ }
+ {
+ auto out = fromGenericValue(makeShared<Values::CVec2Value>("a", "", VEC2{1, 2}));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigVec2*>(out.get()), nullptr);
+ }
+ {
+ auto out = fromGenericValue(makeShared<Values::CCssGapValue>("a", "", 5));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigCssGap*>(out.get()), nullptr);
+ }
+ {
+ auto out = fromGenericValue(makeShared<Values::CFontWeightValue>("a", "", 500));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigFontWeight*>(out.get()), nullptr);
+ }
+ {
+ auto out = fromGenericValue(makeShared<Values::CGradientValue>("a", "", CHyprColor(0xFF112233)));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_NE(dynamic_cast<CLuaConfigGradient*>(out.get()), nullptr);
+ }
+}
+
+TEST(ConfigLuaUtils, fromGenericValueReturnsNullForUnsupportedType) {
+ auto out = fromGenericValue(makeShared<CUnsupportedValue>());
+ EXPECT_EQ(out.get(), nullptr);
+}
+
+TEST(ConfigLuaUtils, fromGenericValueCopiesRefreshBits) {
+ const auto REFRESH = Config::Supplementary::REFRESH_LAYOUTS | Config::Supplementary::REFRESH_WINDOW_STATES;
+
+ auto out = fromGenericValue(makeShared<Values::CIntValue>("a", "", 1, Values::SIntValueOptions{.refresh = REFRESH}));
+ ASSERT_NE(out.get(), nullptr);
+ EXPECT_EQ(out->refreshBits(), REFRESH);
+}
diff --git a/tests/config/lua/LuaEventHandler.cpp b/tests/config/lua/LuaEventHandler.cpp
new file mode 100644
index 00000000..570014d3
--- /dev/null
+++ b/tests/config/lua/LuaEventHandler.cpp
@@ -0,0 +1,115 @@
+#include <config/lua/LuaEventHandler.hpp>
+
+#include <event/EventBus.hpp>
+
+#include <gtest/gtest.h>
+
+extern "C" {
+#include <lualib.h>
+#include <lauxlib.h>
+}
+
+using namespace Config::Lua;
+
+namespace {
+ class CLuaState {
+ public:
+ CLuaState() : m_lua(luaL_newstate()) {
+ luaL_openlibs(m_lua);
+ }
+
+ ~CLuaState() {
+ if (m_lua)
+ lua_close(m_lua);
+ }
+
+ lua_State* get() const {
+ return m_lua;
+ }
+
+ private:
+ lua_State* m_lua = nullptr;
+ };
+
+ int refGlobalFunction(lua_State* L, const char* name) {
+ lua_getglobal(L, name);
+ EXPECT_TRUE(lua_isfunction(L, -1));
+ return luaL_ref(L, LUA_REGISTRYINDEX);
+ }
+
+ int getGlobalInt(lua_State* L, const char* name) {
+ lua_getglobal(L, name);
+ const int val = sc<int>(lua_tointeger(L, -1));
+ lua_pop(L, 1);
+ return val;
+ }
+}
+
+TEST(ConfigLuaEventHandler, knownEventsContainsExpectedEntries) {
+ const auto& known = CLuaEventHandler::knownEvents();
+ EXPECT_TRUE(known.contains("config.reloaded"));
+ EXPECT_TRUE(known.contains("keybinds.submap"));
+ EXPECT_TRUE(known.contains("window.open"));
+}
+
+TEST(ConfigLuaEventHandler, registerDispatchAndUnregister) {
+ CLuaState S;
+ const auto L = S.get();
+ CLuaEventHandler handler(L);
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ count = 0
+ lastSubmap = ""
+ function onSubmap(name)
+ count = count + 1
+ lastSubmap = name
+ end
+ )"),
+ LUA_OK);
+
+ const int ref = refGlobalFunction(L, "onSubmap");
+ const auto handle = handler.registerEvent("keybinds.submap", ref);
+ ASSERT_TRUE(handle.has_value());
+
+ Event::bus()->m_events.keybinds.submap.emit("main");
+ EXPECT_EQ(getGlobalInt(L, "count"), 1);
+
+ lua_getglobal(L, "lastSubmap");
+ ASSERT_TRUE(lua_isstring(L, -1));
+ EXPECT_STREQ(lua_tostring(L, -1), "main");
+ lua_pop(L, 1);
+
+ EXPECT_TRUE(handler.unregisterEvent(*handle));
+
+ Event::bus()->m_events.keybinds.submap.emit("other");
+ EXPECT_EQ(getGlobalInt(L, "count"), 1);
+}
+
+TEST(ConfigLuaEventHandler, registerUnknownEventReturnsNullopt) {
+ CLuaState S;
+ const auto L = S.get();
+ CLuaEventHandler handler(L);
+
+ ASSERT_EQ(luaL_dostring(L, "function cb() end"), LUA_OK);
+
+ const int ref = refGlobalFunction(L, "cb");
+ const auto handle = handler.registerEvent("totally.unknown", ref);
+
+ EXPECT_FALSE(handle.has_value());
+ luaL_unref(L, LUA_REGISTRYINDEX, ref);
+}
+
+TEST(ConfigLuaEventHandler, callbackLuaErrorDoesNotCrashDispatch) {
+ CLuaState S;
+ const auto L = S.get();
+ CLuaEventHandler handler(L);
+
+ ASSERT_EQ(luaL_dostring(L, "function broken() error('boom') end"), LUA_OK);
+
+ const int ref = refGlobalFunction(L, "broken");
+ const auto h = handler.registerEvent("config.reloaded", ref);
+ ASSERT_TRUE(h.has_value());
+
+ Event::bus()->m_events.config.reloaded.emit();
+ SUCCEED();
+}
diff --git a/tests/config/lua/LuaObjectsBasic.cpp b/tests/config/lua/LuaObjectsBasic.cpp
new file mode 100644
index 00000000..10758b7d
--- /dev/null
+++ b/tests/config/lua/LuaObjectsBasic.cpp
@@ -0,0 +1,266 @@
+#include <config/lua/LuaEventHandler.hpp>
+
+#include <config/lua/objects/LuaEventSubscription.hpp>
+#include <config/lua/objects/LuaKeybind.hpp>
+
+#include <managers/KeybindManager.hpp>
+#include <event/EventBus.hpp>
+
+#include <gtest/gtest.h>
+
+extern "C" {
+#include <lualib.h>
+#include <lauxlib.h>
+}
+
+using namespace Config::Lua;
+
+namespace {
+ class CLuaState {
+ public:
+ CLuaState() : m_lua(luaL_newstate()) {
+ luaL_openlibs(m_lua);
+ }
+
+ ~CLuaState() {
+ if (m_lua)
+ lua_close(m_lua);
+ }
+
+ lua_State* get() const {
+ return m_lua;
+ }
+
+ private:
+ lua_State* m_lua = nullptr;
+ };
+
+ int getGlobalInt(lua_State* L, const char* name) {
+ lua_getglobal(L, name);
+ const int v = sc<int>(lua_tointeger(L, -1));
+ lua_pop(L, 1);
+ return v;
+ }
+
+ bool getGlobalBool(lua_State* L, const char* name) {
+ lua_getglobal(L, name);
+ const bool v = lua_toboolean(L, -1) != 0;
+ lua_pop(L, 1);
+ return v;
+ }
+}
+
+TEST(ConfigLuaObjects, keybindCanToggleEnabledFromLua) {
+ CLuaState S;
+ const auto L = S.get();
+
+ Objects::CLuaKeybind{}.setup(L);
+
+ auto keybind = makeShared<SKeybind>();
+ keybind->enabled = true;
+
+ Objects::CLuaKeybind::push(L, keybind);
+ lua_setglobal(L, "kb");
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ assert(kb:is_enabled() == true)
+ kb:set_enabled(false)
+ assert(kb:is_enabled() == false)
+ )"),
+ LUA_OK);
+
+ EXPECT_FALSE(keybind->enabled);
+}
+
+TEST(ConfigLuaObjects, keybindExposesMetadataAndRemoveMethods) {
+ CLuaState S;
+ const auto L = S.get();
+
+ Objects::CLuaKeybind{}.setup(L);
+
+ auto keybind = makeShared<SKeybind>();
+ keybind->enabled = true;
+ keybind->description = "Close active window";
+ keybind->hasDescription = true;
+ keybind->displayKey = "SUPER + Q";
+ keybind->submap.name = "default";
+ keybind->handler = "exec";
+ keybind->arg = "kitty";
+ keybind->modmask = HL_MODIFIER_META;
+ keybind->key = "Q";
+ keybind->keycode = 24;
+ keybind->repeat = true;
+ keybind->locked = true;
+ keybind->release = false;
+ keybind->nonConsuming = true;
+ keybind->transparent = false;
+ keybind->ignoreMods = false;
+ keybind->longPress = false;
+ keybind->dontInhibit = true;
+ keybind->click = false;
+ keybind->drag = false;
+ keybind->submapUniversal = false;
+ keybind->deviceInclusive = true;
+ keybind->devices = {"kbd-a", "kbd-b"};
+
+ Objects::CLuaKeybind::push(L, keybind);
+ lua_setglobal(L, "kb");
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ assert(kb.enabled == true)
+ assert(kb.description == "Close active window")
+ assert(kb.display_key == "SUPER + Q")
+ assert(kb.submap == "default")
+ assert(kb.handler == "exec")
+ assert(kb.arg == "kitty")
+ assert(kb.modmask ~= nil)
+ assert(kb.key == "Q")
+ assert(kb.keycode == 24)
+ assert(kb.repeating == true)
+ assert(kb.locked == true)
+ assert(kb.non_consuming == true)
+ assert(kb.dont_inhibit == true)
+ assert(type(kb.devices) == "table")
+
+ kb:remove()
+ kb:unbind()
+ )"),
+ LUA_OK);
+}
+
+TEST(ConfigLuaObjects, objectsAreReadOnlyFromLua) {
+ CLuaState S;
+ const auto L = S.get();
+
+ Objects::CLuaKeybind{}.setup(L);
+
+ auto keybind = makeShared<SKeybind>();
+ Objects::CLuaKeybind::push(L, keybind);
+ lua_setglobal(L, "kb");
+
+ EXPECT_NE(luaL_dostring(L, "kb.foo = 1"), LUA_OK);
+ ASSERT_TRUE(lua_isstring(L, -1));
+ EXPECT_NE(std::string(lua_tostring(L, -1)).find("read-only"), std::string::npos);
+ lua_pop(L, 1);
+}
+
+TEST(ConfigLuaObjects, keybindSupportsEqAndToString) {
+ CLuaState S;
+ const auto L = S.get();
+
+ Objects::CLuaKeybind{}.setup(L);
+
+ auto keybindA = makeShared<SKeybind>();
+ auto keybindB = makeShared<SKeybind>();
+
+ Objects::CLuaKeybind::push(L, keybindA);
+ lua_setglobal(L, "kb1");
+
+ Objects::CLuaKeybind::push(L, keybindA);
+ lua_setglobal(L, "kb2");
+
+ Objects::CLuaKeybind::push(L, keybindB);
+ lua_setglobal(L, "kb3");
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ eq12 = (kb1 == kb2)
+ neq13 = (kb1 ~= kb3)
+
+ local s = tostring(kb1)
+ hasPrefix = type(s) == "string" and string.sub(s, 1, 11) == "HL.Keybind("
+ )"),
+ LUA_OK);
+
+ EXPECT_TRUE(getGlobalBool(L, "eq12"));
+ EXPECT_TRUE(getGlobalBool(L, "neq13"));
+ EXPECT_TRUE(getGlobalBool(L, "hasPrefix"));
+}
+
+TEST(ConfigLuaObjects, eventSubscriptionRemoveAndIsActive) {
+ CLuaState S;
+ const auto L = S.get();
+
+ Objects::CLuaEventSubscription{}.setup(L);
+ CLuaEventHandler handler(L);
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ count = 0
+ function onReload()
+ count = count + 1
+ end
+ )"),
+ LUA_OK);
+
+ lua_getglobal(L, "onReload");
+ ASSERT_TRUE(lua_isfunction(L, -1));
+ const int ref = luaL_ref(L, LUA_REGISTRYINDEX);
+
+ const auto handle = handler.registerEvent("config.reloaded", ref);
+ ASSERT_TRUE(handle.has_value());
+
+ Objects::CLuaEventSubscription::push(L, &handler, *handle);
+ lua_setglobal(L, "sub");
+
+ Event::bus()->m_events.config.reloaded.emit();
+ EXPECT_EQ(getGlobalInt(L, "count"), 1);
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ assert(sub:is_active() == true)
+ sub:remove()
+ assert(sub:is_active() == false)
+ sub:remove()
+ )"),
+ LUA_OK);
+
+ Event::bus()->m_events.config.reloaded.emit();
+ EXPECT_EQ(getGlobalInt(L, "count"), 1);
+}
+
+TEST(ConfigLuaObjects, eventSubscriptionSupportsEqAndToString) {
+ CLuaState S;
+ const auto L = S.get();
+
+ Objects::CLuaEventSubscription{}.setup(L);
+ CLuaEventHandler handler(L);
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ function cbA() end
+ function cbB() end
+ )"),
+ LUA_OK);
+
+ lua_getglobal(L, "cbA");
+ ASSERT_TRUE(lua_isfunction(L, -1));
+ const int refA = luaL_ref(L, LUA_REGISTRYINDEX);
+
+ lua_getglobal(L, "cbB");
+ ASSERT_TRUE(lua_isfunction(L, -1));
+ const int refB = luaL_ref(L, LUA_REGISTRYINDEX);
+
+ const auto handleA = handler.registerEvent("config.reloaded", refA);
+ const auto handleB = handler.registerEvent("config.reloaded", refB);
+ ASSERT_TRUE(handleA.has_value());
+ ASSERT_TRUE(handleB.has_value());
+
+ Objects::CLuaEventSubscription::push(L, &handler, *handleA);
+ lua_setglobal(L, "sub1");
+
+ Objects::CLuaEventSubscription::push(L, &handler, *handleA);
+ lua_setglobal(L, "sub2");
+
+ Objects::CLuaEventSubscription::push(L, &handler, *handleB);
+ lua_setglobal(L, "sub3");
+
+ ASSERT_EQ(luaL_dostring(L, R"(
+ eq12 = (sub1 == sub2)
+ neq13 = (sub1 ~= sub3)
+
+ local s = tostring(sub1)
+ hasPrefix = type(s) == "string" and string.sub(s, 1, 21) == "HL.EventSubscription("
+ )"),
+ LUA_OK);
+
+ EXPECT_TRUE(getGlobalBool(L, "eq12"));
+ EXPECT_TRUE(getGlobalBool(L, "neq13"));
+ EXPECT_TRUE(getGlobalBool(L, "hasPrefix"));
+}
diff --git a/tests/helpers/MathTransform.cpp b/tests/helpers/MathTransform.cpp
index 686721b9..fe935d16 100644
--- a/tests/helpers/MathTransform.cpp
+++ b/tests/helpers/MathTransform.cpp
@@ -1,4 +1,5 @@
#include <helpers/math/Math.hpp>
+#include <helpers/memory/Memory.hpp>
#include <gtest/gtest.h>
@@ -19,7 +20,7 @@ TEST(Helpers, mathWlTransformToHyprutils) {
TEST(Helpers, mathWlTransformToHyprutilsInvalid) {
// Invalid value falls back to NORMAL
- EXPECT_EQ(wlTransformToHyprutils(static_cast<wl_output_transform>(99)), eTransform::HYPRUTILS_TRANSFORM_NORMAL);
+ EXPECT_EQ(wlTransformToHyprutils(sc<wl_output_transform>(99)), eTransform::HYPRUTILS_TRANSFORM_NORMAL);
}
// invertTransform
@@ -47,7 +48,7 @@ TEST(Helpers, mathInvertTransformFlippedRotated) {
TEST(Helpers, mathInvertTransformDoubleInvert) {
// Double invert returns original for all transforms
for (int i = 0; i <= 7; i++) {
- auto t = static_cast<wl_output_transform>(i);
+ auto t = sc<wl_output_transform>(i);
EXPECT_EQ(invertTransform(invertTransform(t)), t);
}
}
@@ -57,7 +58,7 @@ TEST(Helpers, mathInvertTransformDoubleInvert) {
TEST(Helpers, mathComposeTransformIdentity) {
// Composing with NORMAL is identity
for (int i = 0; i <= 7; i++) {
- auto t = static_cast<eTransform>(i);
+ auto t = sc<eTransform>(i);
EXPECT_EQ(composeTransform(t, eTransform::HYPRUTILS_TRANSFORM_NORMAL), t);
EXPECT_EQ(composeTransform(eTransform::HYPRUTILS_TRANSFORM_NORMAL, t), t);
}