#include "app_core/document_canvas.h" #include "assets/ppi_header.h" #include "test_harness.h" #include #include #include #include namespace { class FakeDocumentCanvasClearServices final : public pp::app::DocumentCanvasClearServices { public: void clear_current_canvas(float r, float g, float b, float a) override { clear_calls += 1; last_r = r; last_g = g; last_b = b; last_a = a; call_order += "clear;"; } int clear_calls = 0; float last_r = 0.0F; float last_g = 0.0F; float last_b = 0.0F; float last_a = 0.0F; std::string call_order; }; void snapshot_plan_projects_canvas_metadata(pp::tests::Harness& harness) { const std::uint32_t base_frames[] { 120U, 240U }; const std::uint32_t paint_frames[] { 180U }; const pp::app::DocumentCanvasLayerSnapshotInput layers[] { { .name = "Base", .visible = false, .alpha_locked = true, .opacity = 0.5F, .blend_mode = 2, .frame_durations_ms = std::span(base_frames), .pending_face_payloads = 6U, }, { .name = "Paint", .visible = true, .alpha_locked = false, .opacity = 0.75F, .blend_mode = 4, .frame_durations_ms = std::span(paint_frames), .pending_face_payloads = 3U, }, }; const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { .has_canvas = true, .width = 64U, .height = 32U, .active_layer_index = 1U, .active_frame_index = 1U, .layers = std::span(layers), }); PP_EXPECT(harness, result); if (!result) { return; } const auto& value = result.value(); PP_EXPECT(harness, value.layer_count == 2U); PP_EXPECT(harness, value.frame_count == 2U); PP_EXPECT(harness, value.pending_face_payloads == 9U); PP_EXPECT(harness, value.metadata_only); PP_EXPECT(harness, value.requires_renderer_payload_readback); PP_EXPECT(harness, value.document.width() == 64U); PP_EXPECT(harness, value.document.height() == 32U); PP_EXPECT(harness, value.document.active_layer_index() == 1U); PP_EXPECT(harness, value.document.active_frame_index() == 1U); PP_EXPECT(harness, value.document.layers().size() == 2U); PP_EXPECT(harness, value.document.frames().size() == 2U); PP_EXPECT(harness, value.document.layers()[0].name == "Base"); PP_EXPECT(harness, !value.document.layers()[0].visible); PP_EXPECT(harness, value.document.layers()[0].alpha_locked); PP_EXPECT(harness, value.document.layers()[0].opacity == 0.5F); PP_EXPECT(harness, value.document.layers()[0].blend_mode == pp::paint::BlendMode::screen); PP_EXPECT(harness, value.document.layers()[0].frames[1].duration_ms == 240U); PP_EXPECT(harness, value.document.layers()[1].name == "Paint"); PP_EXPECT(harness, value.document.layers()[1].blend_mode == pp::paint::BlendMode::overlay); PP_EXPECT(harness, value.document.layers()[1].frames.size() == 1U); PP_EXPECT(harness, value.document.face_pixel_payload_count() == 0U); const auto export_result = pp::app::export_document_canvas_save_snapshot_to_ppi(value); PP_EXPECT(harness, !export_result); PP_EXPECT(harness, export_result.status().code == pp::foundation::StatusCode::invalid_argument); } void snapshot_plan_defaults_empty_names_and_frames(pp::tests::Harness& harness) { const pp::app::DocumentCanvasLayerSnapshotInput layers[] { { .name = "", .frame_durations_ms = {}, }, }; const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { .has_canvas = true, .width = 16U, .height = 8U, .layers = std::span(layers), }); PP_EXPECT(harness, result); if (!result) { return; } PP_EXPECT(harness, result.value().frame_count == 1U); PP_EXPECT(harness, result.value().pending_face_payloads == 0U); PP_EXPECT(harness, !result.value().requires_renderer_payload_readback); PP_EXPECT(harness, result.value().document.layers()[0].name == "Layer 1"); PP_EXPECT(harness, result.value().document.frames()[0].duration_ms == 100U); const auto export_result = pp::app::export_document_canvas_save_snapshot_to_ppi(result.value()); PP_EXPECT(harness, export_result); if (export_result) { const auto decoded = pp::assets::decode_ppi_project_images(export_result.value().bytes); PP_EXPECT(harness, decoded); if (decoded) { PP_EXPECT(harness, decoded.value().project.body.summary.dirty_face_count == 0U); } } } void snapshot_plan_attaches_captured_face_payloads(pp::tests::Harness& harness) { const std::uint32_t frames[] { 100U }; const std::uint8_t rgba[] { 255U, 0U, 0U, 255U, 0U, 255U, 0U, 255U, }; const pp::app::DocumentCanvasFacePayloadInput payloads[] { { .frame_index = 0U, .face_index = 2U, .x = 3U, .y = 4U, .width = 2U, .height = 1U, .rgba8 = std::span(rgba), }, }; const pp::app::DocumentCanvasLayerSnapshotInput layers[] { { .name = "Paint", .frame_durations_ms = std::span(frames), .pending_face_payloads = 1U, .captured_face_payloads = std::span(payloads), }, }; const auto result = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { .has_canvas = true, .width = 16U, .height = 8U, .layers = std::span(layers), }); PP_EXPECT(harness, result); if (!result) { return; } PP_EXPECT(harness, result.value().pending_face_payloads == 1U); PP_EXPECT(harness, result.value().captured_face_payloads == 1U); PP_EXPECT(harness, !result.value().metadata_only); PP_EXPECT(harness, !result.value().requires_renderer_payload_readback); PP_EXPECT(harness, result.value().document.face_pixel_payload_count() == 1U); PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].face_index == 2U); PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].x == 3U); PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].rgba8[4] == 0U); PP_EXPECT(harness, result.value().document.layers()[0].frames[0].face_pixels[0].rgba8[5] == 255U); const auto report = pp::app::make_document_canvas_save_snapshot_report(result.value()); PP_EXPECT(harness, report.width == 16U); PP_EXPECT(harness, report.height == 8U); PP_EXPECT(harness, report.layer_count == 1U); PP_EXPECT(harness, report.frame_count == 1U); PP_EXPECT(harness, report.pending_face_payloads == 1U); PP_EXPECT(harness, report.captured_face_payloads == 1U); PP_EXPECT(harness, report.payload_complete); PP_EXPECT(harness, report.can_export_ppi); const auto export_result = pp::app::export_document_canvas_save_snapshot_to_ppi(result.value()); PP_EXPECT(harness, export_result); if (export_result) { PP_EXPECT(harness, export_result.value().report.can_export_ppi); PP_EXPECT(harness, export_result.value().bytes.size() > 0U); const auto decoded = pp::assets::decode_ppi_project_images(export_result.value().bytes); PP_EXPECT(harness, decoded); if (decoded) { PP_EXPECT(harness, decoded.value().project.body.summary.dirty_face_count == 1U); PP_EXPECT(harness, decoded.value().project.body.summary.rgba_face_payload_count == 1U); } } } void save_writer_route_uses_ppi_writer_for_complete_payloads(pp::tests::Harness& harness) { pp::app::DocumentCanvasSaveSnapshotReport report; report.payload_complete = true; report.can_export_ppi = true; const auto plan = pp::app::plan_document_canvas_save_writer_route(report); PP_EXPECT(harness, plan.action == pp::app::DocumentCanvasSaveWriterAction::use_document_ppi_writer); PP_EXPECT(harness, plan.uses_document_ppi_writer); PP_EXPECT(harness, plan.payload_complete); PP_EXPECT(harness, plan.can_export_ppi); PP_EXPECT(harness, plan.fallback_reason.empty()); } void save_writer_route_falls_back_for_pending_payloads(pp::tests::Harness& harness) { pp::app::DocumentCanvasSaveSnapshotReport report; report.payload_complete = false; report.can_export_ppi = false; const auto plan = pp::app::plan_document_canvas_save_writer_route(report); PP_EXPECT(harness, plan.action == pp::app::DocumentCanvasSaveWriterAction::use_legacy_project_save); PP_EXPECT(harness, !plan.uses_document_ppi_writer); PP_EXPECT(harness, !plan.payload_complete); PP_EXPECT(harness, !plan.can_export_ppi); PP_EXPECT( harness, plan.fallback_reason == "canvas document snapshot still requires renderer payload readback"); } void project_save_target_plan_preserves_legacy_paths(pp::tests::Harness& harness) { const auto plan = pp::app::plan_document_canvas_project_save_target( "D:/Paint/data", "D:/Paint/projects/demo.ppi"); PP_EXPECT(harness, plan); if (!plan) { return; } PP_EXPECT(harness, plan.value().target_path == "D:/Paint/projects/demo.ppi"); PP_EXPECT(harness, plan.value().file_name == "demo"); PP_EXPECT(harness, plan.value().temporary_path == "D:/Paint/data/demo.tmp.ppi"); PP_EXPECT(harness, plan.value().timelapse_path == "D:/Paint/data/demo.pptl"); } void project_save_target_plan_accepts_windows_backslashes(pp::tests::Harness& harness) { const auto plan = pp::app::plan_document_canvas_project_save_target( "D:/Paint/data", "D:\\Paint\\projects\\demo.ppi"); PP_EXPECT(harness, plan); if (!plan) { return; } PP_EXPECT(harness, plan.value().file_name == "demo"); PP_EXPECT(harness, plan.value().temporary_path == "D:/Paint/data/demo.tmp.ppi"); PP_EXPECT(harness, plan.value().timelapse_path == "D:/Paint/data/demo.pptl"); } void project_save_target_plan_rejects_empty_inputs(pp::tests::Harness& harness) { const auto no_data = pp::app::plan_document_canvas_project_save_target("", "D:/Paint/demo.ppi"); const auto no_target = pp::app::plan_document_canvas_project_save_target("D:/Paint/data", ""); const auto no_name = pp::app::plan_document_canvas_project_save_target("D:/Paint/data", "D:/Paint/"); PP_EXPECT(harness, !no_data); PP_EXPECT(harness, !no_target); PP_EXPECT(harness, !no_name); PP_EXPECT(harness, no_data.status().code == pp::foundation::StatusCode::invalid_argument); PP_EXPECT(harness, no_target.status().code == pp::foundation::StatusCode::invalid_argument); PP_EXPECT(harness, no_name.status().code == pp::foundation::StatusCode::invalid_argument); } void project_save_write_plan_writes_direct_for_new_targets(pp::tests::Harness& harness) { const auto target = pp::app::plan_document_canvas_project_save_target( "D:/Paint/data", "D:/Paint/projects/demo.ppi"); PP_EXPECT(harness, target); if (!target) { return; } const auto plan = pp::app::plan_document_canvas_project_save_write(target.value(), false); PP_EXPECT(harness, plan); if (!plan) { return; } PP_EXPECT(harness, plan.value().action == pp::app::DocumentCanvasProjectSaveWriteAction::write_direct_to_target); PP_EXPECT(harness, plan.value().write_path == "D:/Paint/projects/demo.ppi"); PP_EXPECT(harness, plan.value().target_path == "D:/Paint/projects/demo.ppi"); PP_EXPECT(harness, !plan.value().target_exists); PP_EXPECT(harness, !plan.value().uses_temporary); PP_EXPECT(harness, !plan.value().falls_back_to_direct_on_temporary_open_failure); } void project_save_write_plan_prefers_temporary_for_existing_targets(pp::tests::Harness& harness) { const auto target = pp::app::plan_document_canvas_project_save_target( "D:/Paint/data", "D:/Paint/projects/demo.ppi"); PP_EXPECT(harness, target); if (!target) { return; } const auto plan = pp::app::plan_document_canvas_project_save_write(target.value(), true); PP_EXPECT(harness, plan); if (!plan) { return; } PP_EXPECT(harness, plan.value().action == pp::app::DocumentCanvasProjectSaveWriteAction::write_temporary_then_swap); PP_EXPECT(harness, plan.value().write_path == "D:/Paint/data/demo.tmp.ppi"); PP_EXPECT(harness, plan.value().target_path == "D:/Paint/projects/demo.ppi"); PP_EXPECT(harness, plan.value().temporary_path == "D:/Paint/data/demo.tmp.ppi"); PP_EXPECT(harness, plan.value().target_exists); PP_EXPECT(harness, plan.value().uses_temporary); PP_EXPECT(harness, plan.value().falls_back_to_direct_on_temporary_open_failure); } void project_save_write_plan_rejects_missing_paths(pp::tests::Harness& harness) { pp::app::DocumentCanvasProjectSaveTargetPlan empty_target; const auto no_target = pp::app::plan_document_canvas_project_save_write(empty_target, false); pp::app::DocumentCanvasProjectSaveTargetPlan no_temporary; no_temporary.target_path = "D:/Paint/projects/demo.ppi"; const auto missing_temporary = pp::app::plan_document_canvas_project_save_write(no_temporary, true); PP_EXPECT(harness, !no_target); PP_EXPECT(harness, !missing_temporary); PP_EXPECT(harness, no_target.status().code == pp::foundation::StatusCode::invalid_argument); PP_EXPECT(harness, missing_temporary.status().code == pp::foundation::StatusCode::invalid_argument); } void snapshot_plan_rejects_invalid_canvas_state(pp::tests::Harness& harness) { const std::uint32_t frames[] { 100U }; const pp::app::DocumentCanvasLayerSnapshotInput layers[] { { .name = "Layer", .frame_durations_ms = std::span(frames), }, }; const auto no_canvas = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { .has_canvas = false, .width = 16U, .height = 8U, .layers = std::span(layers), }); const auto bad_layer = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { .has_canvas = true, .width = 16U, .height = 8U, .active_layer_index = 1U, .layers = std::span(layers), }); const pp::app::DocumentCanvasLayerSnapshotInput bad_blend_layers[] { { .name = "Layer", .blend_mode = 64, .frame_durations_ms = std::span(frames), }, }; const auto bad_blend = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { .has_canvas = true, .width = 16U, .height = 8U, .layers = std::span(bad_blend_layers), }); const std::uint32_t bad_frames[] { 0U }; const pp::app::DocumentCanvasLayerSnapshotInput bad_duration_layers[] { { .name = "Layer", .frame_durations_ms = std::span(bad_frames), }, }; const auto bad_duration = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { .has_canvas = true, .width = 16U, .height = 8U, .layers = std::span(bad_duration_layers), }); const std::uint8_t bad_rgba[] { 1U, 2U, 3U }; const pp::app::DocumentCanvasFacePayloadInput bad_payloads[] { { .frame_index = 0U, .face_index = 0U, .x = 0U, .y = 0U, .width = 1U, .height = 1U, .rgba8 = std::span(bad_rgba), }, }; const pp::app::DocumentCanvasLayerSnapshotInput bad_payload_layers[] { { .name = "Layer", .frame_durations_ms = std::span(frames), .pending_face_payloads = 1U, .captured_face_payloads = std::span(bad_payloads), }, }; const auto bad_payload = pp::app::plan_document_canvas_snapshot(pp::app::DocumentCanvasSnapshotInput { .has_canvas = true, .width = 16U, .height = 8U, .layers = std::span(bad_payload_layers), }); PP_EXPECT(harness, !no_canvas); PP_EXPECT(harness, !bad_layer); PP_EXPECT(harness, !bad_blend); PP_EXPECT(harness, !bad_duration); PP_EXPECT(harness, !bad_payload); } void clear_plan_records_legacy_canvas_effects(pp::tests::Harness& harness) { const auto plan = pp::app::plan_document_canvas_clear(true, 0.0F, 0.1F, 0.2F, 0.3F); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, plan.value().clears_canvas); PP_EXPECT(harness, plan.value().records_undo); PP_EXPECT(harness, plan.value().marks_unsaved); PP_EXPECT(harness, !plan.value().no_op); PP_EXPECT(harness, plan.value().r == 0.0F); PP_EXPECT(harness, plan.value().g == 0.1F); PP_EXPECT(harness, plan.value().b == 0.2F); PP_EXPECT(harness, plan.value().a == 0.3F); } } void clear_plan_noops_without_canvas(pp::tests::Harness& harness) { const auto plan = pp::app::plan_document_canvas_clear(false); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, !plan.value().clears_canvas); PP_EXPECT(harness, !plan.value().records_undo); PP_EXPECT(harness, !plan.value().marks_unsaved); PP_EXPECT(harness, plan.value().no_op); } } void clear_plan_rejects_bad_color_channels(pp::tests::Harness& harness) { PP_EXPECT(harness, !pp::app::plan_document_canvas_clear(true, -0.01F, 0.0F, 0.0F, 0.0F)); PP_EXPECT(harness, !pp::app::plan_document_canvas_clear(true, 0.0F, 1.01F, 0.0F, 0.0F)); PP_EXPECT(harness, !pp::app::plan_document_canvas_clear( true, 0.0F, 0.0F, std::numeric_limits::infinity(), 0.0F)); } void clear_executor_dispatches_color_to_service(pp::tests::Harness& harness) { FakeDocumentCanvasClearServices services; const auto plan = pp::app::plan_document_canvas_clear(true, 0.25F, 0.5F, 0.75F, 1.0F); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, pp::app::execute_document_canvas_clear_plan(plan.value(), services).ok()); } PP_EXPECT(harness, services.clear_calls == 1); PP_EXPECT(harness, services.last_r == 0.25F); PP_EXPECT(harness, services.last_g == 0.5F); PP_EXPECT(harness, services.last_b == 0.75F); PP_EXPECT(harness, services.last_a == 1.0F); PP_EXPECT(harness, services.call_order == "clear;"); } void clear_executor_preserves_noop_without_canvas(pp::tests::Harness& harness) { FakeDocumentCanvasClearServices services; const auto plan = pp::app::plan_document_canvas_clear(false); PP_EXPECT(harness, plan); if (plan) { PP_EXPECT(harness, pp::app::execute_document_canvas_clear_plan(plan.value(), services).ok()); } PP_EXPECT(harness, services.clear_calls == 0); } void clear_executor_rejects_invalid_color(pp::tests::Harness& harness) { FakeDocumentCanvasClearServices services; pp::app::DocumentCanvasClearPlan plan; plan.clears_canvas = true; plan.records_undo = true; plan.marks_unsaved = true; plan.no_op = false; plan.r = 0.0F; plan.g = 0.0F; plan.b = 1.5F; plan.a = 0.0F; const auto status = pp::app::execute_document_canvas_clear_plan(plan, services); PP_EXPECT(harness, !status.ok()); PP_EXPECT(harness, status.code == pp::foundation::StatusCode::out_of_range); PP_EXPECT(harness, services.clear_calls == 0); } } // namespace int main() { pp::tests::Harness harness; harness.run("snapshot plan projects canvas metadata", snapshot_plan_projects_canvas_metadata); harness.run("snapshot plan defaults empty names and frames", snapshot_plan_defaults_empty_names_and_frames); harness.run("snapshot plan attaches captured face payloads", snapshot_plan_attaches_captured_face_payloads); harness.run("save writer route uses ppi writer for complete payloads", save_writer_route_uses_ppi_writer_for_complete_payloads); harness.run("save writer route falls back for pending payloads", save_writer_route_falls_back_for_pending_payloads); harness.run("project save target plan preserves legacy paths", project_save_target_plan_preserves_legacy_paths); harness.run("project save target plan accepts windows backslashes", project_save_target_plan_accepts_windows_backslashes); harness.run("project save target plan rejects empty inputs", project_save_target_plan_rejects_empty_inputs); harness.run("project save write plan writes direct for new targets", project_save_write_plan_writes_direct_for_new_targets); harness.run("project save write plan prefers temporary for existing targets", project_save_write_plan_prefers_temporary_for_existing_targets); harness.run("project save write plan rejects missing paths", project_save_write_plan_rejects_missing_paths); harness.run("snapshot plan rejects invalid canvas state", snapshot_plan_rejects_invalid_canvas_state); harness.run("clear plan records legacy canvas effects", clear_plan_records_legacy_canvas_effects); harness.run("clear plan noops without canvas", clear_plan_noops_without_canvas); harness.run("clear plan rejects bad color channels", clear_plan_rejects_bad_color_channels); harness.run("clear executor dispatches color to service", clear_executor_dispatches_color_to_service); harness.run("clear executor preserves noop without canvas", clear_executor_preserves_noop_without_canvas); harness.run("clear executor rejects invalid color", clear_executor_rejects_invalid_color); return harness.finish(); }