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
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")

View File

@@ -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.

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
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.

View File

@@ -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"

View File

@@ -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"

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
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

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();
}