Add assets settings document tests

This commit is contained in:
2026-06-01 08:32:29 +02:00
parent 6c435dafb7
commit cc377b5eb5
9 changed files with 374 additions and 9 deletions

View File

@@ -85,7 +85,8 @@ target_link_libraries(pp_foundation
add_library(pp_assets STATIC add_library(pp_assets STATIC
src/assets/image_format.cpp 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 target_include_directories(pp_assets
PUBLIC PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src") "${CMAKE_CURRENT_SOURCE_DIR}/src")

View File

@@ -80,8 +80,8 @@ Known local toolchain state:
`platform-build` automation wrapper for `pp_foundation`, `pp_assets`, `platform-build` automation wrapper for `pp_foundation`, `pp_assets`,
`pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`, `pp_paint`, `pp_document`, `pp_renderer_api`, `pp_paint_renderer`,
`pp_ui_core`, `pano_cli`, and their current headless test binaries, `pp_ui_core`, `pano_cli`, and their current headless test binaries,
including foundation event/logging/task queue coverage, PPI header, paint including foundation event/logging/task queue coverage, PPI header, settings
stroke sampling, and layout XML parse coverage. document, paint stroke sampling, and layout XML parse coverage.
- `panopainter_validate_shaders` validates the current combined GLSL shader - `panopainter_validate_shaders` validates the current combined GLSL shader
files for one vertex stage marker, one fragment stage marker, valid marker files for one vertex stage marker, one fragment stage marker, valid marker
order, and existing relative includes. order, and existing relative includes.

View File

@@ -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 input. A synchronous event dispatcher, structured logging facade, bounded FIFO
task queue, and deterministic `TraceRecorder` now record task queue, and deterministic `TraceRecorder` now record
component/name/thread/frame/stroke metadata with filtering, capacity, and component/name/thread/frame/stroke metadata with filtering, capacity, and
invalid-end tests. `pp_assets` has started with invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection,
PNG/JPEG signature detection plus PPI header recognition, with PPI header recognition, and a pure typed settings document model, with
corrupt/truncated/unsupported tests. corrupt/truncated/unsupported and key/value limit tests.
`pp_paint` has started with CPU reference math for the five current shader `pp_paint` has started with CPU reference math for the five current shader
blend modes plus deterministic stroke spacing/interpolation. `pp_document` has blend modes plus deterministic stroke spacing/interpolation. `pp_document` has
started with a pure canvas/layer/frame model and layer/frame/undo-redo history 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 ```powershell
cmake --preset windows-msvc-default 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 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\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli 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_foundation_trace_tests` passed.
- `pp_assets_image_format_tests` passed. - `pp_assets_image_format_tests` passed.
- `pp_assets_ppi_header_tests` passed. - `pp_assets_ppi_header_tests` passed.
- `pp_assets_settings_document_tests` passed.
- `pp_paint_blend_tests` passed. - `pp_paint_blend_tests` passed.
- `pp_paint_stroke_tests` passed. - `pp_paint_stroke_tests` passed.
- `pp_document_tests` passed. - `pp_document_tests` passed.

View File

@@ -1,7 +1,7 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[string[]]$Presets = @("android-arm64"), [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" $ErrorActionPreference = "Stop"

View File

@@ -3,7 +3,7 @@ set -u
preset="${1:-android-arm64}" preset="${1:-android-arm64}"
shift || true 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)" start="$(date +%s)"
cmake --preset "$preset" cmake --preset "$preset"

View File

@@ -0,0 +1,183 @@
#include "assets/settings_document.h"
#include <algorithm>
#include <cctype>
#include <cmath>
namespace pp::assets {
namespace {
[[nodiscard]] bool is_valid_key_char(char value) noexcept
{
const auto ch = static_cast<unsigned char>(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<SettingsEntry>& 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<SettingsValue> SettingsDocument::get(std::string_view key) const
{
const auto key_status = validate_settings_key(key);
if (!key_status.ok()) {
return pp::foundation::Result<SettingsValue>::failure(key_status);
}
const auto found = find_entry(key);
if (found == entries_.end()) {
return pp::foundation::Result<SettingsValue>::failure(
pp::foundation::Status::out_of_range("settings key was not found"));
}
return pp::foundation::Result<SettingsValue>::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<SettingsEntry>::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<SettingsEntry>::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<std::string>(&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<double>(&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<bool>(value)) {
return "bool";
}
if (std::holds_alternative<std::int64_t>(value)) {
return "int64";
}
if (std::holds_alternative<double>(value)) {
return "double";
}
if (std::holds_alternative<std::string>(value)) {
return "string";
}
return "unknown";
}
}

View File

@@ -0,0 +1,48 @@
#pragma once
#include "foundation/result.h"
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
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<bool, std::int64_t, double, std::string>;
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<SettingsEntry>& entries() const noexcept;
[[nodiscard]] pp::foundation::Status set(std::string_view key, SettingsValue value);
[[nodiscard]] pp::foundation::Result<SettingsValue> get(std::string_view key) const;
[[nodiscard]] pp::foundation::Status unset(std::string_view key) noexcept;
void clear() noexcept;
private:
[[nodiscard]] std::vector<SettingsEntry>::iterator find_entry(std::string_view key) noexcept;
[[nodiscard]] std::vector<SettingsEntry>::const_iterator find_entry(std::string_view key) const noexcept;
std::vector<SettingsEntry> 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;
}

View File

@@ -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 set_tests_properties(pp_assets_ppi_header_tests PROPERTIES
LABELS "assets;desktop-fast") 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 add_executable(pp_paint_blend_tests
paint/blend_tests.cpp) paint/blend_tests.cpp)
target_link_libraries(pp_paint_blend_tests PRIVATE target_link_libraries(pp_paint_blend_tests PRIVATE

View File

@@ -0,0 +1,122 @@
#include "assets/settings_document.h"
#include "test_harness.h"
#include <cmath>
#include <cstdint>
#include <string>
#include <string_view>
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<std::string>(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<std::int64_t>(size.value()) == 42);
PP_EXPECT(h, opacity.ok());
PP_EXPECT(h, std::fabs(std::get<double>(opacity.value()) - 0.75) < 0.0001);
PP_EXPECT(h, tablet.ok());
PP_EXPECT(h, std::get<bool>(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<std::int64_t>(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();
}