From cc377b5eb59b06947be09a8e516be6effead224c Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 08:32:29 +0200 Subject: [PATCH] Add assets settings document tests --- CMakeLists.txt | 3 +- docs/modernization/build-inventory.md | 4 +- docs/modernization/roadmap.md | 9 +- scripts/automation/platform-build.ps1 | 2 +- scripts/automation/platform-build.sh | 2 +- src/assets/settings_document.cpp | 183 +++++++++++++++++++++++ src/assets/settings_document.h | 48 ++++++ tests/CMakeLists.txt | 10 ++ tests/assets/settings_document_tests.cpp | 122 +++++++++++++++ 9 files changed, 374 insertions(+), 9 deletions(-) create mode 100644 src/assets/settings_document.cpp create mode 100644 src/assets/settings_document.h create mode 100644 tests/assets/settings_document_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 5aadf95..676e4f5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,7 +85,8 @@ target_link_libraries(pp_foundation add_library(pp_assets STATIC src/assets/image_format.cpp - src/assets/ppi_header.cpp) + src/assets/ppi_header.cpp + src/assets/settings_document.cpp) target_include_directories(pp_assets PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/src") diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 528ee1f..2749503 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -80,8 +80,8 @@ Known local toolchain state: `platform-build` automation wrapper for `pp_foundation`, `pp_assets`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_ui_core`, `pano_cli`, and their current headless test binaries, - including foundation event/logging/task queue coverage, PPI header, paint - stroke sampling, and layout XML parse coverage. + including foundation event/logging/task queue coverage, PPI header, settings + document, paint stroke sampling, and layout XML parse coverage. - `panopainter_validate_shaders` validates the current combined GLSL shader files for one vertex stage marker, one fragment stage marker, valid marker order, and existing relative includes. diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index ac46958..866fd4f 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -307,9 +307,9 @@ boundary/overread tests. It also owns strict decimal `uint32` parsing used by input. A synchronous event dispatcher, structured logging facade, bounded FIFO task queue, and deterministic `TraceRecorder` now record component/name/thread/frame/stroke metadata with filtering, capacity, and -invalid-end tests. `pp_assets` has started with -PNG/JPEG signature detection plus PPI header recognition, with -corrupt/truncated/unsupported tests. +invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection, +PPI header recognition, and a pure typed settings document model, with +corrupt/truncated/unsupported and key/value limit tests. `pp_paint` has started with CPU reference math for the five current shader blend modes plus deterministic stroke spacing/interpolation. `pp_document` has started with a pure canvas/layer/frame model and layer/frame/undo-redo history @@ -525,7 +525,7 @@ Last verified on 2026-06-01: ```powershell cmake --preset windows-msvc-default -cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests pano_cli PanoPainter ctest --preset desktop-fast --build-config Debug powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli @@ -546,6 +546,7 @@ Results: - `pp_foundation_trace_tests` passed. - `pp_assets_image_format_tests` passed. - `pp_assets_ppi_header_tests` passed. +- `pp_assets_settings_document_tests` passed. - `pp_paint_blend_tests` passed. - `pp_paint_stroke_tests` passed. - `pp_document_tests` passed. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 8432358..e884394 100644 --- a/scripts/automation/platform-build.ps1 +++ b/scripts/automation/platform-build.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param( [string[]]$Presets = @("android-arm64"), - [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") + [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pp_renderer_api", "pp_paint_renderer", "pp_ui_core", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_event_tests", "pp_foundation_log_tests", "pp_foundation_parse_tests", "pp_foundation_task_queue_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_assets_ppi_header_tests", "pp_assets_settings_document_tests", "pp_paint_blend_tests", "pp_paint_stroke_tests", "pp_document_tests", "pp_renderer_api_tests", "pp_paint_renderer_compositor_tests", "pp_ui_core_layout_value_tests", "pp_ui_core_layout_xml_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index a54e9d1..6c40ec4 100644 --- a/scripts/automation/platform-build.sh +++ b/scripts/automation/platform-build.sh @@ -3,7 +3,7 @@ set -u preset="${1:-android-arm64}" shift || true -targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pp_renderer_api pp_paint_renderer pp_ui_core pano_cli pp_foundation_binary_stream_tests pp_foundation_event_tests pp_foundation_log_tests pp_foundation_parse_tests pp_foundation_task_queue_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_assets_ppi_header_tests pp_assets_settings_document_tests pp_paint_blend_tests pp_paint_stroke_tests pp_document_tests pp_renderer_api_tests pp_paint_renderer_compositor_tests pp_ui_core_layout_value_tests pp_ui_core_layout_xml_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/assets/settings_document.cpp b/src/assets/settings_document.cpp new file mode 100644 index 0000000..3a6f0a3 --- /dev/null +++ b/src/assets/settings_document.cpp @@ -0,0 +1,183 @@ +#include "assets/settings_document.h" + +#include +#include +#include + +namespace pp::assets { + +namespace { + +[[nodiscard]] bool is_valid_key_char(char value) noexcept +{ + const auto ch = static_cast(value); + return std::isalnum(ch) != 0 || value == '_' || value == '-' || value == '.'; +} + +} + +std::size_t SettingsDocument::size() const noexcept +{ + return entries_.size(); +} + +bool SettingsDocument::empty() const noexcept +{ + return entries_.empty(); +} + +bool SettingsDocument::has(std::string_view key) const noexcept +{ + return find_entry(key) != entries_.end(); +} + +const std::vector& SettingsDocument::entries() const noexcept +{ + return entries_; +} + +pp::foundation::Status SettingsDocument::set(std::string_view key, SettingsValue value) +{ + const auto key_status = validate_settings_key(key); + if (!key_status.ok()) { + return key_status; + } + + const auto value_status = validate_settings_value(value); + if (!value_status.ok()) { + return value_status; + } + + auto found = find_entry(key); + if (found != entries_.end()) { + found->value = value; + return pp::foundation::Status::success(); + } + + if (entries_.size() >= max_settings_entries) { + return pp::foundation::Status::out_of_range("settings entry count exceeds the configured limit"); + } + + entries_.push_back(SettingsEntry { + .key = std::string(key), + .value = value, + }); + return pp::foundation::Status::success(); +} + +pp::foundation::Result SettingsDocument::get(std::string_view key) const +{ + const auto key_status = validate_settings_key(key); + if (!key_status.ok()) { + return pp::foundation::Result::failure(key_status); + } + + const auto found = find_entry(key); + if (found == entries_.end()) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("settings key was not found")); + } + + return pp::foundation::Result::success(found->value); +} + +pp::foundation::Status SettingsDocument::unset(std::string_view key) noexcept +{ + const auto key_status = validate_settings_key(key); + if (!key_status.ok()) { + return key_status; + } + + const auto found = find_entry(key); + if (found == entries_.end()) { + return pp::foundation::Status::out_of_range("settings key was not found"); + } + + entries_.erase(found); + return pp::foundation::Status::success(); +} + +void SettingsDocument::clear() noexcept +{ + entries_.clear(); +} + +std::vector::iterator SettingsDocument::find_entry(std::string_view key) noexcept +{ + return std::find_if( + entries_.begin(), + entries_.end(), + [key](const SettingsEntry& entry) { + return entry.key == key; + }); +} + +std::vector::const_iterator SettingsDocument::find_entry(std::string_view key) const noexcept +{ + return std::find_if( + entries_.begin(), + entries_.end(), + [key](const SettingsEntry& entry) { + return entry.key == key; + }); +} + +pp::foundation::Status validate_settings_key(std::string_view key) noexcept +{ + if (key.empty()) { + return pp::foundation::Status::invalid_argument("settings key must not be empty"); + } + + if (key.size() > max_settings_key_length) { + return pp::foundation::Status::out_of_range("settings key length exceeds the configured limit"); + } + + if (key.front() == '.' || key.back() == '.') { + return pp::foundation::Status::invalid_argument("settings key must not start or end with a dot"); + } + + for (const auto ch : key) { + if (!is_valid_key_char(ch)) { + return pp::foundation::Status::invalid_argument("settings key contains an unsupported character"); + } + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Status validate_settings_value(const SettingsValue& value) noexcept +{ + if (const auto* string_value = std::get_if(&value)) { + if (string_value->size() > max_settings_string_length) { + return pp::foundation::Status::out_of_range("settings string length exceeds the configured limit"); + } + } + + if (const auto* double_value = std::get_if(&value)) { + if (!std::isfinite(*double_value)) { + return pp::foundation::Status::invalid_argument("settings floating point value must be finite"); + } + } + + return pp::foundation::Status::success(); +} + +const char* settings_value_type_name(const SettingsValue& value) noexcept +{ + if (std::holds_alternative(value)) { + return "bool"; + } + if (std::holds_alternative(value)) { + return "int64"; + } + if (std::holds_alternative(value)) { + return "double"; + } + if (std::holds_alternative(value)) { + return "string"; + } + + return "unknown"; +} + +} diff --git a/src/assets/settings_document.h b/src/assets/settings_document.h new file mode 100644 index 0000000..d9131a4 --- /dev/null +++ b/src/assets/settings_document.h @@ -0,0 +1,48 @@ +#pragma once + +#include "foundation/result.h" + +#include +#include +#include +#include +#include +#include + +namespace pp::assets { + +constexpr std::size_t max_settings_entries = 4096; +constexpr std::size_t max_settings_key_length = 128; +constexpr std::size_t max_settings_string_length = 4096; + +using SettingsValue = std::variant; + +struct SettingsEntry { + std::string key; + SettingsValue value; +}; + +class SettingsDocument { +public: + [[nodiscard]] std::size_t size() const noexcept; + [[nodiscard]] bool empty() const noexcept; + [[nodiscard]] bool has(std::string_view key) const noexcept; + [[nodiscard]] const std::vector& entries() const noexcept; + + [[nodiscard]] pp::foundation::Status set(std::string_view key, SettingsValue value); + [[nodiscard]] pp::foundation::Result get(std::string_view key) const; + [[nodiscard]] pp::foundation::Status unset(std::string_view key) noexcept; + void clear() noexcept; + +private: + [[nodiscard]] std::vector::iterator find_entry(std::string_view key) noexcept; + [[nodiscard]] std::vector::const_iterator find_entry(std::string_view key) const noexcept; + + std::vector entries_; +}; + +[[nodiscard]] pp::foundation::Status validate_settings_key(std::string_view key) noexcept; +[[nodiscard]] pp::foundation::Status validate_settings_value(const SettingsValue& value) noexcept; +[[nodiscard]] const char* settings_value_type_name(const SettingsValue& value) noexcept; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 8bc24d5..0c9c57e 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -86,6 +86,16 @@ add_test(NAME pp_assets_ppi_header_tests COMMAND pp_assets_ppi_header_tests) set_tests_properties(pp_assets_ppi_header_tests PROPERTIES LABELS "assets;desktop-fast") +add_executable(pp_assets_settings_document_tests + assets/settings_document_tests.cpp) +target_link_libraries(pp_assets_settings_document_tests PRIVATE + pp_assets + pp_test_harness) + +add_test(NAME pp_assets_settings_document_tests COMMAND pp_assets_settings_document_tests) +set_tests_properties(pp_assets_settings_document_tests PROPERTIES + LABELS "assets;desktop-fast") + add_executable(pp_paint_blend_tests paint/blend_tests.cpp) target_link_libraries(pp_paint_blend_tests PRIVATE diff --git a/tests/assets/settings_document_tests.cpp b/tests/assets/settings_document_tests.cpp new file mode 100644 index 0000000..9c70951 --- /dev/null +++ b/tests/assets/settings_document_tests.cpp @@ -0,0 +1,122 @@ +#include "assets/settings_document.h" +#include "test_harness.h" + +#include +#include +#include +#include + +using pp::assets::SettingsDocument; +using pp::assets::SettingsValue; +using pp::assets::max_settings_entries; +using pp::assets::settings_value_type_name; +using pp::assets::validate_settings_key; +using pp::assets::validate_settings_value; +using pp::foundation::StatusCode; + +namespace { + +void stores_updates_and_reads_typed_values(pp::tests::Harness& h) +{ + SettingsDocument document; + + PP_EXPECT(h, document.empty()); + PP_EXPECT(h, document.set("ui.theme", std::string("dark")).ok()); + PP_EXPECT(h, document.set("brush.size", std::int64_t { 42 }).ok()); + PP_EXPECT(h, document.set("brush.opacity", 0.75).ok()); + PP_EXPECT(h, document.set("tablet.enabled", true).ok()); + PP_EXPECT(h, document.size() == 4U); + PP_EXPECT(h, document.has("brush.size")); + + const auto theme = document.get("ui.theme"); + const auto size = document.get("brush.size"); + const auto opacity = document.get("brush.opacity"); + const auto tablet = document.get("tablet.enabled"); + + PP_EXPECT(h, theme.ok()); + PP_EXPECT(h, std::get(theme.value()) == std::string_view("dark")); + PP_EXPECT(h, settings_value_type_name(theme.value()) == std::string_view("string")); + PP_EXPECT(h, size.ok()); + PP_EXPECT(h, std::get(size.value()) == 42); + PP_EXPECT(h, opacity.ok()); + PP_EXPECT(h, std::fabs(std::get(opacity.value()) - 0.75) < 0.0001); + PP_EXPECT(h, tablet.ok()); + PP_EXPECT(h, std::get(tablet.value())); + + PP_EXPECT(h, document.set("brush.size", std::int64_t { 64 }).ok()); + PP_EXPECT(h, document.size() == 4U); + PP_EXPECT(h, std::get(document.get("brush.size").value()) == 64); +} + +void unsets_and_clears_entries(pp::tests::Harness& h) +{ + SettingsDocument document; + + PP_EXPECT(h, document.set("a", true).ok()); + PP_EXPECT(h, document.set("b", std::int64_t { 2 }).ok()); + PP_EXPECT(h, document.unset("a").ok()); + PP_EXPECT(h, !document.has("a")); + PP_EXPECT(h, document.size() == 1U); + + const auto missing = document.unset("a"); + PP_EXPECT(h, !missing.ok()); + PP_EXPECT(h, missing.code == StatusCode::out_of_range); + + document.clear(); + PP_EXPECT(h, document.empty()); +} + +void rejects_bad_keys_and_values(pp::tests::Harness& h) +{ + const auto empty = validate_settings_key(""); + const auto dotted_start = validate_settings_key(".bad"); + const auto dotted_end = validate_settings_key("bad."); + const auto invalid_char = validate_settings_key("bad/key"); + const auto long_key = validate_settings_key(std::string(129, 'a')); + const auto non_finite = validate_settings_value(SettingsValue { std::nan("") }); + const auto huge_string = validate_settings_value(SettingsValue { std::string(4097, 'x') }); + + PP_EXPECT(h, !empty.ok()); + PP_EXPECT(h, empty.code == StatusCode::invalid_argument); + PP_EXPECT(h, !dotted_start.ok()); + PP_EXPECT(h, dotted_start.code == StatusCode::invalid_argument); + PP_EXPECT(h, !dotted_end.ok()); + PP_EXPECT(h, dotted_end.code == StatusCode::invalid_argument); + PP_EXPECT(h, !invalid_char.ok()); + PP_EXPECT(h, invalid_char.code == StatusCode::invalid_argument); + PP_EXPECT(h, !long_key.ok()); + PP_EXPECT(h, long_key.code == StatusCode::out_of_range); + PP_EXPECT(h, !non_finite.ok()); + PP_EXPECT(h, non_finite.code == StatusCode::invalid_argument); + PP_EXPECT(h, !huge_string.ok()); + PP_EXPECT(h, huge_string.code == StatusCode::out_of_range); +} + +void rejects_missing_and_excessive_entries(pp::tests::Harness& h) +{ + SettingsDocument document; + const auto missing = document.get("missing"); + PP_EXPECT(h, !missing.ok()); + PP_EXPECT(h, missing.status().code == StatusCode::out_of_range); + + for (std::size_t i = 0; i < max_settings_entries; ++i) { + const auto key = std::string("k") + std::to_string(i); + PP_EXPECT(h, document.set(key, std::int64_t { 1 }).ok()); + } + + const auto excessive = document.set("one-more", std::int64_t { 1 }); + PP_EXPECT(h, !excessive.ok()); + PP_EXPECT(h, excessive.code == StatusCode::out_of_range); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("stores_updates_and_reads_typed_values", stores_updates_and_reads_typed_values); + harness.run("unsets_and_clears_entries", unsets_and_clears_entries); + harness.run("rejects_bad_keys_and_values", rejects_bad_keys_and_values); + harness.run("rejects_missing_and_excessive_entries", rejects_missing_and_excessive_entries); + return harness.finish(); +}