From 2c260ed206919ae2e1625295fcf98877beaa1e9a Mon Sep 17 00:00:00 2001 From: sfan5 Date: Mon, 19 May 2025 17:41:13 +0200 Subject: [PATCH] Add unit tests for Lua vector reading --- builtin/common/strict.lua | 17 ++- games/devtest/mods/unittests/misc.lua | 12 -- src/unittest/CMakeLists.txt | 1 + src/unittest/test.h | 4 +- src/unittest/test_scriptapi.cpp | 183 ++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 20 deletions(-) create mode 100644 src/unittest/test_scriptapi.cpp diff --git a/builtin/common/strict.lua b/builtin/common/strict.lua index 9bfa8d7a2..b3c4ccce4 100644 --- a/builtin/common/strict.lua +++ b/builtin/common/strict.lua @@ -19,12 +19,14 @@ function meta:__newindex(name, value) return end local info = getinfo(2, "Sl") - local desc = ("%s:%d"):format(info.short_src, info.currentline) - local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name) - if not warned[warn_key] and info.what ~= "main" and info.what ~= "C" then - core.log("warning", ("Assignment to undeclared global %q inside a function at %s.") - :format(name, desc)) - warned[warn_key] = true + if info ~= nil then + local desc = ("%s:%d"):format(info.short_src, info.currentline) + local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name) + if not warned[warn_key] and info.what ~= "main" and info.what ~= "C" then + core.log("warning", ("Assignment to undeclared global %q inside a function at %s.") + :format(name, desc)) + warned[warn_key] = true + end end declared[name] = true end @@ -35,6 +37,9 @@ function meta:__index(name) return end local info = getinfo(2, "Sl") + if info == nil then + return + end local warn_key = ("%s\0%d\0%s"):format(info.source, info.currentline, name) if not warned[warn_key] and info.what ~= "C" then core.log("warning", ("Undeclared global variable %q accessed at %s:%s") diff --git a/games/devtest/mods/unittests/misc.lua b/games/devtest/mods/unittests/misc.lua index 65dc3259e..28cc2c1eb 100644 --- a/games/devtest/mods/unittests/misc.lua +++ b/games/devtest/mods/unittests/misc.lua @@ -67,18 +67,6 @@ local function test_dynamic_media(cb, player) end unittests.register("test_dynamic_media", test_dynamic_media, {async=true, player=true}) -local function test_v3f_metatable(player) - assert(vector.check(player:get_pos())) -end -unittests.register("test_v3f_metatable", test_v3f_metatable, {player=true}) - -local function test_v3s16_metatable(player, pos) - local node = core.get_node(pos) - local found_pos = core.find_node_near(pos, 0, node.name, true) - assert(vector.check(found_pos)) -end -unittests.register("test_v3s16_metatable", test_v3s16_metatable, {map=true}) - local function test_clear_meta(_, pos) local ref = core.get_meta(pos) diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt index 9ac275d7f..2a38af68d 100644 --- a/src/unittest/CMakeLists.txt +++ b/src/unittest/CMakeLists.txt @@ -36,6 +36,7 @@ set (UNITTEST_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/test_random.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_sao.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_schematic.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_scriptapi.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_serialization.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_serveractiveobjectmgr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_server_shutdown_state.cpp diff --git a/src/unittest/test.h b/src/unittest/test.h index dcecb9fb4..7e861a24a 100644 --- a/src/unittest/test.h +++ b/src/unittest/test.h @@ -41,7 +41,7 @@ public: #define UTEST(x, fmt, ...) \ if (!(x)) { \ char utest_buf[1024]; \ - snprintf(utest_buf, sizeof(utest_buf), fmt, __VA_ARGS__); \ + porting::mt_snprintf(utest_buf, sizeof(utest_buf), fmt, __VA_ARGS__); \ throw TestFailedException(utest_buf, __FILE__, __LINE__); \ } @@ -68,7 +68,7 @@ public: } catch (EType &e) { \ exception_thrown = true; \ } \ - UASSERT(exception_thrown); \ + UTEST(exception_thrown, "Exception %s not thrown", #EType); \ } class IGameDef; diff --git a/src/unittest/test_scriptapi.cpp b/src/unittest/test_scriptapi.cpp new file mode 100644 index 000000000..4fc03e6c5 --- /dev/null +++ b/src/unittest/test_scriptapi.cpp @@ -0,0 +1,183 @@ +// Luanti +// SPDX-License-Identifier: LGPL-2.1-or-later +// Copyright (C) 2022 Minetest core developers & community + +#include "test.h" + +#include +#include "script/cpp_api/s_base.h" +#include "script/lua_api/l_util.h" +#include "script/lua_api/l_settings.h" +#include "script/common/c_converter.h" +#include "irrlicht_changes/printing.h" +#include "server.h" + +namespace { + class MyScriptApi : virtual public ScriptApiBase { + public: + MyScriptApi() : ScriptApiBase(ScriptingType::Async) {}; + void init(); + using ScriptApiBase::getStack; + }; +} + +class TestScriptApi : public TestBase +{ +public: + TestScriptApi() { TestManager::registerTestModule(this); } + const char *getName() { return "TestScriptApi"; } + + void runTests(IGameDef *gamedef); + + void testVectorMetatable(MyScriptApi *script); + void testVectorRead(MyScriptApi *script); + void testVectorReadErr(MyScriptApi *script); + void testVectorReadMix(MyScriptApi *script); +}; + +static TestScriptApi g_test_instance; + +void MyScriptApi::init() +{ + lua_State *L = getStack(); + + lua_getglobal(L, "core"); + int top = lua_gettop(L); + + // By creating an environment of 'async' type we have the fewest amount + // of external classes needed. + lua_pushstring(L, "async"); + lua_setglobal(L, "INIT"); + + LuaSettings::Register(L); + ModApiUtil::InitializeAsync(L, top); + + lua_pop(L, 1); + + loadMod(Server::getBuiltinLuaPath() + DIR_DELIM + "init.lua", BUILTIN_MOD_NAME); + checkSetByBuiltin(); +} + +void TestScriptApi::runTests(IGameDef *gamedef) +{ + MyScriptApi script; + try { + script.init(); + } catch (ModError &e) { + rawstream << e.what() << std::endl; + num_tests_failed = 1; + return; + } + + TEST(testVectorMetatable, &script); + TEST(testVectorRead, &script); + TEST(testVectorReadErr, &script); + TEST(testVectorReadMix, &script); +} + +// Runs Lua code and leaves `nresults` return values on the stack +static void run(lua_State *L, const char *code, int nresults) +{ + if (luaL_loadstring(L, code) != 0) { + rawstream << lua_tostring(L, -1) << std::endl; + UASSERT(false); + } + if (lua_pcall(L, 0, nresults, 0) != 0) { + throw LuaError(lua_tostring(L, -1)); + } +} + +void TestScriptApi::testVectorMetatable(MyScriptApi *script) +{ + lua_State *L = script->getStack(); + StackUnroller unroller(L); + + const auto &call_vector_check = [&] () -> bool { + lua_setglobal(L, "tmp"); + run(L, "return vector.check(tmp)", 1); + return lua_toboolean(L, -1); + }; + + push_v3s16(L, {1, 2, 3}); + UASSERT(call_vector_check()); + + push_v3f(L, {1, 2, 3}); + UASSERT(call_vector_check()); + + // 2-component vectors must not have this metatable + push_v2s32(L, {0, 0}); + UASSERT(!call_vector_check()); + + push_v2f(L, {0, 0}); + UASSERT(!call_vector_check()); +} + +void TestScriptApi::testVectorRead(MyScriptApi *script) +{ + lua_State *L = script->getStack(); + StackUnroller unroller(L); + + // both methods should parse these + const std::pair pairs1[] = { + {"return {x=1, y=-2, z=3}", {1, -2, 3}}, + {"return {x=1.1, y=0, z=0}", {1, 0, 0}}, + {"return {x=1.5, y=0, z=0}", {2, 0, 0}}, + {"return {x=-1.1, y=0, z=0}", {-1, 0, 0}}, + {"return {x=-1.5, y=0, z=0}", {-2, 0, 0}}, + {"return vector.new(5, 6, 7)", {5, 6, 7}}, + {"return vector.new(32767, 0, -32768)", {S16_MAX, 0, S16_MIN}}, + }; + for (auto &it : pairs1) { + run(L, it.first, 1); + v3s16 v = read_v3s16(L, -1); + UASSERTEQ(auto, v, it.second); + v = check_v3s16(L, -1); + UASSERTEQ(auto, v, it.second); + lua_pop(L, 1); + } +} + +void TestScriptApi::testVectorReadErr(MyScriptApi *script) +{ + lua_State *L = script->getStack(); + StackUnroller unroller(L); + + // both methods should reject these + const char *errs1[] = { + "return {y=1, z=3}", + "return {x=1, z=3}", + "return {x=1, y=3}", + "return {}", + "return 'bamboo'", + "return function() end", + "return nil", + }; + for (auto &it : errs1) { + infostream << it << std::endl; + run(L, it, 1); + EXCEPTION_CHECK(LuaError, read_v3s16(L, -1)); + EXCEPTION_CHECK(LuaError, check_v3s16(L, -1)); + } +} + +void TestScriptApi::testVectorReadMix(MyScriptApi *script) +{ + lua_State *L = script->getStack(); + StackUnroller unroller(L); + + // read_v3s16 should allow these, but check_v3s16 should not + const std::pair pairs2[] = { + {"return {x='3', y='2.9', z=3}", {3, 3, 3}}, + {"return {x=false, y=0, z=0}", {0, 0, 0}}, + {"return {x='?', y=0, z=0}", {0, 0, 0}}, + {"return {x={'baguette'}, y=0, z=0}", {0, 0, 0}}, + }; + for (auto &it : pairs2) { + infostream << it.first << std::endl; + run(L, it.first, 1); + v3s16 v = read_v3s16(L, -1); + UASSERTEQ(auto, v, it.second); + EXCEPTION_CHECK(LuaError, check_v3s16(L, -1)); + lua_pop(L, 1); + } +}