From 002db578eee63fa5b8bc1810f22dcde741d658c1 Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Fri, 9 Jan 2026 17:20:00 +0000 Subject: [PATCH] ROADMAP.md --- CMakeLists.txt | 27 ++++ ROADMAP.md | 4 +- src/services/impl/json_config_service.cpp | 10 +- ...fig_compiler_reference_validation_test.cpp | 137 ++++++++++++++++++ tests/crash_recovery_timeout_test.cpp | 11 +- tests/json_config_schema_validation_test.cpp | 125 ++++++++++++++++ tests/render_coordinator_init_order_test.cpp | 37 +++++ 7 files changed, 342 insertions(+), 9 deletions(-) create mode 100644 tests/config_compiler_reference_validation_test.cpp create mode 100644 tests/json_config_schema_validation_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 7127e5f..bdca470 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -661,9 +661,36 @@ target_include_directories(json_config_merge_test PRIVATE "${CMAKE_CURRENT_SOURC target_link_libraries(json_config_merge_test PRIVATE GTest::gtest GTest::gtest_main + rapidjson ) add_test(NAME json_config_merge_test COMMAND json_config_merge_test) +# Test: JSON schema validation +add_executable(json_config_schema_validation_test + tests/json_config_schema_validation_test.cpp + src/services/impl/json_config_service.cpp +) +target_include_directories(json_config_schema_validation_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(json_config_schema_validation_test PRIVATE + GTest::gtest + GTest::gtest_main + rapidjson +) +add_test(NAME json_config_schema_validation_test COMMAND json_config_schema_validation_test) + +# Test: Config compiler reference validation +add_executable(config_compiler_reference_validation_test + tests/config_compiler_reference_validation_test.cpp + src/services/impl/config_compiler_service.cpp +) +target_include_directories(config_compiler_reference_validation_test PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(config_compiler_reference_validation_test PRIVATE + GTest::gtest + GTest::gtest_main + rapidjson +) +add_test(NAME config_compiler_reference_validation_test COMMAND config_compiler_reference_validation_test) + # Test: Bgfx Draw bounds validation (TDD test for buffer overflow crash) add_executable(bgfx_draw_bounds_validation_test tests/bgfx_draw_bounds_validation_test.cpp diff --git a/ROADMAP.md b/ROADMAP.md index a26fd89..53d60b6 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -206,8 +206,8 @@ Option B: per-shader only - [ ] Cube demo config-only boot path ## Tests and Verification Checklist -- [ ] Unit tests for schema validation, reference resolution, and merge rules -- [ ] Graph validation tests for cycles and invalid dependencies +- [~] Unit tests for schema validation, merge rules, and reference resolution (remaining gaps: component payload validation) +- [x] Graph validation tests for cycles and invalid dependencies - [x] Pipeline compatibility tests (shader inputs vs mesh layouts) - [x] Crash recovery timeout tests (`tests/crash_recovery_timeout_test.cpp`) - [ ] Budget enforcement tests (over-limit textures, transient pool caps) diff --git a/src/services/impl/json_config_service.cpp b/src/services/impl/json_config_service.cpp index 1d837b3..742641f 100644 --- a/src/services/impl/json_config_service.cpp +++ b/src/services/impl/json_config_service.cpp @@ -47,6 +47,12 @@ SceneSource ParseSceneSource(const std::string& value, const std::string& jsonPa throw std::runtime_error("JSON member '" + jsonPath + "' must be 'config' or 'lua'"); } +std::string PointerToString(const rapidjson::Pointer& pointer) { + rapidjson::StringBuffer buffer; + pointer.Stringify(buffer); + return buffer.GetString(); +} + std::filesystem::path NormalizeConfigPath(const std::filesystem::path& path) { std::error_code ec; auto canonicalPath = std::filesystem::weakly_canonical(path, ec); @@ -320,8 +326,8 @@ void ValidateSchemaDocument(const rapidjson::Document& document, rapidjson::SchemaDocument schema(schemaDocument); rapidjson::SchemaValidator validator(schema); if (!document.Accept(validator)) { - const std::string docPointer = validator.GetInvalidDocumentPointer().String(); - const std::string schemaPointer = validator.GetInvalidSchemaPointer().String(); + const std::string docPointer = PointerToString(validator.GetInvalidDocumentPointer()); + const std::string schemaPointer = PointerToString(validator.GetInvalidSchemaPointer()); const std::string keyword = validator.GetInvalidSchemaKeyword(); const std::string message = "JSON schema validation failed at " + docPointer + " (schema " + schemaPointer + ", keyword=" + keyword + ")"; diff --git a/tests/config_compiler_reference_validation_test.cpp b/tests/config_compiler_reference_validation_test.cpp new file mode 100644 index 0000000..bbadd06 --- /dev/null +++ b/tests/config_compiler_reference_validation_test.cpp @@ -0,0 +1,137 @@ +#include + +#include "services/impl/config_compiler_service.hpp" + +namespace { + +bool HasDiagnosticCode(const std::vector& diagnostics, + const std::string& code) { + for (const auto& report : diagnostics) { + if (report.code == code) { + return true; + } + } + return false; +} + +TEST(ConfigCompilerReferenceValidationTest, FlagsUnknownShaderReference) { + const std::string json = R"({ + "assets": { + "shaders": { + "known": { "vs": "shaders/known.vs", "fs": "shaders/known.fs" } + } + }, + "materials": { + "mat": { + "shader": "missing" + } + } +})"; + + sdl3cpp::services::impl::ConfigCompilerService compiler(nullptr, nullptr, nullptr, nullptr); + auto result = compiler.Compile(json); + + EXPECT_FALSE(result.success); + EXPECT_TRUE(HasDiagnosticCode(result.diagnostics, "MATERIAL_SHADER_UNKNOWN")); +} + +TEST(ConfigCompilerReferenceValidationTest, FlagsUnknownTextureReference) { + const std::string json = R"({ + "assets": { + "textures": { + "tex1": { "uri": "textures/tex1.png" } + }, + "shaders": { + "known": { "vs": "shaders/known.vs", "fs": "shaders/known.fs" } + } + }, + "materials": { + "mat": { + "shader": "known", + "textures": { + "u_albedo": "missing" + } + } + } +})"; + + sdl3cpp::services::impl::ConfigCompilerService compiler(nullptr, nullptr, nullptr, nullptr); + auto result = compiler.Compile(json); + + EXPECT_FALSE(result.success); + EXPECT_TRUE(HasDiagnosticCode(result.diagnostics, "MATERIAL_TEXTURE_UNKNOWN")); +} + +TEST(ConfigCompilerReferenceValidationTest, FlagsUnknownPassOutput) { + const std::string json = R"({ + "render": { + "passes": [ + { + "id": "first", + "outputs": { + "color": { "format": "RGBA8", "usage": "renderTarget" } + } + }, + { + "id": "second", + "inputs": { + "source": "@pass.first.missing" + } + } + ] + } +})"; + + sdl3cpp::services::impl::ConfigCompilerService compiler(nullptr, nullptr, nullptr, nullptr); + auto result = compiler.Compile(json); + + EXPECT_FALSE(result.success); + EXPECT_TRUE(HasDiagnosticCode(result.diagnostics, "RG_INPUT_UNKNOWN_OUTPUT")); +} + +TEST(ConfigCompilerReferenceValidationTest, FlagsUnknownPassReference) { + const std::string json = R"({ + "render": { + "passes": [ + { + "id": "second", + "inputs": { + "source": "@pass.missing.color" + } + } + ] + } +})"; + + sdl3cpp::services::impl::ConfigCompilerService compiler(nullptr, nullptr, nullptr, nullptr); + auto result = compiler.Compile(json); + + EXPECT_FALSE(result.success); + EXPECT_TRUE(HasDiagnosticCode(result.diagnostics, "RG_INPUT_UNKNOWN_PASS")); +} + +TEST(ConfigCompilerReferenceValidationTest, FlagsSelfReference) { + const std::string json = R"({ + "render": { + "passes": [ + { + "id": "self", + "inputs": { + "source": "@pass.self.color" + }, + "outputs": { + "color": { "format": "RGBA8", "usage": "renderTarget" } + } + } + ] + } +})"; + + sdl3cpp::services::impl::ConfigCompilerService compiler(nullptr, nullptr, nullptr, nullptr); + auto result = compiler.Compile(json); + + EXPECT_FALSE(result.success); + EXPECT_TRUE(HasDiagnosticCode(result.diagnostics, "RG_INPUT_SELF_REFERENCE")); +} + +} // namespace diff --git a/tests/crash_recovery_timeout_test.cpp b/tests/crash_recovery_timeout_test.cpp index 1c141eb..91e41cc 100644 --- a/tests/crash_recovery_timeout_test.cpp +++ b/tests/crash_recovery_timeout_test.cpp @@ -39,16 +39,17 @@ public: // blocking on the background task's completion. TEST(CrashRecoveryTimeoutTest, ExecuteWithTimeoutReturnsPromptlyAfterTimeout) { auto logger = std::make_shared(); - sdl3cpp::services::impl::CrashRecoveryService crashRecoveryService(logger); + sdl3cpp::services::CrashRecoveryConfig config; + sdl3cpp::services::impl::CrashRecoveryService crashRecoveryService(logger, config); - std::promise completionPromise; - auto completionFuture = completionPromise.get_future(); + auto completionPromise = std::make_shared>(); + auto completionFuture = completionPromise->get_future(); const auto start = std::chrono::steady_clock::now(); const bool success = crashRecoveryService.ExecuteWithTimeout( - [promise = std::move(completionPromise)]() mutable { + [promise = completionPromise]() { std::this_thread::sleep_for(std::chrono::milliseconds(200)); - promise.set_value(); + promise->set_value(); }, 10, "Main Application Loop"); diff --git a/tests/json_config_schema_validation_test.cpp b/tests/json_config_schema_validation_test.cpp new file mode 100644 index 0000000..b0195d6 --- /dev/null +++ b/tests/json_config_schema_validation_test.cpp @@ -0,0 +1,125 @@ +#include + +#include "services/impl/json_config_service.hpp" +#include "services/interfaces/i_logger.hpp" + +#include +#include +#include +#include + +namespace { + +class NullLogger final : public sdl3cpp::services::ILogger { +public: + void SetLevel(sdl3cpp::services::LogLevel) override {} + sdl3cpp::services::LogLevel GetLevel() const override { return sdl3cpp::services::LogLevel::OFF; } + void SetOutputFile(const std::string&) override {} + void SetMaxLinesPerFile(size_t) override {} + void EnableConsoleOutput(bool) override {} + void Log(sdl3cpp::services::LogLevel, const std::string&) override {} + void Trace(const std::string&) override {} + void Trace(const std::string&, const std::string&, const std::string&, const std::string&) override {} + void Debug(const std::string&) override {} + void Info(const std::string&) override {} + void Warn(const std::string&) override {} + void Error(const std::string&) override {} + void TraceFunction(const std::string&) override {} + void TraceVariable(const std::string&, const std::string&) override {} + void TraceVariable(const std::string&, int) override {} + void TraceVariable(const std::string&, size_t) override {} + void TraceVariable(const std::string&, bool) override {} + void TraceVariable(const std::string&, float) override {} + void TraceVariable(const std::string&, double) override {} +}; + +class ScopedTempDir { +public: + ScopedTempDir() { + auto base = std::filesystem::temp_directory_path(); + const auto suffix = std::to_string( + std::chrono::steady_clock::now().time_since_epoch().count()); + path_ = base / ("sdl3cpp_schema_test_" + suffix); + std::filesystem::create_directories(path_); + } + + ~ScopedTempDir() { + std::error_code ec; + std::filesystem::remove_all(path_, ec); + } + + const std::filesystem::path& Path() const { + return path_; + } + +private: + std::filesystem::path path_; +}; + +std::filesystem::path GetRepoRoot() { + return std::filesystem::path(__FILE__).parent_path().parent_path(); +} + +void WriteFile(const std::filesystem::path& path, const std::string& contents) { + std::filesystem::create_directories(path.parent_path()); + std::ofstream output(path); + ASSERT_TRUE(output.is_open()) << "Failed to open file for write: " << path; + output << contents; +} + +void CopySchema(const std::filesystem::path& targetDir) { + auto schemaSource = GetRepoRoot() / "config" / "schema" / "runtime_config_v2.schema.json"; + auto schemaTarget = targetDir / "schema" / "runtime_config_v2.schema.json"; + std::filesystem::create_directories(schemaTarget.parent_path()); + std::ifstream input(schemaSource); + ASSERT_TRUE(input.is_open()) << "Missing schema source: " << schemaSource; + std::ofstream output(schemaTarget); + ASSERT_TRUE(output.is_open()) << "Failed to write schema target: " << schemaTarget; + output << input.rdbuf(); +} + +void WriteLuaScript(const std::filesystem::path& rootDir) { + WriteFile(rootDir / "scripts" / "cube_logic.lua", "-- test script\n"); +} + +TEST(JsonConfigSchemaValidationTest, RejectsInvalidWindowWidthType) { + ScopedTempDir tempDir; + CopySchema(tempDir.Path()); + WriteLuaScript(tempDir.Path()); + auto logger = std::make_shared(); + + const std::string config = R"({ + "schema_version": 2, + "configVersion": 2, + "scripts": { "entry": "scripts/cube_logic.lua", "lua_debug": false }, + "window": { "size": { "width": "wide", "height": 600 } } +})"; + + WriteFile(tempDir.Path() / "config.json", config); + + EXPECT_THROW( + sdl3cpp::services::impl::JsonConfigService(logger, tempDir.Path() / "config.json", false), + std::runtime_error); +} + +TEST(JsonConfigSchemaValidationTest, RejectsInvalidSceneSourceEnum) { + ScopedTempDir tempDir; + CopySchema(tempDir.Path()); + WriteLuaScript(tempDir.Path()); + auto logger = std::make_shared(); + + const std::string config = R"({ + "schema_version": 2, + "configVersion": 2, + "scripts": { "entry": "scripts/cube_logic.lua", "lua_debug": false }, + "runtime": { "scene_source": "broken" } +})"; + + WriteFile(tempDir.Path() / "config.json", config); + + EXPECT_THROW( + sdl3cpp::services::impl::JsonConfigService(logger, tempDir.Path() / "config.json", false), + std::runtime_error); +} + +} // namespace diff --git a/tests/render_coordinator_init_order_test.cpp b/tests/render_coordinator_init_order_test.cpp index 0227485..adc5a77 100644 --- a/tests/render_coordinator_init_order_test.cpp +++ b/tests/render_coordinator_init_order_test.cpp @@ -1,6 +1,7 @@ #include #include "services/impl/render_coordinator_service.hpp" +#include "services/interfaces/i_config_service.hpp" #include "services/interfaces/i_graphics_service.hpp" #include "services/interfaces/i_shader_script_service.hpp" @@ -53,6 +54,40 @@ public: } }; +class StubConfigService final : public sdl3cpp::services::IConfigService { +public: + uint32_t GetWindowWidth() const override { return 1; } + uint32_t GetWindowHeight() const override { return 1; } + std::filesystem::path GetScriptPath() const override { return {}; } + bool IsLuaDebugEnabled() const override { return false; } + std::string GetWindowTitle() const override { return ""; } + sdl3cpp::services::SceneSource GetSceneSource() const override { + return sdl3cpp::services::SceneSource::Lua; + } + 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 sdl3cpp::services::RenderBudgetConfig& GetRenderBudgetConfig() const override { return budgets_; } + const sdl3cpp::services::CrashRecoveryConfig& GetCrashRecoveryConfig() const override { return crashRecovery_; } + const std::string& GetConfigJson() const override { return configJson_; } + +private: + sdl3cpp::services::InputBindings inputBindings_{}; + sdl3cpp::services::MouseGrabConfig mouseGrabConfig_{}; + sdl3cpp::services::BgfxConfig bgfxConfig_{}; + sdl3cpp::services::MaterialXConfig materialXConfig_{}; + std::vector materialXMaterials_{}; + sdl3cpp::services::GuiFontConfig guiFontConfig_{}; + sdl3cpp::services::RenderBudgetConfig budgets_{}; + sdl3cpp::services::CrashRecoveryConfig crashRecovery_{}; + std::string configJson_{}; +}; + std::string JoinCalls(const std::vector& calls) { std::string joined; for (size_t index = 0; index < calls.size(); ++index) { @@ -77,11 +112,13 @@ bool HasEndFrameBeforeLoadShaders(const std::vector& calls) { } TEST(RenderCoordinatorInitOrderTest, LoadsShadersOnlyAfterFirstFrame) { + auto configService = std::make_shared(); auto graphicsService = std::make_shared(); auto shaderScriptService = std::make_shared(); sdl3cpp::services::impl::RenderCoordinatorService service( nullptr, + configService, graphicsService, nullptr, shaderScriptService,