Centralize legacy app startup

This commit is contained in:
2026-06-04 14:32:39 +02:00
parent 884a6d4940
commit 2bd1b12ade
12 changed files with 521 additions and 13 deletions

View File

@@ -226,6 +226,7 @@ add_library(pp_app_core STATIC
src/app_core/about_menu.h src/app_core/about_menu.h
src/app_core/app_preferences.h src/app_core/app_preferences.h
src/app_core/app_status.h src/app_core/app_status.h
src/app_core/app_startup.h
src/app_core/brush_ui.h src/app_core/brush_ui.h
src/app_core/canvas_hotkey.h src/app_core/canvas_hotkey.h
src/app_core/canvas_tool_ui.h src/app_core/canvas_tool_ui.h

View File

@@ -84,6 +84,8 @@ set(PP_PANOPAINTER_APP_SOURCES
src/app_vr.cpp src/app_vr.cpp
src/legacy_app_preference_services.cpp src/legacy_app_preference_services.cpp
src/legacy_app_preference_services.h 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.cpp
src/legacy_cloud_services.h src/legacy_cloud_services.h
src/legacy_document_export_services.cpp src/legacy_document_export_services.cpp

View File

@@ -173,6 +173,12 @@ Known local toolchain state:
the `pp_app_core` `AppPreferenceServices` contract while retained settings the `pp_app_core` `AppPreferenceServices` contract while retained settings
persistence, VR start/stop, recording lifecycle, and legacy canvas/UI persistence, VR start/stop, recording lifecycle, and legacy canvas/UI
execution remain tracked by `DEBT-0045`. 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 - `src/legacy_canvas_tool_services.*` is the current app-shell bridge for
canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and canvas toolbar tool selection, NodeCanvas stylus/input mode switching, and
canvas hotkey/touch execution. It keeps those live paths on the `pp_app_core` 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, timelapse start/stop/no-op decisions, VR mode success/failure dispatch,
simple stored preferences, and `AppPreferenceServices` execution dispatch for simple stored preferences, and `AppPreferenceServices` execution dispatch for
options-menu side effects. 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, - `pp_platform_api_tests` covers service dispatch for clipboard read/write,
empty clipboard writes, cursor visibility, virtual-keyboard visibility, empty clipboard writes, cursor visibility, virtual-keyboard visibility,
external file display, file sharing, and picker callbacks without platform external file display, file sharing, and picker callbacks without platform

View File

@@ -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-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-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-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 ## Closed Debt

View File

@@ -183,6 +183,12 @@ contracts. Options-menu preference execution now dispatches through
`AppPreferenceServices` and `src/legacy_app_preference_services.*` before `AppPreferenceServices` and `src/legacy_app_preference_services.*` before
legacy widgets, settings persistence, recording toggles, and canvas cursor legacy widgets, settings persistence, recording toggles, and canvas cursor
updates continue. 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, It also owns tested app status/display plans for document title text,
resolution mapping/labels, DPI text, history-memory text, and recording-frame resolution mapping/labels, DPI text, history-memory text, and recording-frame
status text, plus renderer diagnostic indicator labels for framebuffer fetch status text, plus renderer diagnostic indicator labels for framebuffer fetch
@@ -1358,6 +1364,11 @@ Results:
- Focused preference CTest coverage passed for - Focused preference CTest coverage passed for
`pp_app_core_app_preferences_tests` and the app-preferences CLI smoke tests `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. 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, - `pp_app_core_document_recording_tests` passed, covering recording start/stop,
clear, platform recorded-file cleanup, frame-count reset, export progress clear, platform recorded-file cleanup, frame-count reset, export progress
totals, and oversized progress-total clamping. totals, and oversized progress-total clamping.

View File

@@ -6,10 +6,12 @@
#include "node_progress_bar.h" #include "node_progress_bar.h"
#include "mp4enc.h" #include "mp4enc.h"
#include "app_core/app_status.h" #include "app_core/app_status.h"
#include "app_core/app_startup.h"
#include "app_core/canvas_tool_ui.h" #include "app_core/canvas_tool_ui.h"
#include "app_core/document_recording.h" #include "app_core/document_recording.h"
#include "app_core/document_route.h" #include "app_core/document_route.h"
#include "app_core/document_session.h" #include "app_core/document_session.h"
#include "legacy_app_startup_services.h"
#include "legacy_document_open_services.h" #include "legacy_document_open_services.h"
#include "legacy_document_session_services.h" #include "legacy_document_session_services.h"
#include "legacy_recording_services.h" #include "legacy_recording_services.h"
@@ -436,12 +438,20 @@ void App::init()
LOG("OpenGL startup state failed: %s", startup_state_status.message); LOG("OpenGL startup state failed: %s", startup_state_status.message);
}); });
int run_counter = Settings::value<Serializer::Integer>("run_counter") + 1; const auto startup_plan = pp::app::plan_app_startup(
Settings::set("run_counter", Serializer::Integer(run_counter)); Settings::value<Serializer::Integer>("run_counter"),
LOG("run_counter %d", run_counter); Settings::value_or<Serializer::Boolean>("auto-timelapse", true),
Settings::value_or<Serializer::Boolean>("vr-controllers-enabled", vr_controllers_enabled),
if (!Settings::save()) check_license());
LOG("save preferences failed"); 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(); initShaders();
initAssets(); initAssets();
@@ -450,13 +460,12 @@ void App::init()
uirtt.create(width, height, -1, rgba8_internal_format(), true); uirtt.create(width, height, -1, rgba8_internal_format(), true);
if (Settings::value_or<Serializer::Boolean>("auto-timelapse", true)) if (startup_plan) {
rec_start(); const auto startup_status = pp::panopainter::execute_legacy_app_startup_runtime_plan(
Settings::value<Serializer::Boolean>("vr-controllers-enabled", vr_controllers_enabled); *this,
startup_plan.value());
if (!check_license()) if (!startup_status.ok())
{ LOG("App startup runtime execution failed: %s", startup_status.message);
message_box("License", "Could not validate this license, running in demo mode.");
} }
} }

112
src/app_core/app_startup.h Normal file
View File

@@ -0,0 +1,112 @@
#pragma once
#include "foundation/result.h"
#include <limits>
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<AppStartupPlan> 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<AppStartupPlan>::failure(
pp::foundation::Status::invalid_argument("run counter must not be negative"));
}
if (current_run_counter == std::numeric_limits<int>::max()) {
return pp::foundation::Result<AppStartupPlan>::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<AppStartupPlan>::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

View File

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

View File

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

View File

@@ -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 set_tests_properties(pp_app_core_app_status_tests PROPERTIES
LABELS "app;desktop-fast;fuzz") 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 add_executable(pp_app_core_document_sharing_tests
app_core/document_sharing_tests.cpp) app_core/document_sharing_tests.cpp)
target_link_libraries(pp_app_core_document_sharing_tests PRIVATE target_link_libraries(pp_app_core_document_sharing_tests PRIVATE
@@ -851,6 +861,22 @@ if(TARGET pano_cli)
LABELS "app;integration;desktop-fast;fuzz" 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\\}") 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 add_test(NAME pano_cli_plan_tools_menu_shortcuts_smoke
COMMAND pano_cli plan-tools-menu --command shortcuts) COMMAND pano_cli plan-tools-menu --command shortcuts)
set_tests_properties(pano_cli_plan_tools_menu_shortcuts_smoke PROPERTIES set_tests_properties(pano_cli_plan_tools_menu_shortcuts_smoke PROPERTIES

View File

@@ -0,0 +1,164 @@
#include "app_core/app_startup.h"
#include "test_harness.h"
#include <limits>
#include <string>
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<int>::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();
}

View File

@@ -1,6 +1,7 @@
#include "app_core/about_menu.h" #include "app_core/about_menu.h"
#include "app_core/app_preferences.h" #include "app_core/app_preferences.h"
#include "app_core/app_status.h" #include "app_core/app_status.h"
#include "app_core/app_startup.h"
#include "app_core/brush_ui.h" #include "app_core/brush_ui.h"
#include "app_core/canvas_hotkey.h" #include "app_core/canvas_hotkey.h"
#include "app_core/canvas_tool_ui.h" #include "app_core/canvas_tool_ui.h"
@@ -227,6 +228,13 @@ struct PlanAppPreferencesArgs {
int cursor_mode = 0; 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 { struct PlanAppStatusArgs {
std::string document_name = "no-name"; std::string document_name = "no-name";
bool unsaved = false; bool unsaved = false;
@@ -1852,6 +1860,7 @@ void print_help()
<< " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n" << " plan-cloud-upload-all [--file-count N] [--no-progress-ui]\n"
<< " plan-recording-session [--running] [--frame-count N] [--platform-deletes-recorded-files]\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-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-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-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" << " 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; 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( pp::foundation::Status parse_plan_tools_menu_args(
int argc, int argc,
char** argv, char** argv,
@@ -8492,6 +8565,10 @@ int main(int argc, char** argv)
return plan_app_preferences(argc, argv); return plan_app_preferences(argc, argv);
} }
if (command == "plan-app-startup") {
return plan_app_startup(argc, argv);
}
if (command == "plan-app-status") { if (command == "plan-app-status") {
return plan_app_status(argc, argv); return plan_app_status(argc, argv);
} }