Skip to content

Commit b864823

Browse files
committed
test: preserve mobile owners and real platform consumers
1 parent 9d3b934 commit b864823

22 files changed

Lines changed: 1302 additions & 195 deletions

‎.github/workflows/build-artifacts.yml‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -511,6 +511,11 @@ jobs:
511511
# Public repositories receive unlimited standard GitHub-hosted runner
512512
# minutes, so pushes, pull requests, and releases exercise macOS too.
513513
if: ${{ github.event_name != 'workflow_dispatch' || inputs.build_macos }}
514+
env:
515+
# MoltenVK's documented discrete-binding path avoids an unsupported
516+
# argument-encoder selector in Apple's hosted paravirtualized Metal GPU.
517+
# This still executes real Vulkan/Metal and retains every Vulkan selector.
518+
MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS: "0"
514519
strategy:
515520
# Apple Silicon and Intel Macs each get a native build; one failing
516521
# architecture should still let the other produce its DMG.
@@ -847,6 +852,8 @@ jobs:
847852
run: nix develop .#native -c env DEPLOY_APP=0 bash tools/build-macos.sh
848853
- name: Run native traditional then GPU GUI tests in isolated Apple TV simulator
849854
if: ${{ matrix.sdk == 'appletvsimulator' }}
855+
env:
856+
SPOOL_TEST_DEVICE_CLEANUP_REGRESSIONS: "1"
850857
run: bash tools/apple/smoke-tvos.sh build/tvos/appletvsimulator-arm64/install/Spool.app
851858
- name: Package artifact-audited unsigned device IPA (no device runtime claim)
852859
if: ${{ matrix.sdk == 'appletvos' }}
@@ -871,8 +878,7 @@ jobs:
871878
name: internal-tvos-simulator-arm64
872879
path: |
873880
${{ runner.temp }}/tvos-simulator-arm64.tar.gz
874-
build/tvos/smoke/*.png
875-
build/tvos/smoke/*.json
881+
build/tvos/smoke/**/safe-export/**
876882
retention-days: ${{ inputs.release && 1 || 7 }}
877883
- name: Save coherent Apple dependency cache
878884
if: ${{ env.SPOOL_CACHE_WRITE == 'true' && !inputs.release && steps.tvos-source-cache.outputs.cache-hit != 'true' }}
@@ -1226,6 +1232,8 @@ jobs:
12261232

12271233
- name: Run native and real app GUI phases on phone and actual Android TV images
12281234
if: ${{ matrix.abi == 'x86_64' }}
1235+
env:
1236+
SPOOL_TEST_DEVICE_CLEANUP_REGRESSIONS: "1"
12291237
run: |
12301238
status=0
12311239
for form_factor in phone tv; do
@@ -1241,8 +1249,7 @@ jobs:
12411249
with:
12421250
name: internal-spool-android-tests
12431251
path: |
1244-
build/android/launch-test
1245-
build/android/emulator-*.log
1252+
build/android/launch-test/**/safe-export/**
12461253
dist/android/spool-tests-x86_64.apk
12471254
dist/android/spool-e2e-tests-x86_64.apk
12481255
dist/android/spool-e2e-app-x86_64.apk

‎CMakeLists.txt‎

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -954,13 +954,39 @@ if(BUILD_TESTING AND NOT SPOOL_WEBOS)
954954
# mutable fixture packages. Keep every test process independent of the
955955
# persistent QML/JavaScript cache, not only the provider test group.
956956
set(SPOOL_TEST_ENV "QML_DISABLE_DISK_CACHE=1")
957-
if(SPOOL_QT_CORE_LOCATION)
958-
get_filename_component(SPOOL_QT_LIB_DIR "${SPOOL_QT_CORE_LOCATION}" DIRECTORY)
959-
get_filename_component(SPOOL_QT_PREFIX "${SPOOL_QT_LIB_DIR}" DIRECTORY)
960-
set(SPOOL_QT_PLUGIN_DIR "${SPOOL_QT_PREFIX}/lib/qt-6/plugins")
961-
if(EXISTS "${SPOOL_QT_PLUGIN_DIR}")
962-
list(APPEND SPOOL_TEST_ENV "QT_PLUGIN_PATH=${SPOOL_QT_PLUGIN_DIR}")
963-
endif()
957+
# Resolve the actual configured Qt packages, not a dylib's directory:
958+
# macOS framework locations and Nix's split QML package differ from the
959+
# ordinary Qt prefix layout. These are the same layouts the native launcher
960+
# admits, pinned by the imported package configurations used in this build.
961+
set(SPOOL_TEST_QML_DIRS "")
962+
set(SPOOL_TEST_PLUGIN_DIRS "")
963+
foreach(qt_package_dir IN ITEMS "${Qt6Core_DIR}" "${Qt6Qml_DIR}" "${Qt6Svg_DIR}")
964+
get_filename_component(qt_package_prefix "${qt_package_dir}/../../.." ABSOLUTE)
965+
foreach(qml_suffix IN ITEMS qml lib/qt-6/qml lib/qt6/qml)
966+
if(IS_DIRECTORY "${qt_package_prefix}/${qml_suffix}")
967+
list(APPEND SPOOL_TEST_QML_DIRS "${qt_package_prefix}/${qml_suffix}")
968+
endif()
969+
endforeach()
970+
foreach(plugin_suffix IN ITEMS plugins lib/qt-6/plugins lib/qt6/plugins)
971+
if(IS_DIRECTORY "${qt_package_prefix}/${plugin_suffix}")
972+
list(APPEND SPOOL_TEST_PLUGIN_DIRS "${qt_package_prefix}/${plugin_suffix}")
973+
endif()
974+
endforeach()
975+
endforeach()
976+
list(REMOVE_DUPLICATES SPOOL_TEST_QML_DIRS)
977+
list(REMOVE_DUPLICATES SPOOL_TEST_PLUGIN_DIRS)
978+
if(WIN32)
979+
set(qt_runtime_separator "\\;")
980+
else()
981+
set(qt_runtime_separator ":")
982+
endif()
983+
if(SPOOL_TEST_QML_DIRS)
984+
list(JOIN SPOOL_TEST_QML_DIRS "${qt_runtime_separator}" qt_test_qml_paths)
985+
list(APPEND SPOOL_TEST_ENV "QML2_IMPORT_PATH=${qt_test_qml_paths}" "QML_IMPORT_PATH=${qt_test_qml_paths}")
986+
endif()
987+
if(SPOOL_TEST_PLUGIN_DIRS)
988+
list(JOIN SPOOL_TEST_PLUGIN_DIRS "${qt_runtime_separator}" qt_test_plugin_paths)
989+
list(APPEND SPOOL_TEST_ENV "QT_PLUGIN_PATH=${qt_test_plugin_paths}")
964990
endif()
965991
set(SPOOL_TRADITIONAL_TEST_SOURCES src/platform/webos/WebOSAudioSyncPolicy.cpp)
966992
foreach(test_group IN LISTS SPOOL_TEST_GROUPS)
@@ -994,12 +1020,15 @@ if(BUILD_TESTING AND NOT SPOOL_WEBOS)
9941020
else()
9951021
find_package(Threads REQUIRED)
9961022
find_package(Qt6 REQUIRED COMPONENTS QuickTest)
1023+
get_target_property(SPOOL_APP_SOURCES spool SOURCES)
1024+
set(SPOOL_TEST_PLATFORM_SOURCES ${SPOOL_APP_SOURCES})
1025+
list(FILTER SPOOL_TEST_PLATFORM_SOURCES INCLUDE REGEX "/[A-Za-z]+PlatformCapabilities\\.cpp$")
9971026
add_executable(spool-tests tests/TestRunner.cpp tests/TestVariants.cpp ${SPOOL_TRADITIONAL_TEST_SOURCES})
9981027
target_include_directories(spool-tests PRIVATE tests src "${CMAKE_CURRENT_BINARY_DIR}/generated")
9991028
target_compile_definitions(spool-tests PRIVATE TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
10001029
SPOOL_TEST_RUNNER=1)
10011030
target_link_libraries(spool-tests PRIVATE spool-core Threads::Threads ${SPOOL_PROVIDER_BUNDLE_TARGET})
1002-
target_sources(spool-tests PRIVATE sdk/provider-contract-runner.cpp)
1031+
target_sources(spool-tests PRIVATE sdk/provider-contract-runner.cpp ${SPOOL_TEST_PLATFORM_SOURCES})
10031032
if(TARGET spoolet)
10041033
target_sources(spool-tests PRIVATE tests/automation/LocalControlProtocolTest.cpp
10051034
tests/automation/LocalControlAdmissionTest.cpp src/automation/LocalControlProtocol.cpp
@@ -1106,6 +1135,8 @@ if(BUILD_TESTING AND NOT SPOOL_WEBOS)
11061135
qt_add_executable(spool-e2e-tests ${SPOOL_E2E_SOURCES})
11071136
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/native-spool-e2e-path.txt"
11081137
CONTENT "$<TARGET_FILE:spool-e2e-tests>\n")
1138+
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/native-spoolet-path.txt"
1139+
CONTENT "$<TARGET_FILE:spoolet>\n")
11091140
target_include_directories(spool-e2e-tests PRIVATE tests src "${CMAKE_CURRENT_BINARY_DIR}/generated")
11101141
target_compile_definitions(spool-e2e-tests PRIVATE TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
11111142
SPOOL_E2E_APP="$<TARGET_FILE:spool>" SPOOL_E2E_SPOOLET="$<TARGET_FILE:spoolet>")
@@ -1168,9 +1199,6 @@ if(BUILD_TESTING AND NOT SPOOL_WEBOS)
11681199
get_target_property(SPOOL_TEST_SCAN_ARGS spool QT_QML_IMPORT_SCANNER_EXTRA_ARGS)
11691200
set_property(TARGET spool-e2e-tests PROPERTY QT_QML_IMPORT_SCANNER_EXTRA_ARGS ${SPOOL_TEST_SCAN_ARGS})
11701201
endif()
1171-
get_target_property(SPOOL_APP_SOURCES spool SOURCES)
1172-
set(SPOOL_TEST_PLATFORM_SOURCES ${SPOOL_APP_SOURCES})
1173-
list(FILTER SPOOL_TEST_PLATFORM_SOURCES INCLUDE REGEX "/[A-Za-z]+PlatformCapabilities\\.cpp$")
11741202
set(SPOOL_TEST_QML_RESOURCES ${SPOOL_APP_SOURCES})
11751203
list(FILTER SPOOL_TEST_QML_RESOURCES INCLUDE REGEX "/qrc_(qmake_Spool|application_icons|trickplay_preview_shaders|spool_raw_qml_[0-9]+(_extra_qmldirs)?)\\.cpp$")
11761204
# Reuse the application's generated resources rather than declaring a

‎README.md‎

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,12 @@ nix develop .#android -c bash tools/android/build.sh
714714

715715
That runs the three cached stages -- `build-dependencies.sh`, `build-qt6.sh`,
716716
`build-apks.sh` -- which can also be invoked on their own.
717+
Android's libmpv cache includes the actual source revision, tracked/untracked
718+
source changes, NDK/API/ABI, build recipe and feature manifests as well as its
719+
static dependency stamps. `MPV_SRC` can select another verified source checkout;
720+
the default remains the repository's `mpv` submodule. A matching revision alone
721+
never admits stale dirty source bytes.
722+
717723

718724
Entering the Android shell may build native Qt host tools before the Android
719725
cross-build starts. These must match the pinned Qt version: `moc`, QML generators,
@@ -839,6 +845,11 @@ Android's host import scanner uses the same pinned recursive-exclusion patch as
839845
tvOS/webOS. This keeps vendored Qt's intentionally malformed test QML outside the
840846
application/test targets' source-root scan while their exact resource QML and
841847
real plugin dependencies remain scanned and packaged.
848+
Windows QML selectors direct QtTest's ordinary text logger to their retained
849+
stdout/stderr pipe instead of the debugger-only OutputDebugString sink. Explicit
850+
CLI logging choices remain available. Retry checks compare the original native
851+
failure bytes before/after retry, independent of the host's text newline encoding.
852+
842853

843854
The additional `linux-sanitizers` CI job uses `SPOOL_SANITIZERS=ON` to instrument
844855
the actual production core, official upstream libzstd static decoder, and unified
@@ -856,6 +867,14 @@ The traditional binary is `spool-tests`; the GUI binary is `spool-e2e-tests`.
856867
Each contains a selector registry and supervises fresh executions of **itself**.
857868
The binary CLI exposes `--list`, `--child SELECTOR`, and
858869
`--run-all --workers N --results FILE`, with `--resume`/`--retry-failed`.
870+
Device adapters also pass `--log ABSOLUTE_PRIVATE_PATH` so Activity/UIKit native
871+
stdout/stderr conditions survive even when the OS does not expose a console.
872+
Those raw native diagnostics stay owner-private; safe exports remain allowlisted.
873+
Each device attempt has a fresh private directory nonce, so a new invocation
874+
cannot overwrite earlier fault evidence or collide with its private capability
875+
files. Kernel-wide CPU and memory availability are checked independently; denied
876+
CPU proc access does not discard readable Android memory counters.
877+
859878
Separate selectors isolate QApplication ownership, mutable environments,
860879
explicit exits and crashes without creating an executable for every test.
861880
All traditional selectors finish before GUI e2e starts, and either phase's
@@ -882,7 +901,14 @@ See `tools/patches/mesa-wayland-fifo-presentation-clock.patch` for the exact
882901
upstream source, introducing commit and protocol references.
883902

884903
macOS uses its real graphics device and bundled/Nix MoltenVK, not a software
885-
scenegraph. Windows deliberately tests the supported OpenGL embedding path with
904+
scenegraph. Hosted Apple paravirtualized GPUs use MoltenVK's documented
905+
`MVK_CONFIG_USE_METAL_ARGUMENT_BUFFERS=0` discrete-binding configuration, avoiding
906+
an unsupported Metal argument-encoder probe while retaining every real Vulkan
907+
selector. This is the [upstream-recommended VM configuration](https://github.com/KhronosGroup/MoltenVK/issues/2373),
908+
not a Metal implementation fork or a change to users' default graphics backend.
909+
QtTest QML/plugin paths come from the configured matching-version Qt package
910+
prefixes, including split Nix modules and macOS frameworks.
911+
Windows deliberately tests the supported OpenGL embedding path with
886912
SHA-256-pinned Mesa WGL llvmpipe: Qt and real libmpv share OpenGL, not D3D WARP or
887913
Qt's software scenegraph. This Windows configuration is not proof of Vulkan or
888914
D3D playback. The real host app journey additionally needs Tesseract English OCR
@@ -900,6 +926,9 @@ native launcher and stages the product's shipped fonts. Failed journeys retain
900926
owner-private screenshots, logs and isolated data under `test-artifacts`
901927
(`SPOOL_E2E_ARTIFACT_DIR` can select its parent); raw logs and credentials are
902928
never printed. Successful journeys remove their temporary data.
929+
Screenshot requests use the public Qt Quick repaint API, await a real swapped
930+
frame, then perform GUI-thread framebuffer readback. This remains an actual
931+
graphics capture, not a software scenegraph substitute or screenshot-only test.
903932
The loopback Jellyfin fixture supplies a quiet real event WebSocket. During the
904933
offline-download scenario it rejects every new authenticated HTTP request or
905934
WebSocket handshake, including playback negotiation and media requests; the
@@ -913,6 +942,38 @@ Android and tvOS simulator adapters launch each selector in the same installed
913942
phase APK/bundle and require a fresh native nonce receipt. Then the same host GUI
914943
binary drives the actual mobile application; `native-spool-e2e-path.txt` in the
915944
host build directory supplies its generated target path without bundle guesses.
945+
`native-spoolet-path.txt` supplies the matching public CLI consumer for cleanup
946+
regressions. Both native phase bundles include the same filesystem fixture
947+
loader and real shipped OSD fonts; host-only fork/process-supervisor code is not
948+
compiled into UIKit/Activity bundles.
949+
Android receipt/file operations use shell-v2 without a PTY, preserving actual
950+
remote exit codes and binary bytes. Mobile screenshot checks match the real
951+
window's logical size times device-pixel ratio and the returned framebuffer
952+
dimensions, rather than imposing a desktop-width threshold on small phones.
953+
Mobile consumer tests retain real persistence, audio-policy, decoded-media,
954+
download-byte and font-loader checks; extracted fixture paths are canonicalized
955+
and downloads use the platform's actual allowed destination choice.
956+
957+
958+
The host adapter owns each production mobile launch and its exact Android
959+
forward/reverse mappings. A private nonce-bound capability file lets the
960+
controller request those operations; adapter cleanup still runs if the controller
961+
crashes or times out, without relying on C++ destructors or removing unrelated
962+
device resources. Request framing has an absolute deadline, including before
963+
authentication. CI enables `SPOOL_TEST_DEVICE_CLEANUP_REGRESSIONS=1` to inject
964+
real controller timeout/crash after public `spoolet` readiness, test an
965+
unauthenticated slow client, verify process/mapping retirement, and exercise
966+
another actual consumer. The inner controller fault remains crashed/timed-out;
967+
only the independent cleanup assertion can pass.
968+
969+
Mobile attempts receive their own `SPOOL_E2E_ARTIFACT_DIR`. CI uploads only the
970+
adapter's `safe-export` subtree: schema-generated native/controller results,
971+
sanitized journals and attempt diagnostics, and named screenshots. Capability
972+
files, control descriptors, credentials, raw product logs and isolated data roots
973+
remain private. Standalone fault checks use `tools/run-device-tests.py --phase
974+
e2e --cleanup-regression timeout|crash` with the actual isolated device and host
975+
controller/CLI paths supplied by the platform driver.
976+
916977
Device phases run serially because one installed activity/application cannot
917978
host parallel native processes safely. Native traditional coverage includes
918979
the package consumer's compressed, corrupt, truncated and checksummed inputs

‎src/automation/LocalCommandServer.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,9 @@ void LocalCommandServer::screenshot(QIODevice *socket, const QString& id, const
391391
{ { QStringLiteral("id"), id }, { QStringLiteral("ok"), true }, { QStringLiteral("result"), result } });
392392
},
393393
Qt::ConnectionType(Qt::QueuedConnection | Qt::SingleShotConnection));
394-
m_window->requestUpdate();
394+
// Request a scene-graph repaint, not just a platform-window update event.
395+
// QQuickWindow::update() schedules a frame even when the scene is unchanged.
396+
m_window->update();
395397
}
396398

397399
void LocalCommandServer::dispatch(QIODevice *socket, const QJsonObject& request)

‎tests/QmlTestRunner.cpp‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,23 @@ public slots:
7272

7373
int spoolRunQml(int argc, char **argv, const char *input)
7474
{
75-
std::vector<char *> arguments(argv, argv + argc);
75+
#ifdef Q_OS_WIN
76+
// Qt's plain stdout logger otherwise uses OutputDebugString when the
77+
// selector has no console. The supervisor retains stdout/stderr in its
78+
// private per-attempt log, so explicitly keep test results on that stream.
79+
qputenv("QT_FORCE_STDERR_LOGGING", "1");
80+
#endif
81+
std::vector<char *> arguments;
82+
arguments.reserve(size_t(argc) + 5);
83+
arguments.insert(arguments.end(), argv, argv + argc);
84+
char logOption[] = "-o";
85+
char logOutput[] = "-,txt";
86+
// Normal supervised selectors have no QtTest arguments. Leave explicit
87+
// command-line logging/format choices intact for targeted invocations.
88+
if (argc == 1) {
89+
arguments.push_back(logOption);
90+
arguments.push_back(logOutput);
91+
}
7692
QByteArray option("-input");
7793
QByteArray path(input);
7894
arguments.push_back(option.data());

0 commit comments

Comments
 (0)