Add Android headless preset and parser tests

This commit is contained in:
2026-05-31 23:46:41 +02:00
parent c38ff8209b
commit e0ea4597e6
11 changed files with 230 additions and 41 deletions

View File

@@ -53,7 +53,8 @@ add_custom_target(panopainter_modernization_status
VERBATIM)
add_library(pp_foundation STATIC
src/foundation/binary_stream.cpp)
src/foundation/binary_stream.cpp
src/foundation/parse.cpp)
target_include_directories(pp_foundation
PUBLIC
"${CMAKE_CURRENT_SOURCE_DIR}/src")

View File

@@ -22,6 +22,16 @@
"PP_ENABLE_VIDEO": "ON"
}
},
{
"name": "platform-headless-base",
"hidden": true,
"inherits": "base",
"cacheVariables": {
"PP_BUILD_APP": "OFF",
"PP_ENABLE_CLOUD": "OFF",
"PP_ENABLE_VIDEO": "OFF"
}
},
{
"name": "windows-vs2026-x64",
"inherits": "base",
@@ -50,7 +60,7 @@
},
{
"name": "linux-clang",
"inherits": "base",
"inherits": "platform-headless-base",
"displayName": "Linux clang",
"generator": "Ninja",
"cacheVariables": {
@@ -59,28 +69,57 @@
}
},
{
"name": "android-arm64",
"inherits": "base",
"displayName": "Android arm64-v8a",
"name": "android-base",
"hidden": true,
"inherits": "platform-headless-base",
"generator": "Ninja",
"toolchainFile": "$env{ANDROID_NDK_HOME}/build/cmake/android.toolchain.cmake",
"cacheVariables": {
"ANDROID_ABI": "arm64-v8a",
"ANDROID_PLATFORM": "android-26"
"ANDROID_PLATFORM": "android-26",
"ANDROID_STL": "c++_shared",
"PP_ENABLE_VR": "OFF",
"PP_ENABLE_OPENGL": "ON"
}
},
{
"name": "android-arm64",
"inherits": "android-base",
"displayName": "Android arm64-v8a",
"cacheVariables": {
"ANDROID_ABI": "arm64-v8a"
}
},
{
"name": "android-x64",
"inherits": "base",
"inherits": "android-base",
"displayName": "Android x86_64",
"generator": "Ninja",
"cacheVariables": {
"ANDROID_ABI": "x86_64",
"ANDROID_PLATFORM": "android-26"
"ANDROID_ABI": "x86_64"
}
},
{
"name": "android-quest-arm64",
"inherits": "android-base",
"displayName": "Android Quest arm64-v8a",
"cacheVariables": {
"ANDROID_ABI": "arm64-v8a",
"PP_ENABLE_VR": "ON",
"PP_ANDROID_FLAVOR": "quest"
}
},
{
"name": "android-focus-arm64",
"inherits": "android-base",
"displayName": "Android Focus/Wave arm64-v8a",
"cacheVariables": {
"ANDROID_ABI": "arm64-v8a",
"PP_ENABLE_VR": "ON",
"PP_ANDROID_FLAVOR": "focus"
}
},
{
"name": "emscripten",
"inherits": "base",
"inherits": "platform-headless-base",
"displayName": "Emscripten WebGL",
"generator": "Ninja",
"cacheVariables": {
@@ -90,13 +129,13 @@
},
{
"name": "macos",
"inherits": "base",
"inherits": "platform-headless-base",
"displayName": "macOS",
"generator": "Ninja"
},
{
"name": "ios-device",
"inherits": "base",
"inherits": "platform-headless-base",
"displayName": "iOS device",
"generator": "Xcode",
"cacheVariables": {
@@ -106,7 +145,7 @@
},
{
"name": "ios-simulator",
"inherits": "base",
"inherits": "platform-headless-base",
"displayName": "iOS simulator",
"generator": "Xcode",
"cacheVariables": {
@@ -131,6 +170,22 @@
{
"name": "linux-clang",
"configurePreset": "linux-clang"
},
{
"name": "android-arm64",
"configurePreset": "android-arm64"
},
{
"name": "android-x64",
"configurePreset": "android-x64"
},
{
"name": "android-quest-arm64",
"configurePreset": "android-quest-arm64"
},
{
"name": "android-focus-arm64",
"configurePreset": "android-focus-arm64"
}
],
"testPresets": [

View File

@@ -14,3 +14,6 @@ option(PP_ENABLE_TSAN "Enable ThreadSanitizer for headless targets where support
option(PP_ENABLE_MSVC_ANALYZE "Enable MSVC static analysis." OFF)
option(PP_ENABLE_CLANG_TIDY "Enable clang-tidy integration." OFF)
option(PP_ENABLE_CPPCHECK "Enable cppcheck integration." OFF)
set(PP_ANDROID_FLAVOR "standard" CACHE STRING "Android package flavor: standard, quest, or focus.")
set_property(CACHE PP_ANDROID_FLAVOR PROPERTY STRINGS standard quest focus)

View File

@@ -63,6 +63,8 @@ cmake --build --preset windows-msvc-default --config Debug --target PanoPainter
ctest --preset desktop-fast --build-config Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
cmake --preset android-arm64
cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests
```
Known local toolchain state:
@@ -71,6 +73,7 @@ Known local toolchain state:
- Local Visual Studio generator selected by CMake: Visual Studio 17 2022
- Android SDK: `C:\Users\omara\AppData\Local\Android\Sdk`
- Android NDK: `C:\Users\omara\AppData\Local\Android\Sdk\ndk\29.0.14206865`
- Android arm64 headless configure/build passes through root CMake.
- `vcpkg` is not on PATH yet; see DEBT-0007.
Known warnings after the current CMake app build:

View File

@@ -28,6 +28,7 @@ agent or engineer to remove them without reconstructing context from chat.
| DEBT-0006 | Open | Modernization | `pano_cli create-document` validates and emits JSON command contracts but does not yet invoke the legacy document/app model | The document model has not been extracted from `Canvas`/`App` yet | `pano_cli create-document --width 64 --height 32 --layers 2`; CTest `pano_cli_create_document_smoke` | Replace command contract implementation with real `pp_document` creation once Phase 4 extracts the document model |
| DEBT-0007 | Open | Modernization | `vcpkg.json` exists but CMake is not yet using a validated vcpkg toolchain on this machine | `vcpkg` is not available on PATH and Visual Studio reports manifest mode is disabled | `cmake --preset windows-msvc-default` currently configures with vendored dependencies | Add validated vcpkg toolchain/preset integration for desktop, Android, and Apple triplets |
| DEBT-0008 | Open | Modernization | `windows-msvc-default` preset is used for local validation because the VS 2026 generator is not installed here | The target VS 2026 preset must remain, but this machine configures with Visual Studio 17 2022 | `cmake --preset windows-msvc-default`; `ctest --preset desktop-fast --build-config Debug` | Validate `windows-vs2026-x64` on a machine with Visual Studio 2026 installed and make it the default Windows validation preset |
| DEBT-0009 | Open | Modernization | Android root CMake validation currently builds headless targets only, not APK/package variants | Platform app entrypoints still live in legacy Gradle/CMake projects and need Phase 6 alignment | `cmake --preset android-arm64`; `cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests` | Android standard, Quest, and Focus/Wave package targets consume shared component targets and have package smoke commands |
## Closed Debt

View File

@@ -142,8 +142,9 @@ Goal: make CMake the canonical source list without breaking existing projects.
Status: in progress. Root `CMakeLists.txt`, `CMakePresets.json`, and project
option targets exist. The Windows desktop app builds through CMake as
`PanoPainter`; the raw Visual Studio solution/project files were removed on
2026-05-31 by user decision. Non-Windows platform build files remain during
Phase 6 alignment.
2026-05-31 by user decision. Android arm64 now configures and builds headless
foundation/tool targets through the root CMake/NDK path. Non-Windows platform
app/package files remain during Phase 6 alignment.
Implementation tasks:
@@ -297,8 +298,10 @@ Gate:
Goal: split libraries while keeping current app behavior.
Status: started. `pp_foundation` exists with binary stream utilities and
boundary/overread tests. Continue extracting legacy-safe utilities before
moving assets, paint, or document behavior.
boundary/overread tests. It also owns strict decimal `uint32` parsing used by
`pano_cli`, with rejection tests for empty, signed, mixed, and overflowing
input. Continue extracting legacy-safe utilities before moving assets, paint,
or document behavior.
Implementation tasks:
@@ -498,20 +501,25 @@ Last verified on 2026-05-31:
```powershell
cmake --preset windows-msvc-default
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_tests pano_cli PanoPainter
cmake --build --preset windows-msvc-default --config Debug --target pp_foundation_binary_stream_tests pp_foundation_parse_tests pano_cli PanoPainter
ctest --preset desktop-fast --build-config Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\test.ps1 -Preset desktop-fast -Configuration Debug
powershell -ExecutionPolicy Bypass -File scripts\automation\build.ps1 -Preset windows-msvc-default -Configuration Debug -Target pano_cli
cmake --preset android-arm64
cmake --build --preset android-arm64 --target pp_foundation pano_cli pp_foundation_binary_stream_tests pp_foundation_parse_tests
```
Results:
- `pp_foundation_tests` passed.
- `pp_foundation_binary_stream_tests` passed.
- `pp_foundation_parse_tests` passed.
- `pano_cli_create_document_smoke` passed.
- `PanoPainter.exe` built through CMake at
`out/build/windows-msvc-default/Debug/PanoPainter.exe`.
- PowerShell build/test automation wrappers return JSON summaries and passed
local smoke checks.
- Android arm64 configured with NDK 29.0.14206865 and compiled headless
foundation/tool/test targets.
- Known remaining warnings: legacy project/vendor diagnostics, Visual Studio
vcpkg-manifest warning, `LNK4099` missing libyuv PDBs, and `LNK4098` runtime
library conflict from retained vendor binaries.

37
src/foundation/parse.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include "foundation/parse.h"
#include <charconv>
namespace pp::foundation {
Result<std::uint32_t> parse_u32(std::string_view text) noexcept
{
if (text.empty()) {
return Result<std::uint32_t>::failure(
Status::invalid_argument("value must not be empty"));
}
if (text.front() == '-' || text.front() == '+') {
return Result<std::uint32_t>::failure(
Status::invalid_argument("value must be an unsigned integer without a sign"));
}
std::uint32_t value = 0;
const auto* begin = text.data();
const auto* end = text.data() + text.size();
const auto [ptr, ec] = std::from_chars(begin, end, value);
if (ec == std::errc::result_out_of_range) {
return Result<std::uint32_t>::failure(
Status::out_of_range("value is outside the uint32 range"));
}
if (ec != std::errc {} || ptr != end) {
return Result<std::uint32_t>::failure(
Status::invalid_argument("value must contain only decimal digits"));
}
return Result<std::uint32_t>::success(value);
}
}

12
src/foundation/parse.h Normal file
View File

@@ -0,0 +1,12 @@
#pragma once
#include "foundation/result.h"
#include <cstdint>
#include <string_view>
namespace pp::foundation {
[[nodiscard]] Result<std::uint32_t> parse_u32(std::string_view text) noexcept;
}

View File

@@ -5,14 +5,25 @@ target_link_libraries(pp_test_harness INTERFACE
pp_project_options
pp_project_warnings)
add_executable(pp_foundation_tests
foundation/binary_stream_tests.cpp)
target_link_libraries(pp_foundation_tests PRIVATE
add_executable(pp_foundation_binary_stream_tests
foundation/binary_stream_tests.cpp
)
target_link_libraries(pp_foundation_binary_stream_tests PRIVATE
pp_foundation
pp_test_harness)
add_test(NAME pp_foundation_tests COMMAND pp_foundation_tests)
set_tests_properties(pp_foundation_tests PROPERTIES
add_test(NAME pp_foundation_binary_stream_tests COMMAND pp_foundation_binary_stream_tests)
set_tests_properties(pp_foundation_binary_stream_tests PROPERTIES
LABELS "foundation;desktop-fast")
add_executable(pp_foundation_parse_tests
foundation/parse_tests.cpp)
target_link_libraries(pp_foundation_parse_tests PRIVATE
pp_foundation
pp_test_harness)
add_test(NAME pp_foundation_parse_tests COMMAND pp_foundation_parse_tests)
set_tests_properties(pp_foundation_parse_tests PROPERTIES
LABELS "foundation;desktop-fast")
if(TARGET pano_cli)

View File

@@ -0,0 +1,66 @@
#include "foundation/parse.h"
#include "test_harness.h"
#include <cstdint>
#include <string_view>
using pp::foundation::parse_u32;
using pp::foundation::StatusCode;
namespace {
void accepts_decimal_uint32_values(pp::tests::Harness& h)
{
const auto zero = parse_u32("0");
const auto ordinary = parse_u32("12345");
const auto max = parse_u32("4294967295");
PP_EXPECT(h, zero.ok());
PP_EXPECT(h, zero.value() == 0U);
PP_EXPECT(h, ordinary.ok());
PP_EXPECT(h, ordinary.value() == 12345U);
PP_EXPECT(h, max.ok());
PP_EXPECT(h, max.value() == UINT32_MAX);
}
void rejects_empty_signed_and_mixed_input(pp::tests::Harness& h)
{
const auto empty = parse_u32("");
const auto negative = parse_u32("-1");
const auto positive = parse_u32("+1");
const auto trailing = parse_u32("12px");
const auto spaced = parse_u32(" 12");
PP_EXPECT(h, !empty.ok());
PP_EXPECT(h, empty.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !negative.ok());
PP_EXPECT(h, negative.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !positive.ok());
PP_EXPECT(h, positive.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !trailing.ok());
PP_EXPECT(h, trailing.status().code == StatusCode::invalid_argument);
PP_EXPECT(h, !spaced.ok());
PP_EXPECT(h, spaced.status().code == StatusCode::invalid_argument);
}
void rejects_overflow_without_wrapping(pp::tests::Harness& h)
{
const auto overflow = parse_u32("4294967296");
const auto very_large = parse_u32("999999999999999999999999999999999999");
PP_EXPECT(h, !overflow.ok());
PP_EXPECT(h, overflow.status().code == StatusCode::out_of_range);
PP_EXPECT(h, !very_large.ok());
PP_EXPECT(h, very_large.status().code == StatusCode::out_of_range);
}
}
int main()
{
pp::tests::Harness harness;
harness.run("accepts_decimal_uint32_values", accepts_decimal_uint32_values);
harness.run("rejects_empty_signed_and_mixed_input", rejects_empty_signed_and_mixed_input);
harness.run("rejects_overflow_without_wrapping", rejects_overflow_without_wrapping);
return harness.finish();
}

View File

@@ -1,6 +1,6 @@
#include "foundation/parse.h"
#include "foundation/result.h"
#include <charconv>
#include <cstdint>
#include <iostream>
#include <string_view>
@@ -13,14 +13,6 @@ struct DocumentArgs {
std::uint32_t layers = 1;
};
bool parse_u32(std::string_view text, std::uint32_t& value)
{
const auto* begin = text.data();
const auto* end = text.data() + text.size();
const auto [ptr, ec] = std::from_chars(begin, end, value);
return ec == std::errc {} && ptr == end;
}
void print_error(std::string_view command, std::string_view message)
{
std::cout << "{\"ok\":false,\"command\":\"" << command
@@ -44,17 +36,17 @@ pp::foundation::Status parse_document_args(int argc, char** argv, DocumentArgs&
return pp::foundation::Status::invalid_argument("missing value for option");
}
std::uint32_t value = 0;
if (!parse_u32(argv[++i], value)) {
return pp::foundation::Status::invalid_argument("option value must be an unsigned integer");
const auto value = pp::foundation::parse_u32(argv[++i]);
if (!value) {
return value.status();
}
if (key == "--width") {
args.width = value;
args.width = value.value();
} else if (key == "--height") {
args.height = value;
args.height = value.value();
} else {
args.layers = value;
args.layers = value.value();
}
} else {
return pp::foundation::Status::invalid_argument("unknown option");