From 0f992911575ad5c51cc76652f7b0cb09ef2cae58 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Wed, 7 Jan 2026 13:35:52 +0000 Subject: [PATCH] feat(tests): Add CubeDemoConfigService and related stubs for testing cube demo scene --- tests/test_cube_script.cpp | 556 +++++++++++++++++++++++++++++++++++++ 1 file changed, 556 insertions(+) diff --git a/tests/test_cube_script.cpp b/tests/test_cube_script.cpp index 4dcbeb1..eb21aca 100644 --- a/tests/test_cube_script.cpp +++ b/tests/test_cube_script.cpp @@ -2,13 +2,23 @@ #include "services/impl/script_engine_service.hpp" #include "services/impl/scene_script_service.hpp" #include "services/impl/shader_script_service.hpp" +#include "services/interfaces/i_audio_command_service.hpp" #include "services/interfaces/i_config_service.hpp" +#include "services/interfaces/i_mesh_service.hpp" +#include "services/interfaces/i_physics_bridge_service.hpp" #include #include +#include #include +#include +#include +#include +#include #include +#include #include +#include #include #include @@ -41,6 +51,26 @@ std::filesystem::path GetTestScriptPath() { return testDir / "scripts" / "unit_cube_logic.lua"; } +std::filesystem::path GetCubeScriptPath() { + auto repoRoot = std::filesystem::path(__FILE__).parent_path().parent_path(); + return repoRoot / "scripts" / "cube_logic.lua"; +} + +std::filesystem::path GetSeedConfigPath() { + auto repoRoot = std::filesystem::path(__FILE__).parent_path().parent_path(); + return repoRoot / "config" / "seed_runtime.json"; +} + +std::optional ReadFileContents(const std::filesystem::path& path) { + std::ifstream input(path); + if (!input) { + return std::nullopt; + } + std::string contents((std::istreambuf_iterator(input)), + std::istreambuf_iterator()); + return contents; +} + class StubConfigService final : public sdl3cpp::services::IConfigService { public: StubConfigService() { @@ -48,6 +78,12 @@ public: materialXConfig_.useConstantColor = true; materialXConfig_.shaderKey = "test"; materialXConfig_.libraryPath = ResolveMaterialXLibraryPath(); + auto configJson = ReadFileContents(GetSeedConfigPath()); + if (configJson) { + configJson_ = *configJson; + } else { + configJson_ = "{}"; + } } uint32_t GetWindowWidth() const override { return 1; } @@ -80,12 +116,530 @@ private: std::string configJson_{}; }; +class CubeDemoConfigService final : public sdl3cpp::services::IConfigService { +public: + CubeDemoConfigService(std::filesystem::path scriptPath, std::string configJson) + : scriptPath_(std::move(scriptPath)), + configJson_(std::move(configJson)) { + materialXConfig_.enabled = true; + materialXConfig_.useConstantColor = true; + materialXConfig_.shaderKey = "test"; + materialXConfig_.libraryPath = ResolveMaterialXLibraryPath(); + } + + uint32_t GetWindowWidth() const override { return 1; } + uint32_t GetWindowHeight() const override { return 1; } + std::filesystem::path GetScriptPath() const override { return scriptPath_; } + bool IsLuaDebugEnabled() const override { return false; } + std::string GetWindowTitle() const override { return ""; } + const sdl3cpp::services::InputBindings& GetInputBindings() const override { return inputBindings_; } + const sdl3cpp::services::MouseGrabConfig& GetMouseGrabConfig() const override { return mouseGrabConfig_; } + const sdl3cpp::services::BgfxConfig& GetBgfxConfig() const override { return bgfxConfig_; } + const sdl3cpp::services::MaterialXConfig& GetMaterialXConfig() const override { return materialXConfig_; } + const std::vector& GetMaterialXMaterialConfigs() const override { + return materialXMaterials_; + } + const sdl3cpp::services::GuiFontConfig& GetGuiFontConfig() const override { return guiFontConfig_; } + const std::string& GetConfigJson() const override { return configJson_; } + +private: + static std::filesystem::path ResolveMaterialXLibraryPath() { + auto repoRoot = std::filesystem::path(__FILE__).parent_path().parent_path(); + return repoRoot / "MaterialX" / "libraries"; + } + + std::filesystem::path scriptPath_; + std::string configJson_; + sdl3cpp::services::InputBindings inputBindings_{}; + sdl3cpp::services::MouseGrabConfig mouseGrabConfig_{}; + sdl3cpp::services::BgfxConfig bgfxConfig_{}; + sdl3cpp::services::MaterialXConfig materialXConfig_{}; + std::vector materialXMaterials_{}; + sdl3cpp::services::GuiFontConfig guiFontConfig_{}; +}; + +class StubAudioCommandService final : public sdl3cpp::services::IAudioCommandService { +public: + bool QueueAudioCommand(sdl3cpp::services::AudioCommandType, + const std::string&, + bool, + std::string&) override { + return true; + } + + bool StopBackground(std::string&) override { + return true; + } +}; + +class StubMeshService final : public sdl3cpp::services::IMeshService { +public: + explicit StubMeshService(sdl3cpp::services::MeshPayload payload) + : payload_(std::move(payload)) {} + + bool LoadFromFile(const std::string&, + sdl3cpp::services::MeshPayload& outPayload, + std::string&) override { + outPayload = payload_; + return true; + } + + bool LoadFromArchive(const std::string&, + const std::string&, + sdl3cpp::services::MeshPayload&, + std::string& outError) override { + outError = "archive loading not supported in tests"; + return false; + } + + void PushMeshToLua(lua_State* L, const sdl3cpp::services::MeshPayload& payload) override { + lua_newtable(L); + + lua_newtable(L); + for (size_t vertexIndex = 0; vertexIndex < payload.positions.size(); ++vertexIndex) { + lua_newtable(L); + + lua_newtable(L); + for (int component = 0; component < 3; ++component) { + lua_pushnumber(L, payload.positions[vertexIndex][component]); + lua_rawseti(L, -2, component + 1); + } + lua_setfield(L, -2, "position"); + + lua_newtable(L); + std::array normal = {0.0f, 0.0f, 1.0f}; + if (vertexIndex < payload.normals.size()) { + normal = payload.normals[vertexIndex]; + } + for (int component = 0; component < 3; ++component) { + lua_pushnumber(L, normal[component]); + lua_rawseti(L, -2, component + 1); + } + lua_setfield(L, -2, "normal"); + + lua_newtable(L); + for (int component = 0; component < 3; ++component) { + lua_pushnumber(L, payload.colors[vertexIndex][component]); + lua_rawseti(L, -2, component + 1); + } + lua_setfield(L, -2, "color"); + + lua_newtable(L); + std::array texcoord = {0.0f, 0.0f}; + if (vertexIndex < payload.texcoords.size()) { + texcoord = payload.texcoords[vertexIndex]; + } + for (int component = 0; component < 2; ++component) { + lua_pushnumber(L, texcoord[component]); + lua_rawseti(L, -2, component + 1); + } + lua_setfield(L, -2, "texcoord"); + + lua_rawseti(L, -2, static_cast(vertexIndex + 1)); + } + lua_setfield(L, -2, "vertices"); + + lua_newtable(L); + for (size_t index = 0; index < payload.indices.size(); ++index) { + lua_pushinteger(L, static_cast(payload.indices[index]) + 1); + lua_rawseti(L, -2, static_cast(index + 1)); + } + lua_setfield(L, -2, "indices"); + } + +private: + sdl3cpp::services::MeshPayload payload_; +}; + +class StubPhysicsBridgeService final : public sdl3cpp::services::IPhysicsBridgeService { +public: + bool SetGravity(const btVector3&, std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool AddBoxRigidBody(const std::string&, + const btVector3&, + float, + const btTransform&, + std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool AddSphereRigidBody(const std::string&, + float, + float, + const btTransform&, + std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool AddTriangleMeshRigidBody(const std::string&, + const std::vector>&, + const std::vector&, + const btTransform&, + std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool RemoveRigidBody(const std::string&, std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool SetRigidBodyTransform(const std::string&, const btTransform&, std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool ApplyForce(const std::string&, const btVector3&, std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool ApplyImpulse(const std::string&, const btVector3&, std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool SetLinearVelocity(const std::string&, const btVector3&, std::string& error) override { + error = "physics disabled in tests"; + return false; + } + + bool GetLinearVelocity(const std::string&, btVector3&, std::string& error) const override { + error = "physics disabled in tests"; + return false; + } + + int StepSimulation(float, int) override { + return 0; + } + + bool GetRigidBodyTransform(const std::string&, btTransform&, std::string& error) const override { + error = "physics disabled in tests"; + return false; + } + + size_t GetBodyCount() const override { + return 0; + } + + void Clear() override {} +}; + void Assert(bool condition, const std::string& message, int& failures) { if (!condition) { std::cerr << "test failure: " << message << '\n'; ++failures; } } + +struct MatrixSummary { + std::array translation{}; + std::array scale{}; +}; + +MatrixSummary ExtractMatrixSummary(const std::array& matrix) { + MatrixSummary summary; + summary.translation = {matrix[12], matrix[13], matrix[14]}; + summary.scale = {matrix[0], matrix[5], matrix[10]}; + return summary; +} + +std::array ToArray(const glm::mat4& matrix) { + std::array values{}; + std::memcpy(values.data(), glm::value_ptr(matrix), sizeof(float) * values.size()); + return values; +} + +bool ExpectMatrixNear(const std::array& actual, + const std::array& expected, + const std::string& label, + int& failures, + float eps = 1e-4f) { + for (size_t i = 0; i < actual.size(); ++i) { + if (!ApproximatelyEqual(actual[i], expected[i], eps)) { + std::cerr << label << " differs at index " << i << " (" << actual[i] + << " vs " << expected[i] << ")\n"; + ++failures; + return false; + } + } + return true; +} + +bool ExpectColorNear(const sdl3cpp::core::Vertex& vertex, + const std::array& expected, + const std::string& label, + int& failures, + float eps = 1e-4f) { + for (size_t i = 0; i < expected.size(); ++i) { + if (!ApproximatelyEqual(vertex.color[i], expected[i], eps)) { + std::cerr << label << " color differs at index " << i << " (" << vertex.color[i] + << " vs " << expected[i] << ")\n"; + ++failures; + return false; + } + } + return true; +} + +sdl3cpp::services::MeshPayload BuildTestCubePayload() { + sdl3cpp::services::MeshPayload payload; + payload.positions = { + {-1.0f, -1.0f, 0.0f}, + {1.0f, -1.0f, 0.0f}, + {1.0f, 1.0f, 0.0f}, + {-1.0f, 1.0f, 0.0f}, + }; + payload.normals = { + {0.0f, 0.0f, 1.0f}, + {0.0f, 0.0f, 1.0f}, + {0.0f, 0.0f, 1.0f}, + {0.0f, 0.0f, 1.0f}, + }; + payload.colors = { + {0.2f, 0.3f, 0.4f}, + {0.2f, 0.3f, 0.4f}, + {0.2f, 0.3f, 0.4f}, + {0.2f, 0.3f, 0.4f}, + }; + payload.texcoords = { + {0.0f, 0.0f}, + {1.0f, 0.0f}, + {1.0f, 1.0f}, + {0.0f, 1.0f}, + }; + payload.indices = {0, 1, 2, 2, 3, 0}; + return payload; +} + +void RunCubeDemoSceneTests(int& failures) { + auto scriptPath = GetCubeScriptPath(); + auto configPath = GetSeedConfigPath(); + auto configJson = ReadFileContents(configPath); + Assert(static_cast(configJson), "seed runtime config missing", failures); + if (!configJson) { + return; + } + + auto logger = std::make_shared(); + auto configService = std::make_shared(scriptPath, *configJson); + auto meshService = std::make_shared(BuildTestCubePayload()); + auto audioService = std::make_shared(); + auto physicsService = std::make_shared(); + auto engineService = std::make_shared( + scriptPath, + logger, + meshService, + audioService, + physicsService, + nullptr, + nullptr, + configService, + false); + engineService->Initialize(); + + sdl3cpp::services::impl::SceneScriptService sceneService(engineService, logger); + auto objects = sceneService.LoadSceneObjects(); + + Assert(objects.size() == 16, "cube demo should return 16 scene objects", failures); + + std::vector floorObjects; + std::vector wallObjects; + std::vector ceilingObjects; + std::vector solidObjects; + std::vector skyboxObjects; + std::vector otherObjects; + + for (const auto& object : objects) { + Assert(!object.shaderKeys.empty(), "scene object missing shader key", failures); + if (object.shaderKeys.empty()) { + continue; + } + const std::string& shaderKey = object.shaderKeys.front(); + if (shaderKey == "floor") { + floorObjects.push_back(&object); + } else if (shaderKey == "wall") { + wallObjects.push_back(&object); + } else if (shaderKey == "ceiling") { + ceilingObjects.push_back(&object); + } else if (shaderKey == "solid") { + solidObjects.push_back(&object); + } else if (shaderKey == "skybox") { + skyboxObjects.push_back(&object); + } else { + otherObjects.push_back(&object); + } + } + + Assert(ceilingObjects.size() == 1, "expected 1 ceiling object", failures); + Assert(wallObjects.size() == 4, "expected 4 wall objects", failures); + Assert(solidObjects.size() == 8, "expected 8 lantern objects", failures); + Assert(skyboxObjects.size() == 1, "expected 1 skybox object", failures); + Assert(floorObjects.size() == 2, "expected 2 floor-key objects (floor + cube)", failures); + Assert(otherObjects.empty(), "unexpected shader keys in cube demo scene", failures); + + const std::array white = {1.0f, 1.0f, 1.0f}; + const std::array lanternColor = {1.0f, 0.9f, 0.6f}; + const std::array skyboxColor = {0.04f, 0.05f, 0.08f}; + + const float roomHalfSize = 15.0f; + const float wallThickness = 0.5f; + const float wallHeight = 4.0f; + const float floorHalfThickness = 0.3f; + const float floorTop = 0.0f; + const float floorCenterY = floorTop - floorHalfThickness; + const float wallCenterY = floorTop + wallHeight; + const float ceilingY = floorTop + wallHeight * 2.0f + floorHalfThickness; + const float wallOffset = roomHalfSize + wallThickness; + const float lanternHeight = 8.0f; + const float lanternSize = 0.2f; + const float lanternOffset = roomHalfSize - 2.0f; + + std::vector> wallTranslations; + wallTranslations.reserve(wallObjects.size()); + for (const auto* object : wallObjects) { + auto matrix = sceneService.ComputeModelMatrix(object->computeModelMatrixRef, 0.0f); + auto summary = ExtractMatrixSummary(matrix); + wallTranslations.push_back(summary.translation); + Assert(ApproximatelyEqual(summary.scale[1], wallHeight), "wall scale height mismatch", failures); + Assert(object->indices.size() == 6, "wall indices should be single-sided", failures); + if (!object->vertices.empty()) { + ExpectColorNear(object->vertices.front(), white, "wall vertex color", failures); + } + } + + const std::vector> expectedWallTranslations = { + {0.0f, wallCenterY, -wallOffset}, + {0.0f, wallCenterY, wallOffset}, + {-wallOffset, wallCenterY, 0.0f}, + {wallOffset, wallCenterY, 0.0f}, + }; + for (const auto& expected : expectedWallTranslations) { + bool found = false; + for (const auto& actual : wallTranslations) { + if (ApproximatelyEqual(actual[0], expected[0]) + && ApproximatelyEqual(actual[1], expected[1]) + && ApproximatelyEqual(actual[2], expected[2])) { + found = true; + break; + } + } + Assert(found, "missing wall at expected translation", failures); + } + + if (!ceilingObjects.empty()) { + const auto* ceiling = ceilingObjects.front(); + auto matrix = sceneService.ComputeModelMatrix(ceiling->computeModelMatrixRef, 0.0f); + auto summary = ExtractMatrixSummary(matrix); + Assert(ApproximatelyEqual(summary.translation[1], ceilingY), "ceiling translation mismatch", failures); + Assert(ApproximatelyEqual(summary.scale[1], floorHalfThickness), "ceiling thickness mismatch", failures); + Assert(ceiling->indices.size() == 6, "ceiling indices should be single-sided", failures); + if (!ceiling->vertices.empty()) { + ExpectColorNear(ceiling->vertices.front(), white, "ceiling vertex color", failures); + } + } + + const sdl3cpp::services::SceneObject* floorObject = nullptr; + const sdl3cpp::services::SceneObject* cubeObject = nullptr; + for (const auto* object : floorObjects) { + auto matrix = sceneService.ComputeModelMatrix(object->computeModelMatrixRef, 0.0f); + auto summary = ExtractMatrixSummary(matrix); + if (ApproximatelyEqual(summary.scale[0], roomHalfSize) + && ApproximatelyEqual(summary.scale[2], roomHalfSize)) { + floorObject = object; + } else if (ApproximatelyEqual(summary.scale[0], 1.5f)) { + cubeObject = object; + } + } + Assert(floorObject != nullptr, "floor object not found", failures); + Assert(cubeObject != nullptr, "dynamic cube object not found", failures); + + if (floorObject) { + auto matrix = sceneService.ComputeModelMatrix(floorObject->computeModelMatrixRef, 0.0f); + auto summary = ExtractMatrixSummary(matrix); + Assert(ApproximatelyEqual(summary.translation[1], floorCenterY), "floor translation mismatch", failures); + Assert(ApproximatelyEqual(summary.scale[1], floorHalfThickness), "floor thickness mismatch", failures); + Assert(floorObject->indices.size() == 6, "floor indices should be single-sided", failures); + if (!floorObject->vertices.empty()) { + ExpectColorNear(floorObject->vertices.front(), white, "floor vertex color", failures); + } + } + + if (cubeObject) { + const float time = 0.0f; + auto matrix = sceneService.ComputeModelMatrix(cubeObject->computeModelMatrixRef, time); + glm::mat4 translation = glm::translate(glm::mat4(1.0f), glm::vec3(0.0f, 5.0f, 0.0f)); + glm::mat4 rotation = glm::rotate(glm::mat4(1.0f), -time * 0.9f, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::mat4 scale = glm::scale(glm::mat4(1.0f), glm::vec3(1.5f, 1.5f, 1.5f)); + glm::mat4 expected = translation * rotation * scale; + ExpectMatrixNear(matrix, ToArray(expected), "spinning cube matrix at t=0", failures); + Assert(cubeObject->indices.size() == 12, "spinning cube indices should be double-sided", failures); + if (!cubeObject->vertices.empty()) { + const std::array expectedColor = {0.2f, 0.3f, 0.4f}; + ExpectColorNear(cubeObject->vertices.front(), expectedColor, "spinning cube vertex color", failures); + } + + const float laterTime = 1.0f; + auto laterMatrix = sceneService.ComputeModelMatrix(cubeObject->computeModelMatrixRef, laterTime); + glm::mat4 laterRotation = glm::rotate(glm::mat4(1.0f), -laterTime * 0.9f, glm::vec3(0.0f, 1.0f, 0.0f)); + glm::mat4 laterExpected = translation * laterRotation * scale; + ExpectMatrixNear(laterMatrix, ToArray(laterExpected), "spinning cube matrix at t=1", failures); + } + + std::vector> lanternTranslations; + lanternTranslations.reserve(solidObjects.size()); + for (const auto* object : solidObjects) { + auto matrix = sceneService.ComputeModelMatrix(object->computeModelMatrixRef, 0.0f); + auto summary = ExtractMatrixSummary(matrix); + lanternTranslations.push_back(summary.translation); + Assert(ApproximatelyEqual(summary.scale[0], lanternSize), "lantern scale mismatch", failures); + if (!object->vertices.empty()) { + ExpectColorNear(object->vertices.front(), lanternColor, "lantern vertex color", failures); + } + } + + const std::vector> expectedLanternTranslations = { + {lanternOffset, lanternHeight, lanternOffset}, + {-lanternOffset, lanternHeight, lanternOffset}, + {lanternOffset, lanternHeight, -lanternOffset}, + {-lanternOffset, lanternHeight, -lanternOffset}, + {0.0f, lanternHeight, lanternOffset}, + {0.0f, lanternHeight, -lanternOffset}, + {lanternOffset, lanternHeight, 0.0f}, + {-lanternOffset, lanternHeight, 0.0f}, + }; + for (const auto& expected : expectedLanternTranslations) { + bool found = false; + for (const auto& actual : lanternTranslations) { + if (ApproximatelyEqual(actual[0], expected[0]) + && ApproximatelyEqual(actual[1], expected[1]) + && ApproximatelyEqual(actual[2], expected[2])) { + found = true; + break; + } + } + Assert(found, "missing lantern at expected translation", failures); + } + + if (!skyboxObjects.empty()) { + const auto* skybox = skyboxObjects.front(); + auto matrix = sceneService.ComputeModelMatrix(skybox->computeModelMatrixRef, 0.0f); + auto summary = ExtractMatrixSummary(matrix); + Assert(ApproximatelyEqual(summary.translation[0], 0.0f), "skybox x translation mismatch", failures); + Assert(ApproximatelyEqual(summary.translation[1], 1.6f), "skybox y translation mismatch", failures); + Assert(ApproximatelyEqual(summary.translation[2], 10.0f), "skybox z translation mismatch", failures); + Assert(skybox->indices.size() == 12, "skybox indices should be double-sided", failures); + if (!skybox->vertices.empty()) { + ExpectColorNear(skybox->vertices.front(), skyboxColor, "skybox vertex color", failures); + } + } +} } // namespace int main() { @@ -144,6 +698,8 @@ int main() { Assert(!testEntry->second.vertexSource.empty(), "vertex shader source missing", failures); Assert(!testEntry->second.fragmentSource.empty(), "fragment shader source missing", failures); } + + RunCubeDemoSceneTests(failures); } catch (const std::exception& ex) { std::cerr << "exception during tests: " << ex.what() << '\n'; return 1;