mirror of
https://github.com/johndoe6345789/SDL3CPlusPlus.git
synced 2026-04-24 13:44:58 +00:00
ROADMAP.md
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 + ")";
|
||||
|
||||
137
tests/config_compiler_reference_validation_test.cpp
Normal file
137
tests/config_compiler_reference_validation_test.cpp
Normal file
@@ -0,0 +1,137 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "services/impl/config_compiler_service.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
bool HasDiagnosticCode(const std::vector<sdl3cpp::services::ProbeReport>& 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
|
||||
@@ -39,16 +39,17 @@ public:
|
||||
// blocking on the background task's completion.
|
||||
TEST(CrashRecoveryTimeoutTest, ExecuteWithTimeoutReturnsPromptlyAfterTimeout) {
|
||||
auto logger = std::make_shared<NullLogger>();
|
||||
sdl3cpp::services::impl::CrashRecoveryService crashRecoveryService(logger);
|
||||
sdl3cpp::services::CrashRecoveryConfig config;
|
||||
sdl3cpp::services::impl::CrashRecoveryService crashRecoveryService(logger, config);
|
||||
|
||||
std::promise<void> completionPromise;
|
||||
auto completionFuture = completionPromise.get_future();
|
||||
auto completionPromise = std::make_shared<std::promise<void>>();
|
||||
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");
|
||||
|
||||
125
tests/json_config_schema_validation_test.cpp
Normal file
125
tests/json_config_schema_validation_test.cpp
Normal file
@@ -0,0 +1,125 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "services/impl/json_config_service.hpp"
|
||||
#include "services/interfaces/i_logger.hpp"
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
|
||||
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<NullLogger>();
|
||||
|
||||
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<NullLogger>();
|
||||
|
||||
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
|
||||
@@ -1,6 +1,7 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#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<sdl3cpp::services::MaterialXMaterialConfig>& 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<sdl3cpp::services::MaterialXMaterialConfig> materialXMaterials_{};
|
||||
sdl3cpp::services::GuiFontConfig guiFontConfig_{};
|
||||
sdl3cpp::services::RenderBudgetConfig budgets_{};
|
||||
sdl3cpp::services::CrashRecoveryConfig crashRecovery_{};
|
||||
std::string configJson_{};
|
||||
};
|
||||
|
||||
std::string JoinCalls(const std::vector<std::string>& calls) {
|
||||
std::string joined;
|
||||
for (size_t index = 0; index < calls.size(); ++index) {
|
||||
@@ -77,11 +112,13 @@ bool HasEndFrameBeforeLoadShaders(const std::vector<std::string>& calls) {
|
||||
}
|
||||
|
||||
TEST(RenderCoordinatorInitOrderTest, LoadsShadersOnlyAfterFirstFrame) {
|
||||
auto configService = std::make_shared<StubConfigService>();
|
||||
auto graphicsService = std::make_shared<CallOrderGraphicsService>();
|
||||
auto shaderScriptService = std::make_shared<StubShaderScriptService>();
|
||||
|
||||
sdl3cpp::services::impl::RenderCoordinatorService service(
|
||||
nullptr,
|
||||
configService,
|
||||
graphicsService,
|
||||
nullptr,
|
||||
shaderScriptService,
|
||||
|
||||
Reference in New Issue
Block a user