diff --git a/.zed/debug.json b/.zed/debug.json index d53ea53..89041ea 100644 --- a/.zed/debug.json +++ b/.zed/debug.json @@ -13,5 +13,16 @@ "program": "$ZED_WORKTREE_ROOT/build/bin/editor", "request": "launch", "adapter": "CodeLLDB" + }, + { + "label": "Debug tests", + "build": { + "command": "cmake", + "args": ["--build", "build", "-j16"], + "cwd": "$ZED_WORKTREE_ROOT" + }, + "program": "$ZED_WORKTREE_ROOT/build/bin/obtest", + "request": "launch", + "adapter": "CodeLLDB" } ] diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ea664f..df56b62 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,4 @@ add_subdirectory(core) add_subdirectory(client) add_subdirectory(editor) - -set( CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/tests ) -enable_testing() add_subdirectory(tests) diff --git a/Testing/Temporary/LastTest.log b/Testing/Temporary/LastTest.log new file mode 100644 index 0000000..97259ec --- /dev/null +++ b/Testing/Temporary/LastTest.log @@ -0,0 +1,3 @@ +Start testing: Dec 11 22:10 CET +---------------------------------------------------------- +End testing: Dec 11 22:10 CET diff --git a/core/src/objects/service/script/scriptcontext.cpp b/core/src/objects/service/script/scriptcontext.cpp index 8ef7a45..b9ea5ae 100644 --- a/core/src/objects/service/script/scriptcontext.cpp +++ b/core/src/objects/service/script/scriptcontext.cpp @@ -137,6 +137,7 @@ void ScriptContext::RunSleepingThreads() { for (i = 0; i < sleepingThreads.size();) { bool deleted = false; + // TODO: Remove threads that belong to non-existent scripts SleepingThread sleep = sleepingThreads[i]; if (tu_clock_micros() >= sleep.targetTimeMicros) { // Time args @@ -167,6 +168,12 @@ void ScriptContext::RunSleepingThreads() { schedTime = tu_clock_micros() - startTime; } +// Temporary stopgap until RunSleepingThreads can clear threads that belong to +// scripts no longer parented to the DataModel +void ScriptContext::DebugClearSleepingThreads() { + sleepingThreads.clear(); +} + void ScriptContext::NewEnvironment(lua_State* L) { lua_newtable(L); // Env table lua_newtable(L); // Metatable diff --git a/core/src/objects/service/script/scriptcontext.h b/core/src/objects/service/script/scriptcontext.h index 1200081..d5da29a 100644 --- a/core/src/objects/service/script/scriptcontext.h +++ b/core/src/objects/service/script/scriptcontext.h @@ -32,6 +32,8 @@ public: lua_State* state; void PushThreadSleep(lua_State* thread, float delay); void RunSleepingThreads(); + // TEMPORARY. USED ONLY FOR TESTING + void DebugClearSleepingThreads(); // Generates an environment with a metatable and pushes it both the env table and metatable in order onto the stack void NewEnvironment(lua_State* state); diff --git a/justfile b/justfile new file mode 100644 index 0000000..bbd802d --- /dev/null +++ b/justfile @@ -0,0 +1,22 @@ +help: + just -l + +configure: + cmake -Bbuild -DCMAKE_BUILD_TYPE=Debug . + +# Commented out configure because it takes unnecessarily long +# Just run configure manually if you've made any changes +build: #configure + cmake --build build -j$(nproc) + +editor: build + ./build/bin/editor + +test: build + ctest --test-dir=build + +test-v: build + ctest --test-dir=build --rerun-failed --output-on-failure + +test-dbg: build + gdb -q ./build/bin/obtest \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 47af85b..ac7fcd8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,14 +1,12 @@ -function (create_test TEST_NAME) - set(TARGET_NAME test_${TEST_NAME}) - add_executable(${TARGET_NAME} ${ARGN}) - target_link_libraries(${TARGET_NAME} PRIVATE openblocks) - add_dependencies(${TARGET_NAME} openblocks) - add_test(NAME ${TARGET_NAME} COMMAND ${TARGET_NAME}) -endfunction () -create_test(lua src/luatest.cpp) -create_test(luasched src/luaschedtest.cpp) -create_test(luasignal src/luasignaltest.cpp) +include(${CMAKE_CURRENT_SOURCE_DIR}/deps.cmake) -# https://stackoverflow.com/a/36729074/16255372 -add_custom_target(check ${CMAKE_CTEST_COMMAND} --output-on-failure WORKING_DIRECTORY ${CMAKE_BINARY_DIR}) \ No newline at end of file +add_executable(obtest + src/common.cpp + src/lua/luasched.cpp + src/lua/luasignal.cpp + src/lua/luageneric.cpp +) +target_link_libraries(obtest PRIVATE openblocks Catch2::Catch2WithMain) +target_include_directories(obtest PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) +catch_discover_tests(obtest) \ No newline at end of file diff --git a/tests/deps.cmake b/tests/deps.cmake new file mode 100644 index 0000000..19650d2 --- /dev/null +++ b/tests/deps.cmake @@ -0,0 +1,8 @@ + +include(CPM) + +CPMAddPackage("gh:catchorg/Catch2@3.8.1") + +list(APPEND CMAKE_MODULE_PATH ${Catch2_SOURCE_DIR}/extras) +include(CTest) +include(Catch) diff --git a/tests/src/common.cpp b/tests/src/common.cpp new file mode 100644 index 0000000..a9069e5 --- /dev/null +++ b/tests/src/common.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include "logger.h" +#include "objects/base/instance.h" +#include "objects/service/script/scriptcontext.h" +#include "objects/service/script/serverscriptservice.h" +#include "objects/service/workspace.h" +#include "testcommon.h" + +std::shared_ptr gTestModel; +std::stringstream testLogOutput; + +class commonTestListener : public Catch::EventListenerBase { +public: + using Catch::EventListenerBase::EventListenerBase; + + void testRunStarting(Catch::TestRunInfo const&) override { + // TODO: Make physicsInit optional in headless environments + physicsInit(); + + gTestModel = DataModel::New(); + gTestModel->Init(true); + Logger::initTest(&testLogOutput); + } + + void testRunEnded(Catch::TestRunStats const&) override { + gTestModel = nullptr; + physicsDeinit(); + Logger::initTest(nullptr); + } + + void testCasePartialStarting(const Catch::TestCaseInfo &testInfo, uint64_t partNumber) override { + // Clear the log output prior to each test + testLogOutput.str(""); + } + + void testCasePartialEnded(const Catch::TestCaseStats &testCaseStats, uint64_t partNumber) override { + auto ctx = gTestModel->GetService(); + ctx->DebugClearSleepingThreads(); + + // Clean up remaining scripts from ServerScriptService + for (auto& obj : gTestModel->GetService()->GetChildren()) { + obj->Destroy(); + } + + // Also clear workspace + for (auto& obj : gTestModel->GetService()->GetChildren()) { + obj->Destroy(); + } + } +}; + +CATCH_REGISTER_LISTENER(commonTestListener) \ No newline at end of file diff --git a/tests/src/lua/luageneric.cpp b/tests/src/lua/luageneric.cpp new file mode 100644 index 0000000..3365d31 --- /dev/null +++ b/tests/src/lua/luageneric.cpp @@ -0,0 +1,20 @@ +#include + +#include "testcommon.h" +#include "testutil.h" + +TEST_CASE("Generic lua test", "[luageneric]") { + auto m = gTestModel; + + SECTION("Script output") { + REQUIRE(luaEvalOut(m, "print('Hello, world!')") == "INFO: Hello, world!\n"); + } + + // SECTION("Script warning") { + // REQUIRE(luaEvalOut(m, "warn('Some warning here.')") == "WARN: Some warning here.\n"); + // } + + // SECTION("Script error") { + // REQUIRE(luaEvalOut(m, "error('An error!')") == "ERROR: An error!.\n"); + // } +} \ No newline at end of file diff --git a/tests/src/lua/luasched.cpp b/tests/src/lua/luasched.cpp new file mode 100644 index 0000000..c9949d1 --- /dev/null +++ b/tests/src/lua/luasched.cpp @@ -0,0 +1,81 @@ +#include + +#include "objects/service/script/scriptcontext.h" +#include "testcommon.h" +#include "testutil.h" +#include "timeutil.h" + +static auto& m = gTestModel; +static auto& out = testLogOutput; + +TEST_CASE("Wait with delay") { + auto ctx = m->GetService(); + + tu_set_override(0); + luaEval(m, "wait(1) print('Wait')"); + + SECTION("Empty output at 0s") { + ctx->RunSleepingThreads(); + REQUIRE(out.str() == ""); + } + + SECTION("Empty output at 0.5s") { + TT_ADVANCETIME(0.5); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == ""); + } + + SECTION("Print output at 1s") { + TT_ADVANCETIME(1); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == "INFO: Wait\n"); + } +} + +TEST_CASE("Wait with minimum delay") { + auto ctx = m->GetService(); + + tu_set_override(0); + luaEval(m, "wait(0) print('Wait')"); + + SECTION("Empty output at 0s") { + ctx->RunSleepingThreads(); + REQUIRE(out.str() == ""); + } + + SECTION("Empty output at 0.02s") { + TT_ADVANCETIME(0.02); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == ""); + } + + SECTION("Print output at 0.03s") { + TT_ADVANCETIME(0.03); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == "INFO: Wait\n"); + } +} + +TEST_CASE("Run callback after delay") { + auto ctx = m->GetService(); + + tu_set_override(0); + luaEval(m, "delay(1, function() print('Delay') end)"); + + SECTION("Empty output at 0s") { + ctx->RunSleepingThreads(); + REQUIRE(out.str() == ""); + } + + SECTION("Empty output at 0.5s") { + TT_ADVANCETIME(0.5); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == ""); + } + + SECTION("Print output at 1s") { + TT_ADVANCETIME(1); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == "INFO: Delay\n"); + } +} diff --git a/tests/src/lua/luasignal.cpp b/tests/src/lua/luasignal.cpp new file mode 100644 index 0000000..645fdac --- /dev/null +++ b/tests/src/lua/luasignal.cpp @@ -0,0 +1,79 @@ +#include + +#include "objects/part/part.h" +#include "objects/service/script/scriptcontext.h" +#include "objects/service/workspace.h" +#include "testcommon.h" +#include "testutil.h" +#include "timeutil.h" + +static auto& m = gTestModel; +static auto& out = testLogOutput; + +TEST_CASE("Connect to event") { + auto ws = m->GetService(); + auto part = Part::New(); + ws->AddChild(part); + + luaEval(m, "workspace.Part.Touched:Connect(function() print('Fired!') end)"); + + SECTION("Single fire") { + part->Touched->Fire(); + REQUIRE(out.str() == "INFO: Fired!\n"); + } + + SECTION("Double fire") { + part->Touched->Fire(); + part->Touched->Fire(); + REQUIRE(out.str() == "INFO: Fired!\nINFO: Fired!\n"); + } +} + +TEST_CASE("Wait within event listener") { + auto ctx = m->GetService(); + auto ws = m->GetService(); + auto part = Part::New(); + ws->AddChild(part); + + tu_set_override(0); + luaEval(m, "workspace.Part.Touched:Connect(function() print('Fired!') wait(1) print('Waited') end)"); + + SECTION("Single fire") { + part->Touched->Fire(); + REQUIRE(out.str() == "INFO: Fired!\n"); + TT_ADVANCETIME(0.5); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == "INFO: Fired!\n"); + TT_ADVANCETIME(0.5); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == "INFO: Fired!\nINFO: Waited\n"); + } + + SECTION("Nested double fire") { + part->Touched->Fire(); + TT_ADVANCETIME(0.2); + part->Touched->Fire(); + REQUIRE(out.str() == "INFO: Fired!\nINFO: Fired!\n"); + TT_ADVANCETIME(1-0.2); // Small extra delay is necessary because floating point math + ctx->RunSleepingThreads(); + REQUIRE(out.str() == "INFO: Fired!\nINFO: Fired!\nINFO: Waited\n"); + TT_ADVANCETIME(0.2); + ctx->RunSleepingThreads(); + REQUIRE(out.str() == "INFO: Fired!\nINFO: Fired!\nINFO: Waited\nINFO: Waited\n"); + } +} + +TEST_CASE("Wait for event") { + auto ctx = m->GetService(); + auto ws = m->GetService(); + auto part = Part::New(); + ws->AddChild(part); + + tu_set_override(0); + luaEval(m, "workspace.Part.Touched:Wait() print('Fired!')"); + + part->Touched->Fire(); + REQUIRE(out.str() == "INFO: Fired!\n"); + part->Touched->Fire(); // Firing again should not affect output + REQUIRE(out.str() == "INFO: Fired!\n"); +} diff --git a/tests/src/luaschedtest.cpp b/tests/src/luaschedtest.cpp deleted file mode 100644 index 2c1825c..0000000 --- a/tests/src/luaschedtest.cpp +++ /dev/null @@ -1,77 +0,0 @@ -#include "testutillua.h" - -#include "timeutil.h" - -void test_wait1(DATAMODEL_REF m) { - auto ctx = m->GetService(); - std::stringstream out; - Logger::initTest(&out); - - tu_set_override(0); - luaEval(m, "wait(1) print('Wait')"); - - ctx->RunSleepingThreads(); - ASSERT_EQ("", out.str()); - - TT_ADVANCETIME(0.5); - ctx->RunSleepingThreads(); - ASSERT_EQ("", out.str()); - - TT_ADVANCETIME(0.5); - ctx->RunSleepingThreads(); - ASSERT_EQ("INFO: Wait\n", out.str()); - - Logger::initTest(nullptr); -} - -void test_wait0(DATAMODEL_REF m) { - auto ctx = m->GetService(); - std::stringstream out; - Logger::initTest(&out); - - tu_set_override(0); - luaEval(m, "wait(0) print('Wait')"); - ASSERT_EQ("", out.str()); - - ctx->RunSleepingThreads(); - ASSERT_EQ("", out.str()); - - TT_ADVANCETIME(0.03); - ctx->RunSleepingThreads(); - ASSERT_EQ("INFO: Wait\n", out.str()); - - Logger::initTest(nullptr); -} - -void test_delay(DATAMODEL_REF m) { - auto ctx = m->GetService(); - std::stringstream out; - Logger::initTest(&out); - - tu_set_override(0); - luaEval(m, "delay(1, function() print('Delay') end)"); - - ctx->RunSleepingThreads(); - ASSERT_EQ("", out.str()); - - TT_ADVANCETIME(0.5); - ctx->RunSleepingThreads(); - ASSERT_EQ("", out.str()); - - TT_ADVANCETIME(0.5); - ctx->RunSleepingThreads(); - ASSERT_EQ("INFO: Delay\n", out.str()); - - Logger::initTest(nullptr); -} - -int main() { - auto m = DataModel::New(); - m->Init(true); - - test_wait1(m); - test_wait0(m); - test_delay(m); - - return TEST_STATUS; -} \ No newline at end of file diff --git a/tests/src/luasignaltest.cpp b/tests/src/luasignaltest.cpp deleted file mode 100644 index 00342f7..0000000 --- a/tests/src/luasignaltest.cpp +++ /dev/null @@ -1,103 +0,0 @@ - -#include "testutil.h" -#include "testutillua.h" - -#include "timeutil.h" -#include "objects/part/part.h" -#include "objects/service/workspace.h" -#include -#include - -void test_connect(DATAMODEL_REF m) { - auto ctx = m->GetService(); - auto part = Part::New(); - m->GetService()->AddChild(part); - std::stringstream out; - Logger::initTest(&out); - - luaEval(m, "workspace.Part.Touched:Connect(function() print('Fired!') end)"); - ASSERT_EQ("", out.str()); - - part->Touched->Fire(); - ASSERT_EQ("INFO: Fired!\n", out.str()); - part->Touched->Fire(); - ASSERT_EQ("INFO: Fired!\nINFO: Fired!\n", out.str()); - - Logger::initTest(nullptr); - part->Destroy(); -} - -void test_waitwithin(DATAMODEL_REF m) { - auto ctx = m->GetService(); - auto part = Part::New(); - m->GetService()->AddChild(part); - std::stringstream out; - Logger::initTest(&out); - - tu_set_override(0); - luaEval(m, "workspace.Part.Touched:Connect(function() print('Fired!') wait(1) print('Waited') end)"); - ASSERT_EQ("", out.str()); - - // One shot - part->Touched->Fire(); - ctx->RunSleepingThreads(); - ASSERT_EQ("INFO: Fired!\n", out.str()); - TT_ADVANCETIME(0.5); - ctx->RunSleepingThreads(); - ASSERT_EQ("INFO: Fired!\n", out.str()); - TT_ADVANCETIME(0.5); - ctx->RunSleepingThreads(); - ASSERT_EQ("INFO: Fired!\nINFO: Waited\n", out.str()); - - // Clear - out = std::stringstream(); - Logger::initTest(&out); // Shouldn't *theoretically* be necessary, but just in principle... - - // Double fire - part->Touched->Fire(); - TT_ADVANCETIME(0.2); - part->Touched->Fire(); - ASSERT_EQ("INFO: Fired!\nINFO: Fired!\n", out.str()); - TT_ADVANCETIME(1-0.2); // Small extra delay is necessary because floating point math - ctx->RunSleepingThreads(); - ASSERT_EQ("INFO: Fired!\nINFO: Fired!\nINFO: Waited\n", out.str()); - TT_ADVANCETIME(0.2); - ctx->RunSleepingThreads(); - ASSERT_EQ("INFO: Fired!\nINFO: Fired!\nINFO: Waited\nINFO: Waited\n", out.str()); - - tu_set_override(-1UL); - Logger::initTest(nullptr); - part->Destroy(); -} - -void test_await(DATAMODEL_REF m) { - auto ctx = m->GetService(); - auto part = Part::New(); - m->GetService()->AddChild(part); - std::stringstream out; - Logger::initTest(&out); - - tu_set_override(0); - luaEval(m, "workspace.Part.Touched:Wait() print('Fired!')"); - ASSERT_EQ("", out.str()); - - part->Touched->Fire(); - ASSERT_EQ("INFO: Fired!\n", out.str()); - part->Touched->Fire(); // Firing again should not affect output - ASSERT_EQ("INFO: Fired!\n", out.str()); - - tu_set_override(-1UL); - Logger::initTest(nullptr); - part->Destroy(); -} - -int main() { - auto m = DataModel::New(); - m->Init(true); - - test_connect(m); - test_waitwithin(m); - test_await(m); - - return TEST_STATUS; -} \ No newline at end of file diff --git a/tests/src/luatest.cpp b/tests/src/luatest.cpp deleted file mode 100644 index 29b6252..0000000 --- a/tests/src/luatest.cpp +++ /dev/null @@ -1,20 +0,0 @@ -#include "testutil.h" -#include "testutillua.h" - -#include - - -void test_output(DATAMODEL_REF m) { - ASSERT_EQ("INFO: Hello, world!\n", luaEvalOut(m, "print('Hello, world!')")); - // ASSERT_EQ("WARN: Some warning here.\n", luaEvalOut(m, "warn('Some warning here.')")); - // ASSERT_EQ("ERROR: An error!.\n", luaEvalOut(m, "error('An error!')")); -} - -int main() { - auto m = DataModel::New(); - m->Init(true); - - test_output(m); - - return TEST_STATUS; -} \ No newline at end of file diff --git a/tests/src/testcommon.h b/tests/src/testcommon.h new file mode 100644 index 0000000..cc5b77c --- /dev/null +++ b/tests/src/testcommon.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +#include "objects/datamodel.h" + +extern std::shared_ptr gTestModel; +extern std::stringstream testLogOutput; \ No newline at end of file diff --git a/tests/src/testutil.h b/tests/src/testutil.h index 64c8a43..d26f52f 100644 --- a/tests/src/testutil.h +++ b/tests/src/testutil.h @@ -1,70 +1,31 @@ #pragma once -// https://bastian.rieck.me/blog/2017/simple_unit_tests/ - -#include -#include -#include -#include -#include -#include - -#ifdef __FUNCTION__ -#define __FUNC_NAME __FUNCTION__ -#else -#define __FUNC_NAME __func__ -#endif - -#define ASSERT(x, msg) __assert((x), __FILE__, __LINE__, __FUNC_NAME, msg) -#define ASSERT_EQ(x, y) __assert_eq((x) == (y), __FILE__, __LINE__, __FUNC_NAME, #x, (y)) -// #define ASSERT_EQSTR(x, y) ASSERT(strcmp(x, y) == 0, #x " != " #y) -#define ASSERT_EQSTR(x, y) ASSERT_EQ(x, y) - -#define DATAMODEL_REF std::shared_ptr +#include "objects/datamodel.h" +#include "objects/script.h" +#include "objects/service/script/serverscriptservice.h" +#include "testcommon.h" #define TU_TIME_EXPOSE_TEST #define TT_ADVANCETIME(secs) tu_set_override(tu_clock_micros() + (secs) * 1'000'000); -#include -#include +inline std::string luaEvalOut(std::shared_ptr m, std::string source) { + testLogOutput.seekp(0, std::ios::end); + size_t offset = testLogOutput.tellp(); + testLogOutput.seekp(0); -int TEST_STATUS = 0; + auto ss = m->GetService(); + auto s = Script::New(); + m->AddChild(s); + s->source = source; + s->Run(); -inline void __assert(bool cond, std::string file, int line, std::string func, std::string message) { - if (cond) return; - fprintf(stderr, "ASSERT FAILED : %s:%d : %s : '%s'\n", file.c_str(), line, func.c_str(), message.c_str()); - TEST_STATUS = 1; + return testLogOutput.str().substr(offset); } -template -inline std::string quote(T value) { - return std::to_string(value); +inline void luaEval(std::shared_ptr m, std::string source) { + auto ss = m->GetService(); + auto s = Script::New(); + ss->AddChild(s); + s->source = source; + s->Run(); } - -template <> -std::string quote(std::string value) { - std::stringstream ss; - ss << std::quoted(value); - std::string newstr = ss.str(); - - newstr = std::regex_replace(newstr, std::regex("\n"), "\\n"); - return newstr; -} - -template <> -inline std::string quote(const char* value) { - return quote(value); -} - -template <> -inline std::string quote(char* value) { - return quote(value); -} - -template -void __assert_eq(bool cond, std::string file, int line, std::string func, std::string model, T value) { - if (cond) return; - std::string message = model + " != " + quote(value); - fprintf(stderr, "ASSERT FAILED : %s:%d : %s : '%s'\n", file.c_str(), line, func.c_str(), message.c_str()); - TEST_STATUS = 1; -} \ No newline at end of file diff --git a/tests/src/testutillua.h b/tests/src/testutillua.h deleted file mode 100644 index a61c657..0000000 --- a/tests/src/testutillua.h +++ /dev/null @@ -1,30 +0,0 @@ -#pragma once - -#include "testutil.h" - -#include - -#include "logger.h" -#include "objects/datamodel.h" -#include "objects/script.h" -#include "objects/service/script/scriptcontext.h" - -std::string luaEvalOut(DATAMODEL_REF m, std::string source) { - std::stringstream out; - Logger::initTest(&out); - - auto s = Script::New(); - m->AddChild(s); - s->source = source; - s->Run(); - - Logger::initTest(nullptr); - return out.str(); -} - -void luaEval(DATAMODEL_REF m, std::string source) { - auto s = Script::New(); - m->AddChild(s); - s->source = source; - s->Run(); -} \ No newline at end of file