From 2bd1b12ade6fec0a15ef870a3b12dde37b1acf73 Mon Sep 17 00:00:00 2001 From: omigamedev Date: Thu, 4 Jun 2026 14:32:39 +0200 Subject: [PATCH] Centralize legacy app startup --- CMakeLists.txt | 1 + cmake/PanoPainterSources.cmake | 2 + docs/modernization/build-inventory.md | 10 ++ docs/modernization/debt.md | 1 + docs/modernization/roadmap.md | 11 ++ src/app.cpp | 35 ++++-- src/app_core/app_startup.h | 112 ++++++++++++++++++ src/legacy_app_startup_services.cpp | 76 ++++++++++++ src/legacy_app_startup_services.h | 19 +++ tests/CMakeLists.txt | 26 ++++ tests/app_core/app_startup_tests.cpp | 164 ++++++++++++++++++++++++++ tools/pano_cli/main.cpp | 77 ++++++++++++ 12 files changed, 521 insertions(+), 13 deletions(-) create mode 100644 src/app_core/app_startup.h create mode 100644 src/legacy_app_startup_services.cpp create mode 100644 src/legacy_app_startup_services.h create mode 100644 tests/app_core/app_startup_tests.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e1cb9c..f9f71e0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -226,6 +226,7 @@ add_library(pp_app_core STATIC src/app_core/about_menu.h src/app_core/app_preferences.h src/app_core/app_status.h + src/app_core/app_startup.h src/app_core/brush_ui.h src/app_core/canvas_hotkey.h src/app_core/canvas_tool_ui.h diff --git a/cmake/PanoPainterSources.cmake b/cmake/PanoPainterSources.cmake index ea49457..25744b2 100644 --- a/cmake/PanoPainterSources.cmake +++ b/cmake/PanoPainterSources.cmake @@ -84,6 +84,8 @@ set(PP_PANOPAINTER_APP_SOURCES src/app_vr.cpp src/legacy_app_preference_services.cpp src/legacy_app_preference_services.h + src/legacy_app_startup_services.cpp + src/legacy_app_startup_services.h src/legacy_cloud_services.cpp src/legacy_cloud_services.h src/legacy_document_export_services.cpp diff --git a/docs/modernization/build-inventory.md b/docs/modernization/build-inventory.md index 5539c1c..4ff2e05 100644 --- a/docs/modernization/build-inventory.md +++ b/docs/modernization/build-inventory.md @@ -173,6 +173,12 @@ Known local toolchain state: the `pp_app_core` `AppPreferenceServices` contract while retained settings persistence, VR start/stop, recording lifecycle, and legacy canvas/UI execution remain tracked by `DEBT-0045`. +- `src/legacy_app_startup_services.*` is the current app-shell bridge for + startup preference/runtime execution. It keeps run-counter persistence, + startup preference save, auto-timelapse startup, stored VR-controller state, + and license-warning decisions on the `pp_app_core` `AppStartupServices` + contract while retained `Settings`, `App::rec_start`, and message-box + execution remain tracked by `DEBT-0046`. - `src/legacy_canvas_tool_services.*` is the current app-shell bridge for canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and canvas hotkey/touch execution. It keeps those live paths on the `pp_app_core` @@ -587,6 +593,10 @@ Known local toolchain state: timelapse start/stop/no-op decisions, VR mode success/failure dispatch, simple stored preferences, and `AppPreferenceServices` execution dispatch for options-menu side effects. +- `pp_app_core_app_startup_tests` covers startup run-counter increment + planning, optional auto-timelapse/license/VR-controller decisions, negative + and overflow run-counter rejection, stable full startup dispatch ordering, + split persistence/runtime dispatch, and malformed startup-plan rejection. - `pp_platform_api_tests` covers service dispatch for clipboard read/write, empty clipboard writes, cursor visibility, virtual-keyboard visibility, external file display, file sharing, and picker callbacks without platform diff --git a/docs/modernization/debt.md b/docs/modernization/debt.md index 32ff59f..d395ef1 100644 --- a/docs/modernization/debt.md +++ b/docs/modernization/debt.md @@ -63,6 +63,7 @@ agent or engineer to remove them without reconstructing context from chat. | DEBT-0043 | Open | Modernization | Equirectangular, layer, animation-frame, depth, and cube-face export planning/execution dispatch now consumes pure `pp_app_core` through `App::dialog_export`, `App::dialog_export_layers`, `App::dialog_export_anim_frames`, `App::dialog_export_depth`, `App::dialog_export_cube_faces`, `pano_cli plan-export-*`, `DocumentExportServices`, and `src/legacy_document_export_services.*`, but the bridge still calls legacy `Canvas` export methods, owns platform-specific export success messages, creates export directories, handles picker-selected stems, and performs Web prepared-file handoff directly | Preserve current image/collection/depth/cube export behavior while export execution moves toward document/renderer/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-start --requires-license --demo`; `pano_cli plan-export-menu --kind layers`; `pano_cli plan-export-target --kind collection --work-dir D:/Paint --doc-name demo --suffix _layers`; `pano_cli simulate-document-export`; `ctest --preset desktop-fast --build-config Debug` | File, collection, stem, depth, and cube export execution, export-directory creation, platform success reporting, Web file handoff, and legacy canvas export calls are owned by injected document/renderer/platform/storage services with export dialogs acting only as UI adapters | | DEBT-0044 | Open | Modernization | Timelapse and animation MP4 export execution dispatch now consumes pure `pp_app_core` through `App::dialog_timelapse_export`, `App::dialog_export_mp4`, `pano_cli plan-export-menu`, `pano_cli plan-export-target --kind name`, `DocumentVideoExportServices`, and `src/legacy_document_export_services.*`, but the bridge still launches legacy desktop timelapse worker threads, calls `App::rec_export`, calls `Canvas::export_anim_mp4`, owns mobile/Web save callbacks, and emits success messages directly | Preserve current MP4/timelapse export behavior while video export moves toward app/document/renderer/video/platform/storage services | `pp_app_core_document_export_tests`; `pano_cli plan-export-menu --kind animation-mp4`; `pano_cli plan-export-menu --kind timelapse`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -animation`; `pano_cli plan-export-target --kind name --doc-name demo --suffix -timelapse`; `ctest --preset desktop-fast --build-config Debug` | Timelapse and animation MP4 execution, desktop worker threading, frame readback/video encoding handoff, mobile/Web save callbacks, and success reporting are owned by injected app/document/renderer/video/platform/storage services with export dialogs acting only as UI adapters | | DEBT-0045 | Open | Modernization | Options-menu preference execution now consumes pure `pp_app_core` through UI scale, viewport scale, RTL direction, VR mode, VR-controller, auto-timelapse, and canvas cursor-mode callbacks plus `AppPreferenceServices` and `src/legacy_app_preference_services.*`, but the bridge still calls legacy `App::set_ui_scale`, `App::set_ui_rtl`, `App::vr_start`, `App::vr_stop`, `NodeCanvas::set_density`, `NodeCanvas::set_cursor_visibility`, `App::rec_start`, `App::rec_stop`, and `Settings::save` directly | Preserve current options-menu behavior while preferences move toward app/UI/platform/storage services | `pp_app_core_app_preferences_tests`; `pano_cli plan-app-preferences --ui-scale 1.5 --display-density 2 --current-scale 1.6 --scale-option 1 --scale-option 1.5 --rtl`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Preference persistence, UI/layout direction, viewport density, cursor mode, VR mode start/stop/failure handling, VR-controller state, and auto-timelapse recording side effects are owned by injected app/UI/platform/storage services with options-menu callbacks acting only as UI adapters | +| DEBT-0046 | Open | Modernization | Startup preference/runtime execution now consumes pure `pp_app_core` through `App::init`, `pano_cli plan-app-startup`, `AppStartupServices`, and `src/legacy_app_startup_services.*`, but the bridge still calls legacy `Settings::set`, `Settings::save`, `App::rec_start`, app VR-controller state mutation, and message-box license warning execution directly | Preserve current startup behavior while app startup moves toward app/preferences/storage/recording/UI services | `pp_app_core_app_startup_tests`; `pano_cli plan-app-startup --run-counter 7 --vr-controllers-disabled --license-invalid`; `pano_cli plan-app-startup --run-counter -1`; `ctest --preset desktop-fast --build-config Debug`; `cmake --build --preset windows-msvc-default --config Debug --target PanoPainter` | Startup preference persistence, auto-timelapse startup, stored VR-controller state, license validation/warning, and startup UI/runtime side effects are owned by injected app/preferences/storage/recording/UI services with `App::init` acting only as orchestration | ## Closed Debt diff --git a/docs/modernization/roadmap.md b/docs/modernization/roadmap.md index b39acfd..2a4d625 100644 --- a/docs/modernization/roadmap.md +++ b/docs/modernization/roadmap.md @@ -183,6 +183,12 @@ contracts. Options-menu preference execution now dispatches through `AppPreferenceServices` and `src/legacy_app_preference_services.*` before legacy widgets, settings persistence, recording toggles, and canvas cursor updates continue. +It also owns tested startup plans for run-counter increments, preference-save +intent, auto-timelapse startup, stored VR-controller state, and license-warning +visibility. `App::init` now plans those decisions before heavy initialization, +executes run-counter persistence through `src/legacy_app_startup_services.*` +before asset/layout setup, and executes runtime startup side effects after the +UI layout and main render target exist. It also owns tested app status/display plans for document title text, resolution mapping/labels, DPI text, history-memory text, and recording-frame status text, plus renderer diagnostic indicator labels for framebuffer fetch @@ -1358,6 +1364,11 @@ Results: - Focused preference CTest coverage passed for `pp_app_core_app_preferences_tests` and the app-preferences CLI smoke tests after the live bridge split, including VR mode failed-start status coverage. +- `PanoPainter`, `pp_app_core_app_startup_tests`, and `pano_cli` built after + startup preference/runtime execution moved behind app startup services. +- Focused startup CTest coverage passed for `pp_app_core_app_startup_tests`, + `pano_cli_plan_app_startup_smoke`, and + `pano_cli_plan_app_startup_rejects_negative_counter`. - `pp_app_core_document_recording_tests` passed, covering recording start/stop, clear, platform recorded-file cleanup, frame-count reset, export progress totals, and oversized progress-total clamping. diff --git a/src/app.cpp b/src/app.cpp index 39745d1..098f155 100644 --- a/src/app.cpp +++ b/src/app.cpp @@ -6,10 +6,12 @@ #include "node_progress_bar.h" #include "mp4enc.h" #include "app_core/app_status.h" +#include "app_core/app_startup.h" #include "app_core/canvas_tool_ui.h" #include "app_core/document_recording.h" #include "app_core/document_route.h" #include "app_core/document_session.h" +#include "legacy_app_startup_services.h" #include "legacy_document_open_services.h" #include "legacy_document_session_services.h" #include "legacy_recording_services.h" @@ -436,12 +438,20 @@ void App::init() LOG("OpenGL startup state failed: %s", startup_state_status.message); }); - int run_counter = Settings::value("run_counter") + 1; - Settings::set("run_counter", Serializer::Integer(run_counter)); - LOG("run_counter %d", run_counter); - - if (!Settings::save()) - LOG("save preferences failed"); + const auto startup_plan = pp::app::plan_app_startup( + Settings::value("run_counter"), + Settings::value_or("auto-timelapse", true), + Settings::value_or("vr-controllers-enabled", vr_controllers_enabled), + check_license()); + if (!startup_plan) { + LOG("App startup plan failed: %s", startup_plan.status().message); + } else { + const auto persistence_status = pp::panopainter::execute_legacy_app_startup_persistence_plan( + *this, + startup_plan.value()); + if (!persistence_status.ok()) + LOG("App startup persistence failed: %s", persistence_status.message); + } initShaders(); initAssets(); @@ -450,13 +460,12 @@ void App::init() uirtt.create(width, height, -1, rgba8_internal_format(), true); - if (Settings::value_or("auto-timelapse", true)) - rec_start(); - Settings::value("vr-controllers-enabled", vr_controllers_enabled); - - if (!check_license()) - { - message_box("License", "Could not validate this license, running in demo mode."); + if (startup_plan) { + const auto startup_status = pp::panopainter::execute_legacy_app_startup_runtime_plan( + *this, + startup_plan.value()); + if (!startup_status.ok()) + LOG("App startup runtime execution failed: %s", startup_status.message); } } diff --git a/src/app_core/app_startup.h b/src/app_core/app_startup.h new file mode 100644 index 0000000..bf8c052 --- /dev/null +++ b/src/app_core/app_startup.h @@ -0,0 +1,112 @@ +#pragma once + +#include "foundation/result.h" + +#include + +namespace pp::app { + +struct AppStartupPlan { + int previous_run_counter = 0; + int next_run_counter = 1; + bool save_preferences = true; + bool start_timelapse = false; + bool vr_controllers_enabled = true; + bool show_license_warning = false; +}; + +class AppStartupServices { +public: + virtual ~AppStartupServices() = default; + + virtual void store_run_counter(int value) = 0; + virtual void save_preferences() = 0; + virtual void start_timelapse_recording() = 0; + virtual void apply_vr_controllers_enabled(bool enabled) = 0; + virtual void show_license_warning() = 0; +}; + +[[nodiscard]] inline pp::foundation::Result plan_app_startup( + int current_run_counter, + bool auto_timelapse_enabled, + bool stored_vr_controllers_enabled, + bool license_valid) +{ + if (current_run_counter < 0) { + return pp::foundation::Result::failure( + pp::foundation::Status::invalid_argument("run counter must not be negative")); + } + + if (current_run_counter == std::numeric_limits::max()) { + return pp::foundation::Result::failure( + pp::foundation::Status::out_of_range("run counter would overflow")); + } + + AppStartupPlan plan; + plan.previous_run_counter = current_run_counter; + plan.next_run_counter = current_run_counter + 1; + plan.start_timelapse = auto_timelapse_enabled; + plan.vr_controllers_enabled = stored_vr_controllers_enabled; + plan.show_license_warning = !license_valid; + return pp::foundation::Result::success(plan); +} + +[[nodiscard]] inline pp::foundation::Status execute_app_startup_plan( + const AppStartupPlan& plan, + AppStartupServices& services) +{ + if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) { + return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state"); + } + + services.store_run_counter(plan.next_run_counter); + if (plan.save_preferences) { + services.save_preferences(); + } + if (plan.start_timelapse) { + services.start_timelapse_recording(); + } + services.apply_vr_controllers_enabled(plan.vr_controllers_enabled); + if (plan.show_license_warning) { + services.show_license_warning(); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status execute_app_startup_persistence_plan( + const AppStartupPlan& plan, + AppStartupServices& services) +{ + if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) { + return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state"); + } + + services.store_run_counter(plan.next_run_counter); + if (plan.save_preferences) { + services.save_preferences(); + } + + return pp::foundation::Status::success(); +} + +[[nodiscard]] inline pp::foundation::Status execute_app_startup_runtime_plan( + const AppStartupPlan& plan, + AppStartupServices& services) +{ + if (plan.previous_run_counter < 0 || plan.next_run_counter <= plan.previous_run_counter) { + return pp::foundation::Status::invalid_argument("startup plan has invalid run counter state"); + } + + if (plan.start_timelapse) { + services.start_timelapse_recording(); + } + services.apply_vr_controllers_enabled(plan.vr_controllers_enabled); + if (plan.show_license_warning) { + services.show_license_warning(); + } + + return pp::foundation::Status::success(); +} + +} // namespace pp::app diff --git a/src/legacy_app_startup_services.cpp b/src/legacy_app_startup_services.cpp new file mode 100644 index 0000000..1130a6f --- /dev/null +++ b/src/legacy_app_startup_services.cpp @@ -0,0 +1,76 @@ +#include "pch.h" + +#include "legacy_app_startup_services.h" + +#include "app.h" +#include "serializer.h" +#include "settings.h" + +namespace pp::panopainter { +namespace { + +class LegacyAppStartupServices final : public pp::app::AppStartupServices { +public: + explicit LegacyAppStartupServices(App& app) noexcept + : app_(app) + { + } + + void store_run_counter(int value) override + { + Settings::set("run_counter", Serializer::Integer(value)); + LOG("run_counter %d", value); + } + + void save_preferences() override + { + if (!Settings::save()) + LOG("save preferences failed"); + } + + void start_timelapse_recording() override + { + app_.rec_start(); + } + + void apply_vr_controllers_enabled(bool enabled) override + { + app_.vr_controllers_enabled = enabled; + } + + void show_license_warning() override + { + app_.message_box("License", "Could not validate this license, running in demo mode."); + } + +private: + App& app_; +}; + +} // namespace + +pp::foundation::Status execute_legacy_app_startup_plan( + App& app, + const pp::app::AppStartupPlan& plan) +{ + LegacyAppStartupServices services(app); + return pp::app::execute_app_startup_plan(plan, services); +} + +pp::foundation::Status execute_legacy_app_startup_persistence_plan( + App& app, + const pp::app::AppStartupPlan& plan) +{ + LegacyAppStartupServices services(app); + return pp::app::execute_app_startup_persistence_plan(plan, services); +} + +pp::foundation::Status execute_legacy_app_startup_runtime_plan( + App& app, + const pp::app::AppStartupPlan& plan) +{ + LegacyAppStartupServices services(app); + return pp::app::execute_app_startup_runtime_plan(plan, services); +} + +} // namespace pp::panopainter diff --git a/src/legacy_app_startup_services.h b/src/legacy_app_startup_services.h new file mode 100644 index 0000000..7bda81d --- /dev/null +++ b/src/legacy_app_startup_services.h @@ -0,0 +1,19 @@ +#pragma once + +#include "app_core/app_startup.h" + +class App; + +namespace pp::panopainter { + +[[nodiscard]] pp::foundation::Status execute_legacy_app_startup_plan( + App& app, + const pp::app::AppStartupPlan& plan); +[[nodiscard]] pp::foundation::Status execute_legacy_app_startup_persistence_plan( + App& app, + const pp::app::AppStartupPlan& plan); +[[nodiscard]] pp::foundation::Status execute_legacy_app_startup_runtime_plan( + App& app, + const pp::app::AppStartupPlan& plan); + +} // namespace pp::panopainter diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 917c703..f634122 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -488,6 +488,16 @@ add_test(NAME pp_app_core_app_status_tests COMMAND pp_app_core_app_status_tests) set_tests_properties(pp_app_core_app_status_tests PROPERTIES LABELS "app;desktop-fast;fuzz") +add_executable(pp_app_core_app_startup_tests + app_core/app_startup_tests.cpp) +target_link_libraries(pp_app_core_app_startup_tests PRIVATE + pp_app_core + pp_test_harness) + +add_test(NAME pp_app_core_app_startup_tests COMMAND pp_app_core_app_startup_tests) +set_tests_properties(pp_app_core_app_startup_tests PROPERTIES + LABELS "app;desktop-fast;fuzz") + add_executable(pp_app_core_document_sharing_tests app_core/document_sharing_tests.cpp) target_link_libraries(pp_app_core_document_sharing_tests PRIVATE @@ -851,6 +861,22 @@ if(TARGET pano_cli) LABELS "app;integration;desktop-fast;fuzz" PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-preferences\".*\"scaleSelection\":\\{\"hasSelection\":false,\"index\":0\\}.*\"direction\":\"left-to-right\".*\"timelapse\":\\{\"enabled\":false,\"recordingAction\":\"stop-recording\"\\}.*\"vrControllers\":\\{\"enabled\":false\\}") + add_test(NAME pano_cli_plan_app_startup_smoke + COMMAND pano_cli plan-app-startup + --run-counter 7 + --vr-controllers-disabled + --license-invalid) + set_tests_properties(pano_cli_plan_app_startup_smoke PROPERTIES + LABELS "app;integration;desktop-fast" + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-startup\".*\"runCounter\":7.*\"licenseValid\":false.*\"previousRunCounter\":7.*\"nextRunCounter\":8.*\"savePreferences\":true.*\"startTimelapse\":true.*\"vrControllersEnabled\":false.*\"showLicenseWarning\":true") + + add_test(NAME pano_cli_plan_app_startup_rejects_negative_counter + COMMAND pano_cli plan-app-startup --run-counter -1) + set_tests_properties(pano_cli_plan_app_startup_rejects_negative_counter PROPERTIES + LABELS "app;integration;desktop-fast;fuzz" + WILL_FAIL TRUE + PASS_REGULAR_EXPRESSION "\"command\":\"plan-app-startup\".*\"message\":\"run counter must not be negative\"") + add_test(NAME pano_cli_plan_tools_menu_shortcuts_smoke COMMAND pano_cli plan-tools-menu --command shortcuts) set_tests_properties(pano_cli_plan_tools_menu_shortcuts_smoke PROPERTIES diff --git a/tests/app_core/app_startup_tests.cpp b/tests/app_core/app_startup_tests.cpp new file mode 100644 index 0000000..8377728 --- /dev/null +++ b/tests/app_core/app_startup_tests.cpp @@ -0,0 +1,164 @@ +#include "app_core/app_startup.h" +#include "test_harness.h" + +#include +#include + +namespace { + +class FakeAppStartupServices final : public pp::app::AppStartupServices { +public: + void store_run_counter(int value) override + { + stored_run_counter = value; + call_order += "store-counter;"; + } + + void save_preferences() override + { + save_calls += 1; + call_order += "save;"; + } + + void start_timelapse_recording() override + { + timelapse_starts += 1; + call_order += "timelapse;"; + } + + void apply_vr_controllers_enabled(bool enabled) override + { + vr_controllers_enabled = enabled; + call_order += "vr-controllers;"; + } + + void show_license_warning() override + { + license_warnings += 1; + call_order += "license;"; + } + + int stored_run_counter = 0; + int save_calls = 0; + int timelapse_starts = 0; + bool vr_controllers_enabled = false; + int license_warnings = 0; + std::string call_order; +}; + +void startup_plan_increments_counter_and_enables_requested_side_effects(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_startup(7, true, false, false); + + PP_EXPECT(harness, plan); + PP_EXPECT(harness, plan.value().previous_run_counter == 7); + PP_EXPECT(harness, plan.value().next_run_counter == 8); + PP_EXPECT(harness, plan.value().save_preferences); + PP_EXPECT(harness, plan.value().start_timelapse); + PP_EXPECT(harness, !plan.value().vr_controllers_enabled); + PP_EXPECT(harness, plan.value().show_license_warning); +} + +void startup_plan_preserves_disabled_optional_work(pp::tests::Harness& harness) +{ + const auto plan = pp::app::plan_app_startup(0, false, true, true); + + PP_EXPECT(harness, plan); + PP_EXPECT(harness, plan.value().next_run_counter == 1); + PP_EXPECT(harness, !plan.value().start_timelapse); + PP_EXPECT(harness, plan.value().vr_controllers_enabled); + PP_EXPECT(harness, !plan.value().show_license_warning); +} + +void startup_plan_rejects_invalid_counters(pp::tests::Harness& harness) +{ + const auto negative = pp::app::plan_app_startup(-1, true, true, true); + const auto overflow = pp::app::plan_app_startup(std::numeric_limits::max(), true, true, true); + + PP_EXPECT(harness, !negative); + PP_EXPECT(harness, negative.status().code == pp::foundation::StatusCode::invalid_argument); + PP_EXPECT(harness, !overflow); + PP_EXPECT(harness, overflow.status().code == pp::foundation::StatusCode::out_of_range); +} + +void startup_executor_dispatches_in_stable_order(pp::tests::Harness& harness) +{ + FakeAppStartupServices services; + const auto plan = pp::app::plan_app_startup(2, true, true, false); + + PP_EXPECT(harness, plan); + PP_EXPECT(harness, pp::app::execute_app_startup_plan(plan.value(), services).ok()); + PP_EXPECT(harness, services.stored_run_counter == 3); + PP_EXPECT(harness, services.save_calls == 1); + PP_EXPECT(harness, services.timelapse_starts == 1); + PP_EXPECT(harness, services.vr_controllers_enabled); + PP_EXPECT(harness, services.license_warnings == 1); + PP_EXPECT(harness, services.call_order == "store-counter;save;timelapse;vr-controllers;license;"); +} + +void startup_executor_preserves_no_op_side_effects(pp::tests::Harness& harness) +{ + FakeAppStartupServices services; + auto plan = pp::app::plan_app_startup(4, false, false, true).value(); + + PP_EXPECT(harness, pp::app::execute_app_startup_plan(plan, services).ok()); + PP_EXPECT(harness, services.stored_run_counter == 5); + PP_EXPECT(harness, services.save_calls == 1); + PP_EXPECT(harness, services.timelapse_starts == 0); + PP_EXPECT(harness, !services.vr_controllers_enabled); + PP_EXPECT(harness, services.license_warnings == 0); + PP_EXPECT(harness, services.call_order == "store-counter;save;vr-controllers;"); +} + +void startup_split_executors_keep_persistence_and_runtime_separate(pp::tests::Harness& harness) +{ + FakeAppStartupServices persistence_services; + FakeAppStartupServices runtime_services; + const auto plan = pp::app::plan_app_startup(5, true, false, false); + + PP_EXPECT(harness, plan); + PP_EXPECT(harness, pp::app::execute_app_startup_persistence_plan(plan.value(), persistence_services).ok()); + PP_EXPECT(harness, persistence_services.stored_run_counter == 6); + PP_EXPECT(harness, persistence_services.save_calls == 1); + PP_EXPECT(harness, persistence_services.timelapse_starts == 0); + PP_EXPECT(harness, persistence_services.license_warnings == 0); + PP_EXPECT(harness, persistence_services.call_order == "store-counter;save;"); + + PP_EXPECT(harness, pp::app::execute_app_startup_runtime_plan(plan.value(), runtime_services).ok()); + PP_EXPECT(harness, runtime_services.stored_run_counter == 0); + PP_EXPECT(harness, runtime_services.save_calls == 0); + PP_EXPECT(harness, runtime_services.timelapse_starts == 1); + PP_EXPECT(harness, !runtime_services.vr_controllers_enabled); + PP_EXPECT(harness, runtime_services.license_warnings == 1); + PP_EXPECT(harness, runtime_services.call_order == "timelapse;vr-controllers;license;"); +} + +void startup_executor_rejects_malformed_counter_state(pp::tests::Harness& harness) +{ + FakeAppStartupServices services; + pp::app::AppStartupPlan plan; + plan.previous_run_counter = 3; + plan.next_run_counter = 3; + + PP_EXPECT(harness, !pp::app::execute_app_startup_plan(plan, services).ok()); + PP_EXPECT(harness, services.call_order.empty()); +} + +} + +int main() +{ + pp::tests::Harness harness; + harness.run( + "startup plan increments counter and enables requested side effects", + startup_plan_increments_counter_and_enables_requested_side_effects); + harness.run("startup plan preserves disabled optional work", startup_plan_preserves_disabled_optional_work); + harness.run("startup plan rejects invalid counters", startup_plan_rejects_invalid_counters); + harness.run("startup executor dispatches in stable order", startup_executor_dispatches_in_stable_order); + harness.run("startup executor preserves no-op side effects", startup_executor_preserves_no_op_side_effects); + harness.run( + "startup split executors keep persistence and runtime separate", + startup_split_executors_keep_persistence_and_runtime_separate); + harness.run("startup executor rejects malformed counter state", startup_executor_rejects_malformed_counter_state); + return harness.finish(); +} diff --git a/tools/pano_cli/main.cpp b/tools/pano_cli/main.cpp index e5ff24d..62f097c 100644 --- a/tools/pano_cli/main.cpp +++ b/tools/pano_cli/main.cpp @@ -1,6 +1,7 @@ #include "app_core/about_menu.h" #include "app_core/app_preferences.h" #include "app_core/app_status.h" +#include "app_core/app_startup.h" #include "app_core/brush_ui.h" #include "app_core/canvas_hotkey.h" #include "app_core/canvas_tool_ui.h" @@ -227,6 +228,13 @@ struct PlanAppPreferencesArgs { int cursor_mode = 0; }; +struct PlanAppStartupArgs { + int run_counter = 0; + bool auto_timelapse_enabled = true; + bool vr_controllers_enabled = true; + bool license_valid = true; +}; + struct PlanAppStatusArgs { std::string document_name = "no-name"; bool unsaved = false; @@ -1852,6 +1860,7 @@ void print_help() << " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n" << " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files]\n" << " plan-app-preferences [--ui-scale N] [--display-density N] [--current-scale N] [--scale-option N] [--viewport-scale N] [--rtl] [--timelapse-disabled] [--recording-running] [--vr-controllers-disabled] [--cursor-mode N]\n" + << " plan-app-startup [--run-counter N] [--auto-timelapse-disabled] [--vr-controllers-disabled] [--license-invalid]\n" << " plan-app-status [--doc-name NAME] [--unsaved] [--resolution N] [--resolution-index N] [--zoom N] [--history-bytes N] [--recording-running] [--encoder-available] [--encoded-frames N] [--framebuffer-fetch] [--float32] [--float32-linear] [--float16]\n" << " plan-tools-menu --command panels|options|clear-grids|reset-camera|shortcuts|sonarpen [--sonarpen-available]\n" << " plan-tools-panel --panel presets|color|color-advanced|layers|brush|grids|animation [--already-visible]\n" @@ -3416,6 +3425,70 @@ int plan_app_preferences(int argc, char** argv) return 0; } +pp::foundation::Status parse_plan_app_startup_args( + int argc, + char** argv, + PlanAppStartupArgs& args) +{ + for (int i = 2; i < argc; ++i) { + const std::string_view key(argv[i]); + if (key == "--run-counter") { + if (i + 1 >= argc) { + return pp::foundation::Status::invalid_argument("missing value for option"); + } + const auto value = parse_i32_arg(argv[++i]); + if (!value) { + return value.status(); + } + args.run_counter = value.value(); + } else if (key == "--auto-timelapse-disabled") { + args.auto_timelapse_enabled = false; + } else if (key == "--vr-controllers-disabled") { + args.vr_controllers_enabled = false; + } else if (key == "--license-invalid") { + args.license_valid = false; + } else { + return pp::foundation::Status::invalid_argument("unknown option"); + } + } + + return pp::foundation::Status::success(); +} + +int plan_app_startup(int argc, char** argv) +{ + PlanAppStartupArgs args; + const auto status = parse_plan_app_startup_args(argc, argv, args); + if (!status.ok()) { + print_error("plan-app-startup", status.message); + return 2; + } + + const auto plan = pp::app::plan_app_startup( + args.run_counter, + args.auto_timelapse_enabled, + args.vr_controllers_enabled, + args.license_valid); + if (!plan) { + print_error("plan-app-startup", plan.status().message); + return 2; + } + + std::cout << "{\"ok\":true,\"command\":\"plan-app-startup\"" + << ",\"state\":{\"runCounter\":" << args.run_counter + << ",\"autoTimelapseEnabled\":" << json_bool(args.auto_timelapse_enabled) + << ",\"vrControllersEnabled\":" << json_bool(args.vr_controllers_enabled) + << ",\"licenseValid\":" << json_bool(args.license_valid) + << "},\"plan\":{\"previousRunCounter\":" << plan.value().previous_run_counter + << ",\"nextRunCounter\":" << plan.value().next_run_counter + << ",\"savePreferences\":" << json_bool(plan.value().save_preferences) + << ",\"startTimelapse\":" << json_bool(plan.value().start_timelapse) + << ",\"vrControllersEnabled\":" << json_bool(plan.value().vr_controllers_enabled) + << ",\"showLicenseWarning\":" << json_bool(plan.value().show_license_warning) + << "}}\n"; + return 0; +} + pp::foundation::Status parse_plan_tools_menu_args( int argc, char** argv, @@ -8492,6 +8565,10 @@ int main(int argc, char** argv) return plan_app_preferences(argc, argv); } + if (command == "plan-app-startup") { + return plan_app_startup(argc, argv); + } + if (command == "plan-app-status") { return plan_app_status(argc, argv); }