Add document undo history tests
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# Modernization Debt Log
|
||||
|
||||
Status: live
|
||||
Last updated: 2026-05-31
|
||||
Last updated: 2026-06-01
|
||||
|
||||
Every shortcut, temporary adapter, retained vendored dependency, skipped
|
||||
platform gate, compatibility shim, or incomplete automation path must be
|
||||
@@ -28,7 +28,7 @@ agent or engineer to remove them without reconstructing context from chat.
|
||||
| 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/frame/document model but is not yet wired to legacy `Canvas`, PPI load/save, selection masks, 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 |
|
||||
| DEBT-0010 | Open | Modernization | `pp_document` is a pure layer/frame/document/undo-history model but is not yet wired to legacy `Canvas`, PPI load/save, selection masks, or legacy action commands | 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 |
|
||||
| DEBT-0011 | Open | Modernization | `package-smoke` validates the Windows CMake app artifact only, not AppX/APK/Apple/WebGL package outputs | Platform package targets are not migrated to root CMake yet | `powershell -ExecutionPolicy Bypass -File scripts\automation\package-smoke.ps1 -Preset windows-msvc-default -Configuration Debug` | Package-smoke covers Windows AppX, Android APK variants, Apple bundles, and WebGL output where local toolchains are present |
|
||||
| DEBT-0012 | Open | Modernization | `pp_vendor_tinyxml2` compiles the retained vendored tinyxml2 copy for `pp_ui_core` layout parsing | vcpkg is not validated yet, but layout parsing needs a structured XML parser now | `ctest --preset desktop-fast --build-config Debug`; `powershell -ExecutionPolicy Bypass -File scripts\automation\platform-build.ps1 -Presets android-arm64` | Replace with vcpkg tinyxml2 target once desktop and mobile triplets are validated |
|
||||
| DEBT-0013 | Open | Modernization | `pp_assets` and `pano_cli inspect-project` recognize only the fixed PPI header, not thumbnail bytes or the project body | Full PPI parsing requires staged extraction of legacy `Canvas` serialization and image/layer payload handling | `ctest --preset desktop-fast --build-config Debug`; `pp_assets_ppi_header_tests` | Full PPI load/save fixtures cover thumbnail, layers, frames, metadata, corrupt payloads, and round-trip compatibility |
|
||||
|
||||
@@ -309,8 +309,8 @@ PNG/JPEG signature detection plus PPI header recognition, with
|
||||
corrupt/truncated/unsupported tests.
|
||||
`pp_paint` has started with CPU reference math for the five current shader
|
||||
blend modes. `pp_document` has started with a pure canvas/layer/frame model
|
||||
and layer/frame invariant tests. `pp_renderer_api` has started with renderer-neutral
|
||||
texture/readback descriptors and validation tests. `pp_paint_renderer` has
|
||||
and layer/frame/undo-redo history invariant tests. `pp_renderer_api` has started
|
||||
with renderer-neutral texture/readback descriptors and validation tests. `pp_paint_renderer` has
|
||||
started with deterministic CPU layer compositing over renderer extents using
|
||||
the paint blend reference. `pp_ui_core` has started with XML-layout-facing
|
||||
length parsing, tinyxml-backed layout XML parsing, and invalid input tests.
|
||||
|
||||
@@ -231,4 +231,91 @@ pp::foundation::Status CanvasDocument::set_active_frame(std::size_t index) noexc
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Result<DocumentHistory> DocumentHistory::create(
|
||||
CanvasDocument initial_document,
|
||||
std::size_t max_entries)
|
||||
{
|
||||
if (max_entries < min_document_history_entries) {
|
||||
return pp::foundation::Result<DocumentHistory>::failure(
|
||||
pp::foundation::Status::invalid_argument("document history must keep at least two entries"));
|
||||
}
|
||||
|
||||
if (max_entries > max_document_history_entries) {
|
||||
return pp::foundation::Result<DocumentHistory>::failure(
|
||||
pp::foundation::Status::out_of_range("document history entry limit exceeds the configured limit"));
|
||||
}
|
||||
|
||||
DocumentHistory history;
|
||||
history.max_entries_ = max_entries;
|
||||
history.entries_.reserve(max_entries);
|
||||
history.entries_.push_back(initial_document);
|
||||
return pp::foundation::Result<DocumentHistory>::success(history);
|
||||
}
|
||||
|
||||
const CanvasDocument& DocumentHistory::current() const noexcept
|
||||
{
|
||||
return entries_[current_index_];
|
||||
}
|
||||
|
||||
std::size_t DocumentHistory::size() const noexcept
|
||||
{
|
||||
return entries_.size();
|
||||
}
|
||||
|
||||
std::size_t DocumentHistory::current_index() const noexcept
|
||||
{
|
||||
return current_index_;
|
||||
}
|
||||
|
||||
bool DocumentHistory::can_undo() const noexcept
|
||||
{
|
||||
return current_index_ > 0;
|
||||
}
|
||||
|
||||
bool DocumentHistory::can_redo() const noexcept
|
||||
{
|
||||
return current_index_ + 1U < entries_.size();
|
||||
}
|
||||
|
||||
pp::foundation::Status DocumentHistory::apply(CanvasDocument next_document)
|
||||
{
|
||||
if (entries_.empty()) {
|
||||
return pp::foundation::Status::invalid_argument("document history is not initialized");
|
||||
}
|
||||
|
||||
if (can_redo()) {
|
||||
entries_.erase(entries_.begin() + static_cast<std::ptrdiff_t>(current_index_ + 1U), entries_.end());
|
||||
}
|
||||
|
||||
entries_.push_back(next_document);
|
||||
if (entries_.size() > max_entries_) {
|
||||
entries_.erase(entries_.begin());
|
||||
} else {
|
||||
++current_index_;
|
||||
}
|
||||
|
||||
current_index_ = entries_.size() - 1U;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status DocumentHistory::undo() noexcept
|
||||
{
|
||||
if (!can_undo()) {
|
||||
return pp::foundation::Status::out_of_range("document history has no undo entry");
|
||||
}
|
||||
|
||||
--current_index_;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
pp::foundation::Status DocumentHistory::redo() noexcept
|
||||
{
|
||||
if (!can_redo()) {
|
||||
return pp::foundation::Status::out_of_range("document history has no redo entry");
|
||||
}
|
||||
|
||||
++current_index_;
|
||||
return pp::foundation::Status::success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ constexpr std::uint32_t max_canvas_dimension = 131072;
|
||||
constexpr std::uint32_t max_layer_count = 1024;
|
||||
constexpr std::uint32_t max_frame_count = 100000;
|
||||
constexpr std::uint32_t min_frame_duration_ms = 1;
|
||||
constexpr std::size_t min_document_history_entries = 2;
|
||||
constexpr std::size_t max_document_history_entries = 10000;
|
||||
|
||||
struct DocumentConfig {
|
||||
std::uint32_t width = 0;
|
||||
@@ -64,4 +66,26 @@ private:
|
||||
std::vector<AnimationFrame> frames_;
|
||||
};
|
||||
|
||||
class DocumentHistory {
|
||||
public:
|
||||
[[nodiscard]] static pp::foundation::Result<DocumentHistory> create(
|
||||
CanvasDocument initial_document,
|
||||
std::size_t max_entries = 256);
|
||||
|
||||
[[nodiscard]] const CanvasDocument& current() const noexcept;
|
||||
[[nodiscard]] std::size_t size() const noexcept;
|
||||
[[nodiscard]] std::size_t current_index() const noexcept;
|
||||
[[nodiscard]] bool can_undo() const noexcept;
|
||||
[[nodiscard]] bool can_redo() const noexcept;
|
||||
|
||||
[[nodiscard]] pp::foundation::Status apply(CanvasDocument next_document);
|
||||
[[nodiscard]] pp::foundation::Status undo() noexcept;
|
||||
[[nodiscard]] pp::foundation::Status redo() noexcept;
|
||||
|
||||
private:
|
||||
std::size_t max_entries_ = 0;
|
||||
std::size_t current_index_ = 0;
|
||||
std::vector<CanvasDocument> entries_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
#include <string_view>
|
||||
|
||||
using pp::document::CanvasDocument;
|
||||
using pp::document::DocumentHistory;
|
||||
using pp::document::DocumentConfig;
|
||||
using pp::document::max_document_history_entries;
|
||||
using pp::document::max_canvas_dimension;
|
||||
using pp::document::max_frame_count;
|
||||
using pp::document::max_layer_count;
|
||||
@@ -145,6 +147,117 @@ void rejects_invalid_animation_frame_operations(pp::tests::Harness& h)
|
||||
PP_EXPECT(h, max_frame_count > document.frames().size());
|
||||
}
|
||||
|
||||
void records_document_history_and_restores_snapshots(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
|
||||
auto history_result = DocumentHistory::create(document_result.value(), 4);
|
||||
PP_EXPECT(h, history_result.ok());
|
||||
auto history = history_result.value();
|
||||
|
||||
auto with_layer = history.current();
|
||||
const auto added_layer = with_layer.add_layer("Paint");
|
||||
PP_EXPECT(h, added_layer.ok());
|
||||
PP_EXPECT(h, history.apply(with_layer).ok());
|
||||
|
||||
auto with_frame = history.current();
|
||||
const auto added_frame = with_frame.add_frame(250);
|
||||
PP_EXPECT(h, added_frame.ok());
|
||||
PP_EXPECT(h, history.apply(with_frame).ok());
|
||||
|
||||
PP_EXPECT(h, history.size() == 3U);
|
||||
PP_EXPECT(h, history.current_index() == 2U);
|
||||
PP_EXPECT(h, history.current().layers().size() == 2U);
|
||||
PP_EXPECT(h, history.current().frames().size() == 2U);
|
||||
PP_EXPECT(h, history.can_undo());
|
||||
PP_EXPECT(h, !history.can_redo());
|
||||
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.current().layers().size() == 2U);
|
||||
PP_EXPECT(h, history.current().frames().size() == 1U);
|
||||
PP_EXPECT(h, history.can_redo());
|
||||
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.current().layers().size() == 1U);
|
||||
PP_EXPECT(h, history.current().frames().size() == 1U);
|
||||
const auto undo_past_start = history.undo();
|
||||
PP_EXPECT(h, !undo_past_start.ok());
|
||||
PP_EXPECT(h, undo_past_start.code == StatusCode::out_of_range);
|
||||
|
||||
PP_EXPECT(h, history.redo().ok());
|
||||
PP_EXPECT(h, history.current().layers().size() == 2U);
|
||||
}
|
||||
|
||||
void applying_after_undo_discards_redo_branch(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
|
||||
auto history_result = DocumentHistory::create(document_result.value(), 5);
|
||||
PP_EXPECT(h, history_result.ok());
|
||||
auto history = history_result.value();
|
||||
|
||||
auto first_branch = history.current();
|
||||
PP_EXPECT(h, first_branch.add_layer("Branch A").ok());
|
||||
PP_EXPECT(h, history.apply(first_branch).ok());
|
||||
|
||||
auto second_branch = history.current();
|
||||
PP_EXPECT(h, second_branch.add_layer("Branch B").ok());
|
||||
PP_EXPECT(h, history.apply(second_branch).ok());
|
||||
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.can_redo());
|
||||
|
||||
auto replacement_branch = history.current();
|
||||
PP_EXPECT(h, replacement_branch.add_layer("Replacement").ok());
|
||||
PP_EXPECT(h, history.apply(replacement_branch).ok());
|
||||
|
||||
PP_EXPECT(h, !history.can_redo());
|
||||
PP_EXPECT(h, history.current().layers().size() == 3U);
|
||||
PP_EXPECT(h, history.current().layers()[2].name == std::string_view("Replacement"));
|
||||
}
|
||||
|
||||
void bounds_document_history_capacity(pp::tests::Harness& h)
|
||||
{
|
||||
auto document_result = CanvasDocument::create(
|
||||
DocumentConfig { .width = 64, .height = 64, .layer_count = 1 });
|
||||
PP_EXPECT(h, document_result.ok());
|
||||
|
||||
auto too_small = DocumentHistory::create(document_result.value(), 1);
|
||||
auto too_large = DocumentHistory::create(document_result.value(), max_document_history_entries + 1U);
|
||||
PP_EXPECT(h, !too_small.ok());
|
||||
PP_EXPECT(h, too_small.status().code == StatusCode::invalid_argument);
|
||||
PP_EXPECT(h, !too_large.ok());
|
||||
PP_EXPECT(h, too_large.status().code == StatusCode::out_of_range);
|
||||
|
||||
auto history_result = DocumentHistory::create(document_result.value(), 3);
|
||||
PP_EXPECT(h, history_result.ok());
|
||||
auto history = history_result.value();
|
||||
|
||||
for (std::uint32_t i = 0; i < 5U; ++i) {
|
||||
auto next = history.current();
|
||||
const auto added = next.add_frame(100U + i);
|
||||
PP_EXPECT(h, added.ok());
|
||||
PP_EXPECT(h, history.apply(next).ok());
|
||||
PP_EXPECT(h, history.size() <= 3U);
|
||||
}
|
||||
|
||||
PP_EXPECT(h, history.size() == 3U);
|
||||
PP_EXPECT(h, history.current_index() == 2U);
|
||||
PP_EXPECT(h, history.current().frames().size() == 6U);
|
||||
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.current().frames().size() == 5U);
|
||||
PP_EXPECT(h, history.undo().ok());
|
||||
PP_EXPECT(h, history.current().frames().size() == 4U);
|
||||
const auto undo_evicted_entry = history.undo();
|
||||
PP_EXPECT(h, !undo_evicted_entry.ok());
|
||||
PP_EXPECT(h, undo_evicted_entry.code == StatusCode::out_of_range);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
int main()
|
||||
@@ -156,5 +269,8 @@ int main()
|
||||
harness.run("moves_layers_and_preserves_active_layer_identity", moves_layers_and_preserves_active_layer_identity);
|
||||
harness.run("manages_animation_frames_and_duration", manages_animation_frames_and_duration);
|
||||
harness.run("rejects_invalid_animation_frame_operations", rejects_invalid_animation_frame_operations);
|
||||
harness.run("records_document_history_and_restores_snapshots", records_document_history_and_restores_snapshots);
|
||||
harness.run("applying_after_undo_discards_redo_branch", applying_after_undo_discards_redo_branch);
|
||||
harness.run("bounds_document_history_capacity", bounds_document_history_capacity);
|
||||
return harness.finish();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user