From 23eba079013a78b2d0d119b48f89457dfd7ee43c Mon Sep 17 00:00:00 2001 From: omigamedev Date: Mon, 1 Jun 2026 00:02:42 +0200 Subject: [PATCH] Start document model tests --- CMakeLists.txt | 13 +++ docs/modernization/build-inventory.md | 3 +- docs/modernization/debt.md | 6 +- docs/modernization/roadmap.md | 14 +-- scripts/automation/platform-build.ps1 | 2 +- scripts/automation/platform-build.sh | 2 +- src/document/document.cpp | 144 ++++++++++++++++++++++++++ src/document/document.h | 51 +++++++++ tests/CMakeLists.txt | 10 ++ tests/document/document_tests.cpp | 100 ++++++++++++++++++ tools/pano_cli/CMakeLists.txt | 3 +- tools/pano_cli/main.cpp | 19 +++- 12 files changed, 352 insertions(+), 15 deletions(-) create mode 100644 src/document/document.cpp create mode 100644 src/document/document.h create mode 100644 tests/document/document_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index ef9eb09..0268ab1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,19 @@ target_link_libraries(pp_paint PRIVATE pp_project_warnings) +add_library(pp_document STATIC + src/document/document.cpp) +target_include_directories(pp_document + PUBLIC + "${CMAKE_CURRENT_SOURCE_DIR}/src") +target_link_libraries(pp_document + PUBLIC + pp_foundation + pp_paint + pp_project_options + PRIVATE + pp_project_warnings) + if(PP_BUILD_TOOLS) add_subdirectory(tools/pano_cli) endif() diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index f9d13e5..37b9e5b 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -75,7 +75,8 @@ Known local toolchain state: - Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865` - Android arm64 headless configure/build passes through root CMake and the `platform-build` automation wrapper for `pp_foundation`, `pp_assets`, - `pp_paint`, `pano_cli`, and their current headless test binaries. + `pp_paint`, `pp_document`, `pano_cli`, and their current headless test + binaries. - `vcpkg` is not on PATH yet; see DEBT-0007. Known warnings after the current CMake app build: diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 1a742b1..9ef1b01 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -25,11 +25,13 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0003 | Open | Modernization | Existing singletons remain during initial split | Avoid behavior changes while introducing component boundaries | App launch and component tests | Replace singleton reaches with context/service injection at component boundaries | | DEBT-0004 | Open | Modernization | Android, Linux, WebGL, Apple, and AppX build files remain platform-specific until root CMake alignment reaches them | Prevent platform regressions during incremental migration; raw Windows `.sln/.vcxproj` files were removed on 2026-05-31 by user decision | `cmake --preset windows-msvc-default`; platform-specific configure/build smoke checks as each platform is migrated | Root CMake owns every platform source list and package path | | DEBT-0005 | Open | Modernization | Temporary local CTest harness is used before Catch2 is wired through vcpkg | `vcpkg` is not currently on PATH, but headless tests need to run now | `ctest --preset desktop-fast --build-config Debug` | Replace `tests/test_harness.h` tests with Catch2 tests once vcpkg toolchain/presets are validated | -| DEBT-0006 | Open | Modernization | `pano_cli create-document` validates and emits JSON command contracts but does not yet invoke the legacy document/app model | The document model has not been extracted from `Canvas`/`App` yet | `pano_cli create-document --width 64 --height 32 --layers 2`; CTest `pano_cli_create_document_smoke` | Replace command contract implementation with real `pp_document` creation once Phase 4 extracts the document model | | DEBT-0007 | Open | Modernization | `vcpkg.json` exists but CMake is not yet using a validated vcpkg toolchain on this machine | `vcpkg` is not available on PATH and Visual Studio reports manifest mode is disabled | `cmake --preset windows-msvc-default` currently configures with vendored dependencies | Add validated vcpkg toolchain/preset integration for desktop, Android, and Apple triplets | | DEBT-0008 | Open | Modernization | `windows-msvc-default` preset is used for local validation because the VS 2026 generator is not installed here | The target VS 2026 preset must remain, but this machine configures with Visual Studio 17 2022 | `cmake --preset windows-msvc-default`; `ctest --preset desktop-fast --build-config Debug` | Validate `windows-vs2026-x64` on a machine with Visual Studio 2026 installed and make it the default Windows validation preset | | DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands | +| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/document model but is not yet wired to legacy `Canvas`, PPI load/save, animation frames, or undo/redo | Keep extraction incremental while preserving app behavior | `ctest --preset desktop-fast --build-config Debug`; `pano_cli create-document --width 64 --height 32 --layers 2` | Legacy document behavior is represented by `pp_document` tests and the app consumes it through a boundary/facade | ## Closed Debt -None yet. +| ID | Status | Owner | Item | Reason | Validation | Removal Condition | +| --- | --- | --- | --- | --- | --- | --- | +| DEBT-0006 | Closed | Modernization | `pano_cli create-document` validates and emits JSON command contracts but does not yet invoke the legacy document/app model | The document model had not been extracted from `Canvas`/`App` yet | `ctest --preset desktop-fast --build-config Debug`; `pano_cli_create_document_smoke` | Closed on 2026-05-31: command now constructs a real `pp_document::CanvasDocument` | diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index 84ff58f..41c38dc 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -243,9 +243,9 @@ Goal: make each component reachable by automated tools and future agents. Status: in progress. `tests/` exists, `desktop-fast` runs headlessly, and PowerShell/bash wrappers exist for configure/build/test/analyze/platform-build. -`pano_cli` exists with JSON automation commands for validating create-document -inputs and inspecting image signatures; full document/app integration is -debt-tracked as DEBT-0006. +`pano_cli` exists with JSON automation commands for creating a `pp_document` +model and inspecting image signatures; full document/app integration is +debt-tracked as DEBT-0010. Implementation tasks: @@ -305,8 +305,9 @@ input. A deterministic `TraceRecorder` now records component/name/thread/frame and stroke timing spans with invalid-end tests. `pp_assets` has started with PNG/JPEG signature detection and corrupt/truncated/unsupported tests. `pp_paint` has started with CPU reference math for the five current shader -blend modes. Continue extracting legacy-safe utilities before moving document -behavior. +blend modes. `pp_document` has started with a pure canvas/layer model and +layer invariant tests. Continue expanding document behavior toward legacy +Canvas parity. Implementation tasks: @@ -506,7 +507,7 @@ Last verified on 2026-05-31: ```powershell cmake --preset windows-msvc-default -cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pano_cli PanoPainter +cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_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 @@ -521,6 +522,7 @@ Results: - `pp_foundation_trace_tests` passed. - `pp_assets_image_format_tests` passed. - `pp_paint_blend_tests` passed. +- `pp_document_tests` passed. - `pano_cli_create_document_smoke` passed. - `pano_cli_inspect_image_rejects_unsupported` passed as an expected failure test. diff --git a/scripts/automation/platform-build.ps1 b/scripts/automation/platform-build.ps1 index 6b3bfe6..ca36dd5 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", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_paint_blend_tests") + [string[]]$Targets = @("pp_foundation", "pp_assets", "pp_paint", "pp_document", "pano_cli", "pp_foundation_binary_stream_tests", "pp_foundation_parse_tests", "pp_foundation_trace_tests", "pp_assets_image_format_tests", "pp_paint_blend_tests", "pp_document_tests") ) $ErrorActionPreference = "Stop" diff --git a/scripts/automation/platform-build.sh b/scripts/automation/platform-build.sh index 9f9b842..b5ad5b0 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 pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests}" +targets="${*:-pp_foundation pp_assets pp_paint pp_document pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests pp_foundation_trace_tests pp_assets_image_format_tests pp_paint_blend_tests pp_document_tests}" start="$(date +%s)" cmake --preset "$preset" diff --git a/src/document/document.cpp b/src/document/document.cpp new file mode 100644 index 0000000..a31dc32 --- /dev/null +++ b/src/document/document.cpp @@ -0,0 +1,144 @@ +#include "document/document.h" + +#include + +namespace pp::document { + +namespace { + +[[nodiscard]] pp::foundation::Status validate_config(DocumentConfig config) noexcept +{ + if (config.width == 0 || config.height == 0) { + return pp::foundation::Status::invalid_argument("document dimensions must be greater than zero"); + } + + if (config.width > max_canvas_dimension || config.height > max_canvas_dimension) { + return pp::foundation::Status::out_of_range("document dimensions exceed the configured limit"); + } + + if (config.layer_count == 0) { + return pp::foundation::Status::invalid_argument("document must contain at least one layer"); + } + + if (config.layer_count > max_layer_count) { + return pp::foundation::Status::out_of_range("document layer count exceeds the configured limit"); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] std::string default_layer_name(std::size_t index) +{ + return "Layer " + std::to_string(index + 1U); +} + +} + +pp::foundation::Result CanvasDocument::create(DocumentConfig config) +{ + const auto status = validate_config(config); + if (!status.ok()) { + return pp::foundation::Result::failure(status); + } + + CanvasDocument document; + document.width_ = config.width; + document.height_ = config.height; + document.layers_.reserve(config.layer_count); + for (std::uint32_t i = 0; i < config.layer_count; ++i) { + document.layers_.push_back(Layer { .name = default_layer_name(i) }); + } + + return pp::foundation::Result::success(document); +} + +std::uint32_t CanvasDocument::width() const noexcept +{ + return width_; +} + +std::uint32_t CanvasDocument::height() const noexcept +{ + return height_; +} + +std::size_t CanvasDocument::active_layer_index() const noexcept +{ + return active_layer_index_; +} + +std::span CanvasDocument::layers() const noexcept +{ + return layers_; +} + +pp::foundation::Result CanvasDocument::add_layer(std::string_view name) +{ + if (layers_.size() >= max_layer_count) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("document layer count exceeds the configured limit")); + } + + Layer layer; + layer.name = name.empty() ? default_layer_name(layers_.size()) : std::string(name); + layers_.push_back(layer); + active_layer_index_ = layers_.size() - 1U; + return pp::foundation::Result::success(active_layer_index_); +} + +pp::foundation::Status CanvasDocument::remove_layer(std::size_t index) +{ + if (index >= layers_.size()) { + return pp::foundation::Status::out_of_range("layer index is outside the document"); + } + + if (layers_.size() == 1U) { + return pp::foundation::Status::invalid_argument("document must keep at least one layer"); + } + + layers_.erase(layers_.begin() + static_cast(index)); + if (active_layer_index_ >= layers_.size()) { + active_layer_index_ = layers_.size() - 1U; + } else if (active_layer_index_ > index) { + --active_layer_index_; + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Status CanvasDocument::move_layer(std::size_t from, std::size_t to) +{ + if (from >= layers_.size() || to >= layers_.size()) { + return pp::foundation::Status::out_of_range("layer index is outside the document"); + } + + if (from == to) { + return pp::foundation::Status::success(); + } + + auto layer = layers_[from]; + layers_.erase(layers_.begin() + static_cast(from)); + layers_.insert(layers_.begin() + static_cast(to), layer); + + if (active_layer_index_ == from) { + active_layer_index_ = to; + } else if (from < active_layer_index_ && active_layer_index_ <= to) { + --active_layer_index_; + } else if (to <= active_layer_index_ && active_layer_index_ < from) { + ++active_layer_index_; + } + + return pp::foundation::Status::success(); +} + +pp::foundation::Status CanvasDocument::set_active_layer(std::size_t index) noexcept +{ + if (index >= layers_.size()) { + return pp::foundation::Status::out_of_range("layer index is outside the document"); + } + + active_layer_index_ = index; + return pp::foundation::Status::success(); +} + +} diff --git a/src/document/document.h b/src/document/document.h new file mode 100644 index 0000000..64af7f6 --- /dev/null +++ b/src/document/document.h @@ -0,0 +1,51 @@ +#pragma once + +#include "foundation/result.h" +#include "paint/blend.h" + +#include +#include +#include +#include +#include + +namespace pp::document { + +constexpr std::uint32_t max_canvas_dimension = 131072; +constexpr std::uint32_t max_layer_count = 1024; + +struct DocumentConfig { + std::uint32_t width = 0; + std::uint32_t height = 0; + std::uint32_t layer_count = 1; +}; + +struct Layer { + std::string name; + bool visible = true; + float opacity = 1.0F; + pp::paint::BlendMode blend_mode = pp::paint::BlendMode::normal; +}; + +class CanvasDocument { +public: + [[nodiscard]] static pp::foundation::Result create(DocumentConfig config); + + [[nodiscard]] std::uint32_t width() const noexcept; + [[nodiscard]] std::uint32_t height() const noexcept; + [[nodiscard]] std::size_t active_layer_index() const noexcept; + [[nodiscard]] std::span layers() const noexcept; + + [[nodiscard]] pp::foundation::Result add_layer(std::string_view name); + [[nodiscard]] pp::foundation::Status remove_layer(std::size_t index); + [[nodiscard]] pp::foundation::Status move_layer(std::size_t from, std::size_t to); + [[nodiscard]] pp::foundation::Status set_active_layer(std::size_t index) noexcept; + +private: + std::uint32_t width_ = 0; + std::uint32_t height_ = 0; + std::size_t active_layer_index_ = 0; + std::vector layers_; +}; + +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index bcf27b4..cc0ea09 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -56,6 +56,16 @@ add_test(NAME pp_paint_blend_tests COMMAND pp_paint_blend_tests) set_tests_properties(pp_paint_blend_tests PROPERTIES LABELS "paint;desktop-fast") +add_executable(pp_document_tests + document/document_tests.cpp) +target_link_libraries(pp_document_tests PRIVATE + pp_document + pp_test_harness) + +add_test(NAME pp_document_tests COMMAND pp_document_tests) +set_tests_properties(pp_document_tests PROPERTIES + LABELS "document;desktop-fast") + if(TARGET pano_cli) add_test(NAME pano_cli_create_document_smoke COMMAND pano_cli create-document --width 64 --height 32 --layers 2) diff --git a/tests/document/document_tests.cpp b/tests/document/document_tests.cpp new file mode 100644 index 0000000..91f7389 --- /dev/null +++ b/tests/document/document_tests.cpp @@ -0,0 +1,100 @@ +#include "document/document.h" +#include "test_harness.h" + +#include + +using pp::document::CanvasDocument; +using pp::document::DocumentConfig; +using pp::document::max_canvas_dimension; +using pp::document::max_layer_count; +using pp::foundation::StatusCode; + +namespace { + +void creates_document_with_default_layers(pp::tests::Harness& h) +{ + const auto document = CanvasDocument::create( + DocumentConfig { .width = 128, .height = 64, .layer_count = 2 }); + + PP_EXPECT(h, document.ok()); + PP_EXPECT(h, document.value().width() == 128U); + PP_EXPECT(h, document.value().height() == 64U); + PP_EXPECT(h, document.value().layers().size() == 2U); + PP_EXPECT(h, document.value().layers()[0].name == std::string_view("Layer 1")); + PP_EXPECT(h, document.value().layers()[1].name == std::string_view("Layer 2")); + PP_EXPECT(h, document.value().active_layer_index() == 0U); +} + +void rejects_invalid_document_configs(pp::tests::Harness& h) +{ + const auto zero_width = CanvasDocument::create( + DocumentConfig { .width = 0, .height = 64, .layer_count = 1 }); + const auto huge_width = CanvasDocument::create( + DocumentConfig { .width = max_canvas_dimension + 1U, .height = 64, .layer_count = 1 }); + const auto no_layers = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 64, .layer_count = 0 }); + const auto too_many_layers = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 64, .layer_count = max_layer_count + 1U }); + + PP_EXPECT(h, !zero_width.ok()); + PP_EXPECT(h, zero_width.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !huge_width.ok()); + PP_EXPECT(h, huge_width.status().code == StatusCode::out_of_range); + PP_EXPECT(h, !no_layers.ok()); + PP_EXPECT(h, no_layers.status().code == StatusCode::invalid_argument); + PP_EXPECT(h, !too_many_layers.ok()); + PP_EXPECT(h, too_many_layers.status().code == StatusCode::out_of_range); +} + +void manages_layer_add_remove_and_active_index(pp::tests::Harness& h) +{ + auto document_result = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 64, .layer_count = 1 }); + PP_EXPECT(h, document_result.ok()); + auto document = document_result.value(); + + const auto added = document.add_layer("Paint"); + PP_EXPECT(h, added.ok()); + PP_EXPECT(h, added.value() == 1U); + PP_EXPECT(h, document.active_layer_index() == 1U); + PP_EXPECT(h, document.layers()[1].name == std::string_view("Paint")); + + PP_EXPECT(h, document.remove_layer(0).ok()); + PP_EXPECT(h, document.layers().size() == 1U); + PP_EXPECT(h, document.active_layer_index() == 0U); + + const auto remove_last = document.remove_layer(0); + PP_EXPECT(h, !remove_last.ok()); + PP_EXPECT(h, remove_last.code == StatusCode::invalid_argument); +} + +void moves_layers_and_preserves_active_layer_identity(pp::tests::Harness& h) +{ + auto document_result = CanvasDocument::create( + DocumentConfig { .width = 64, .height = 64, .layer_count = 3 }); + PP_EXPECT(h, document_result.ok()); + auto document = document_result.value(); + + PP_EXPECT(h, document.set_active_layer(2).ok()); + PP_EXPECT(h, document.move_layer(2, 0).ok()); + PP_EXPECT(h, document.active_layer_index() == 0U); + PP_EXPECT(h, document.layers()[0].name == std::string_view("Layer 3")); + PP_EXPECT(h, document.layers()[1].name == std::string_view("Layer 1")); + PP_EXPECT(h, document.layers()[2].name == std::string_view("Layer 2")); + + const auto bad_move = document.move_layer(4, 0); + PP_EXPECT(h, !bad_move.ok()); + PP_EXPECT(h, bad_move.code == StatusCode::out_of_range); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run("creates_document_with_default_layers", creates_document_with_default_layers); + harness.run("rejects_invalid_document_configs", rejects_invalid_document_configs); + harness.run("manages_layer_add_remove_and_active_index", manages_layer_add_remove_and_active_index); + harness.run("moves_layers_and_preserves_active_layer_identity", moves_layers_and_preserves_active_layer_identity); + return harness.finish(); +} diff --git a/tools/pano_cli/CMakeLists.txt b/tools/pano_cli/CMakeLists.txt index d8fc448..20ecebf 100644 --- a/tools/pano_cli/CMakeLists.txt +++ b/tools/pano_cli/CMakeLists.txt @@ -4,4 +4,5 @@ target_link_libraries(pano_cli PRIVATE pp_project_options pp_project_warnings pp_foundation - pp_assets) + pp_assets + pp_document) diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index da4967b..d63d874 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,4 +1,5 @@ #include "assets/image_format.h" +#include "document/document.h" #include "foundation/parse.h" #include "foundation/result.h" @@ -83,10 +84,22 @@ int create_document(int argc, char** argv) return 2; } + const auto document = pp::document::CanvasDocument::create( + pp::document::DocumentConfig { + .width = args.width, + .height = args.height, + .layer_count = args.layers, + }); + if (!document) { + print_error("create-document", document.status().message); + return 2; + } + std::cout << "{\"ok\":true,\"command\":\"create-document\",\"document\":{" - << "\"width\":" << args.width - << ",\"height\":" << args.height - << ",\"layers\":" << args.layers + << "\"width\":" << document.value().width() + << ",\"height\":" << document.value().height() + << ",\"layers\":" << document.value().layers().size() + << ",\"activeLayer\":" << document.value().active_layer_index() << "}}\n"; return 0; }