From c01213962c81aaac3d69e24b86e5b6f996fac571 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Wed, 7 Jan 2026 14:25:46 +0000 Subject: [PATCH] feat(tests): Enhance CubeDemoConfigService and GPU smoke test with improved error handling and dynamic renderer selection --- tests/test_cube_script.cpp | 305 +++++++++++++++++++++---------------- 1 file changed, 172 insertions(+), 133 deletions(-) diff --git a/tests/test_cube_script.cpp b/tests/test_cube_script.cpp index 785973f..b715d92 100644 --- a/tests/test_cube_script.cpp +++ b/tests/test_cube_script.cpp @@ -6,12 +6,15 @@ #include "services/impl/script_engine_service.hpp" #include "services/impl/scene_script_service.hpp" #include "services/impl/shader_script_service.hpp" +#include "services/impl/ecs_service.hpp" +#include "services/impl/scene_service.hpp" #include "services/interfaces/i_audio_command_service.hpp" #include "services/interfaces/i_config_service.hpp" #include #include #include +#include #include #include #include @@ -124,14 +127,15 @@ private: class CubeDemoConfigService final : public sdl3cpp::services::IConfigService { public: - CubeDemoConfigService(std::filesystem::path scriptPath, std::string configJson) + CubeDemoConfigService(std::filesystem::path scriptPath, std::string configJson, std::string renderer = "auto") : scriptPath_(std::move(scriptPath)), configJson_(std::move(configJson)) { materialXConfig_.enabled = true; materialXConfig_.useConstantColor = true; materialXConfig_.shaderKey = "test"; materialXConfig_.libraryPath = ResolveMaterialXLibraryPath(); - bgfxConfig_.renderer = "auto"; + renderer_ = std::move(renderer); + bgfxConfig_.renderer = renderer_; } uint32_t GetWindowWidth() const override { return 1; } @@ -163,6 +167,7 @@ private: sdl3cpp::services::MaterialXConfig materialXConfig_{}; std::vector materialXMaterials_{}; sdl3cpp::services::GuiFontConfig guiFontConfig_{}; + std::string renderer_; }; class StubAudioCommandService final : public sdl3cpp::services::IAudioCommandService { @@ -236,7 +241,7 @@ bool ExpectColorNear(const sdl3cpp::core::Vertex& vertex, return true; } -void RunGpuSmokeTest(int& failures, +bool RunGpuSmokeTest(int& failures, const std::shared_ptr& configService, const std::shared_ptr& logger) { const char* preferredDrivers[] = {"x11", "wayland", "offscreen", "dummy", nullptr}; @@ -264,10 +269,15 @@ void RunGpuSmokeTest(int& failures, }; for (const char* driver : preferredDrivers) { setVideoDriver(driver); + SDL_ClearError(); if (SDL_Init(SDL_INIT_VIDEO) == 0) { initialized = true; selectedDriver = driver; break; + } else { + const char* error = SDL_GetError(); + std::cerr << "SDL_Init failed for driver [" << (driver ? driver : "default") + << "]: " << (error && error[0] ? error : "no error message") << '\n'; } } @@ -289,10 +299,10 @@ void RunGpuSmokeTest(int& failures, if (driverList.empty()) { driverList = "none"; } - std::cerr << "test failure: SDL video init failed: " << message + std::cerr << "GPU smoke test failed: no SDL driver available: " << message << " (available drivers: " << driverList << ")\n"; ++failures; - return; + return false; } if (selectedDriver) { @@ -303,10 +313,10 @@ void RunGpuSmokeTest(int& failures, SDL_Window* window = SDL_CreateWindow("cube_gpu_test", 64, 64, SDL_WINDOW_HIDDEN); if (!window) { - std::cerr << "test failure: SDL window creation failed: " << SDL_GetError() << '\n'; + std::cerr << "GPU smoke test failed: unable to create SDL window: " << SDL_GetError() << '\n'; ++failures; SDL_QuitSubSystem(SDL_INIT_VIDEO); - return; + return false; } auto platformService = std::make_shared(logger); @@ -315,15 +325,20 @@ void RunGpuSmokeTest(int& failures, sdl3cpp::services::GraphicsConfig graphicsConfig{}; backend.Initialize(window, graphicsConfig); } catch (const std::exception& ex) { - std::cerr << "test failure: bgfx init threw: " << ex.what() << '\n'; + std::cerr << "GPU smoke test failed: bgfx init threw: " << ex.what() << '\n'; ++failures; SDL_DestroyWindow(window); SDL_QuitSubSystem(SDL_INIT_VIDEO); - return; + return false; } - Assert(bgfx::getRendererType() != bgfx::RendererType::Noop, - "bgfx selected Noop renderer; GPU unavailable", failures); + if (bgfx::getRendererType() == bgfx::RendererType::Noop) { + std::cerr << "GPU smoke test failed: bgfx selected Noop renderer despite SDL success\n"; + ++failures; + SDL_DestroyWindow(window); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + return false; + } auto device = backend.CreateDevice(); backend.BeginFrame(device); @@ -333,6 +348,7 @@ void RunGpuSmokeTest(int& failures, SDL_DestroyWindow(window); SDL_QuitSubSystem(SDL_INIT_VIDEO); + return true; } void RunCubeDemoSceneTests(int& failures) { @@ -345,12 +361,16 @@ void RunCubeDemoSceneTests(int& failures) { } auto logger = std::make_shared(); - auto configService = std::make_shared(scriptPath, *configJson); + auto configService = std::make_shared(scriptPath, *configJson, "vulkan"); auto meshService = std::make_shared(configService, logger); auto audioService = std::make_shared(); auto physicsService = std::make_shared(logger); - RunGpuSmokeTest(failures, configService, logger); + if (!RunGpuSmokeTest(failures, configService, logger)) { + std::cerr << "Aborting cube scene checks because Vulkan smoke test failed\n"; + return; + } + auto engineService = std::make_shared( scriptPath, logger, @@ -363,45 +383,38 @@ void RunCubeDemoSceneTests(int& failures) { false); engineService->Initialize(); - sdl3cpp::services::impl::SceneScriptService sceneService(engineService, logger); - auto objects = sceneService.LoadSceneObjects(); + auto sceneScriptService = std::make_shared(engineService, logger); + auto objects = sceneScriptService->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); - } + if (objects.empty()) { + engineService->Shutdown(); + return; } - 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); + for (const auto& object : objects) { + Assert(!object.vertices.empty(), "scene object missing vertices", failures); + Assert(!object.indices.empty(), "scene object missing indices", failures); + Assert(!object.shaderKeys.empty(), "scene object missing shader key", failures); + Assert(object.computeModelMatrixRef >= 0, "scene object must keep a Lua reference", failures); + } + + auto ecsService = std::make_shared(logger); + auto sceneManager = std::make_shared(sceneScriptService, ecsService, logger); + sceneManager->LoadScene(objects); + Assert(sceneManager->GetObjectCount() == objects.size(), "scene service object count mismatch", failures); + + size_t expectedVertexCount = 0; + size_t expectedIndexCount = 0; + for (const auto& object : objects) { + expectedVertexCount += object.vertices.size(); + expectedIndexCount += object.indices.size(); + } + + const auto& combinedVertices = sceneManager->GetCombinedVertices(); + const auto& combinedIndices = sceneManager->GetCombinedIndices(); + Assert(combinedVertices.size() == expectedVertexCount, "combined vertex count mismatch", failures); + Assert(combinedIndices.size() == expectedIndexCount, "combined index count mismatch", failures); const std::array white = {1.0f, 1.0f, 1.0f}; const std::array lanternColor = {1.0f, 0.9f, 0.6f}; @@ -422,19 +435,73 @@ void RunCubeDemoSceneTests(int& failures) { const float lanternOffset = roomHalfSize - 2.0f; const float cubeSpawnY = floorTop + wallHeight + 1.5f + 0.5f; + auto staticCommands = sceneManager->GetRenderCommands(0.0f); + auto dynamicCommands = sceneManager->GetRenderCommands(0.1f); + Assert(staticCommands.size() == objects.size(), "render command count mismatch", failures); + Assert(dynamicCommands.size() == objects.size(), "dynamic render command count mismatch", failures); + + std::vector floorIndices; + std::vector wallIndices; + std::vector ceilingIndices; + std::vector solidIndices; + std::vector skyboxIndices; + std::vector otherIndices; 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.empty(), "wall indices should not be empty", failures); - if (!object->vertices.empty()) { - ExpectColorNear(object->vertices.front(), white, "wall vertex color", failures); + wallTranslations.reserve(4); + std::vector> lanternTranslations; + lanternTranslations.reserve(8); + + for (size_t index = 0; index < staticCommands.size(); ++index) { + const auto& command = staticCommands[index]; + const auto& object = objects[index]; + Assert(!command.shaderKeys.empty(), "scene object missing shader key", failures); + Assert(!object.indices.empty(), "scene object should have indices", failures); + + const auto summary = ExtractMatrixSummary(command.modelMatrix); + const std::string& shaderKey = command.shaderKeys.front(); + if (shaderKey == "floor") { + floorIndices.push_back(index); + if (!object.vertices.empty()) { + ExpectColorNear(object.vertices.front(), white, "floor vertex color", failures); + } + } else if (shaderKey == "wall") { + wallIndices.push_back(index); + wallTranslations.push_back(summary.translation); + Assert(ApproximatelyEqual(summary.scale[1], wallHeight), "wall scale height mismatch", failures); + if (!object.vertices.empty()) { + ExpectColorNear(object.vertices.front(), white, "wall vertex color", failures); + } + } else if (shaderKey == "ceiling") { + ceilingIndices.push_back(index); + Assert(ApproximatelyEqual(summary.translation[1], ceilingY), "ceiling translation mismatch", failures); + Assert(ApproximatelyEqual(summary.scale[1], floorHalfThickness), "ceiling thickness mismatch", failures); + if (!object.vertices.empty()) { + ExpectColorNear(object.vertices.front(), white, "ceiling vertex color", failures); + } + } else if (shaderKey == "solid") { + solidIndices.push_back(index); + 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); + } + } else if (shaderKey == "skybox") { + skyboxIndices.push_back(index); + if (!object.vertices.empty()) { + ExpectColorNear(object.vertices.front(), skyboxColor, "skybox vertex color", failures); + } + } else { + otherIndices.push_back(index); } } + Assert(ceilingIndices.size() == 1, "expected 1 ceiling object", failures); + Assert(wallIndices.size() == 4, "expected 4 wall objects", failures); + Assert(solidIndices.size() == 8, "expected 8 lantern objects", failures); + Assert(skyboxIndices.size() == 1, "expected 1 skybox object", failures); + Assert(floorIndices.size() == 2, "expected 2 floor-key objects (floor + cube)", failures); + Assert(otherIndices.empty(), "unexpected shader keys in cube demo scene", failures); + const std::vector> expectedWallTranslations = { {0.0f, wallCenterY, -wallOffset}, {0.0f, wallCenterY, wallOffset}, @@ -454,74 +521,6 @@ void RunCubeDemoSceneTests(int& failures) { 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.empty(), "ceiling indices should not be empty", 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.empty(), "floor indices should not be empty", failures); - if (!floorObject->vertices.empty()) { - ExpectColorNear(floorObject->vertices.front(), white, "floor vertex color", failures); - } - } - - if (cubeObject) { - const float time = 0.1f; - auto matrix = sceneService.ComputeModelMatrix(cubeObject->computeModelMatrixRef, time); - auto summary = ExtractMatrixSummary(matrix); - Assert(ApproximatelyEqual(summary.translation[0], 0.0f, 0.05f), - "physics cube x translation mismatch", failures); - Assert(ApproximatelyEqual(summary.translation[2], 0.0f, 0.05f), - "physics cube z translation mismatch", failures); - Assert(ApproximatelyEqual(summary.translation[1], cubeSpawnY, 0.25f), - "physics cube y translation mismatch", failures); - Assert(ApproximatelyEqual(summary.scale[0], 1.5f, 0.05f), - "physics cube scale mismatch", failures); - Assert(!cubeObject->indices.empty(), "cube indices should not be empty", failures); - if (!cubeObject->vertices.empty()) { - ExpectColorNear(cubeObject->vertices.front(), cubeColor, "physics cube vertex color", 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}, @@ -545,21 +544,61 @@ void RunCubeDemoSceneTests(int& failures) { 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); + size_t floorObjectIndex = std::numeric_limits::max(); + size_t cubeObjectIndex = std::numeric_limits::max(); + for (size_t idx : floorIndices) { + auto summary = ExtractMatrixSummary(staticCommands[idx].modelMatrix); + if (ApproximatelyEqual(summary.scale[0], roomHalfSize) + && ApproximatelyEqual(summary.scale[2], roomHalfSize)) { + floorObjectIndex = idx; + } else if (ApproximatelyEqual(summary.scale[0], 1.5f)) { + cubeObjectIndex = idx; + } + } + Assert(floorObjectIndex != std::numeric_limits::max(), "floor object not found", failures); + Assert(cubeObjectIndex != std::numeric_limits::max(), "dynamic cube object not found", failures); + + if (floorObjectIndex != std::numeric_limits::max()) { + auto summary = ExtractMatrixSummary(staticCommands[floorObjectIndex].modelMatrix); + Assert(ApproximatelyEqual(summary.translation[1], floorCenterY), "floor translation mismatch", failures); + Assert(ApproximatelyEqual(summary.scale[1], floorHalfThickness), "floor thickness mismatch", failures); + if (!objects[floorObjectIndex].vertices.empty()) { + ExpectColorNear(objects[floorObjectIndex].vertices.front(), white, "floor vertex color", failures); + } + Assert(!objects[floorObjectIndex].indices.empty(), "floor indices should not be empty", failures); + } + + if (cubeObjectIndex != std::numeric_limits::max()) { + auto summary = ExtractMatrixSummary(dynamicCommands[cubeObjectIndex].modelMatrix); + Assert(ApproximatelyEqual(summary.translation[0], 0.0f, 0.05f), + "physics cube x translation mismatch", failures); + Assert(ApproximatelyEqual(summary.translation[2], 0.0f, 0.05f), + "physics cube z translation mismatch", failures); + Assert(ApproximatelyEqual(summary.translation[1], cubeSpawnY, 0.25f), + "physics cube y translation mismatch", failures); + Assert(ApproximatelyEqual(summary.scale[0], 1.5f, 0.05f), + "physics cube scale mismatch", failures); + if (!objects[cubeObjectIndex].vertices.empty()) { + ExpectColorNear(objects[cubeObjectIndex].vertices.front(), cubeColor, "physics cube vertex color", failures); + } + Assert(!objects[cubeObjectIndex].indices.empty(), "cube indices should not be empty", failures); + } + + if (!skyboxIndices.empty()) { + auto summary = ExtractMatrixSummary(staticCommands[skyboxIndices.front()].modelMatrix); 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.empty(), "skybox indices should not be empty", failures); - if (!skybox->vertices.empty()) { - ExpectColorNear(skybox->vertices.front(), skyboxColor, "skybox vertex color", failures); - } + Assert(!objects[skyboxIndices.front()].indices.empty(), "skybox indices should not be empty", failures); } + + sceneManager->Shutdown(); + engineService->Shutdown(); } } // namespace +// When invoking this test locally, prefer `python scripts/dev_commands.py cmake -- --target script_engine_tests` +// so the helper picks the right build dir/generator. int main() { int failures = 0; auto scriptPath = GetTestScriptPath();