makepad/libs/makepad_test/ANDROID.md
andodeki 477421e314 nigig: NIGIG test-mode forwarding, custom manifest hook, ortho camera support
- makepad_test/runtime.rs: forward NIGIG_TEST_MODE from host env to the
  Android app via 'am start' intent extra; add wait_timeout (60s) used by
  wait_visible/wait_hidden/wait_count; make query_widgets tolerant of
  snapshot timeouts; grant READ_CONTACTS during adb setup
- makepad-platform android_jni.rs: read makepad.NIGIG_TEST_MODE intent
  extra and surface it as the NIGIG_TEST_MODE env var via apply_studio_env
- cargo_makepad compile.rs: support verbatim custom AndroidManifest.xml in
  addition to the templated variant
- makepad-xr xr_root.rs: add ortho camera controls (ortho, ortho_height,
  min/max), derive Debug on XrCamera
- docs: ANDROID.md and DESKTOP_VISIBLE.md for makepad_test
2026-09-03 10:55:00 +03:00

195 lines
8.6 KiB
Markdown

# makepad_test on Android
How `makepad_test` runs Makepad UI tests on real Android devices, and the
steps that were taken to get it working end-to-end. This is the Android
companion to [GUIDE.md](./GUIDE.md), which covers the desktop headless and
visible-Studio modes.
## What "Android mode" does
When `MAKEPAD_TEST_ANDROID=1` is set, the test runtime:
1. starts an in-process `StudioHub` that listens on `127.0.0.1:<port>` on the host
2. forwards that port to the device with `adb reverse tcp:<port> tcp:<port>`
3. builds the APK through `cargo-makepad` (`android build -p <package>`)
4. installs the APK with `adb install -r`
5. force-stops any previous instance of the app
6. launches the app with `am start`, passing the hub address, build id, and crate name as intent extras
7. waits for the app to connect to the hub (`AppStarted`)
8. settles until the app actually answers a request (see "Startup race fix")
9. drives the test through the normal `TestApp` / `Locator` / `Selector` APIs
The app dials `127.0.0.1:<port>` on the device; `adb reverse` maps that back
to the host listener, so no separate device-side network setup is needed.
## Two runtime modes
| Mode | Activity launched | Platform build | When |
|------|------------------|----------------|------|
| Legacy Java | `dev.makepad.<pkg>/.MakepadApp` | no `--cfg native_activity` | default |
| NativeActivity | `<pkg>/android.app.NativeActivity` | `--cfg native_activity` + `--native-activity` build flag | `MAKEPAD_TEST_NATIVE_ACTIVITY=1` |
- **Legacy Java** is the default. `cargo-makepad` generates a `MakepadApp`
Java `Activity` that bridges into `MakepadNative.activityOnCreate`.
- **NativeActivity** requires the flag on both sides: the platform crate must
be compiled with `--cfg native_activity` (so the `native_activity.rs`
module and the `ANativeActivity_onCreate` entry point are used instead of
the Java activity), and the APK must be built with
`cargo makepad android --native-activity build -p <pkg>`.
`build_android_apk` in `runtime.rs` adds the flag automatically when
`config.android_native_activity` is set.
The launch is wired up in `adb_launch` in `libs/makepad_test/src/runtime.rs`:
```text
am start -n <activity>
-e makepad.STUDIO_HOST 127.0.0.1:<port>
-e makepad.STUDIO_BUILD <build_id>
-e makepad.STUDIO_CRATE <package>
```
## Environment variables
| Variable | Purpose | Default |
|----------|---------|---------|
| `MAKEPAD_TEST_ANDROID` | enable Android mode (any truthy value) | unset (off) |
| `MAKEPAD_TEST_DEVICE` | adb device serial (`-s <serial>`) | unset (default adb device) |
| `MAKEPAD_TEST_ADB` | path to the adb binary | `adb` on `PATH` |
| `MAKEPAD_TEST_ANDROID_PORT` | host hub port / adb reverse port | `8001` |
| `MAKEPAD_TEST_NATIVE_ACTIVITY` | use NativeActivity instead of legacy Java | unset (legacy) |
| `MAKEPAD_WORKSPACE_ROOT` | workspace root holding `tools/cargo_makepad` | auto-detected by walking up from the manifest dir |
| `MAKEPAD_STUDIO_HUB_DEBUG` | print every hub child line (build + app output) | unset |
The hub binds `127.0.0.1:<port>` and may fall back to a different port if
`8001` is already taken (for example by a real Studio). `start_android_app`
reads the port the hub actually bound (`connection.studio_addr()`) and routes
`adb reverse` and the intent extras at that port, never a hardcoded one.
## Build system requirements
- Android builds use the **nightly** toolchain: the platform crate needs
`cargo +nightly` and the NDK target installed for `aarch64-linux-android`.
- The `cargo-makepad` binary must exist at `<workspace>/target/release/cargo-makepad`:
`cargo build --release -p cargo-makepad`.
- The APK lands at
`target/android/makepad-android-apk/<pkg_underscored>/apk/<pkg_underscored>.apk`.
- `tools/cargo_makepad/src/android/compile.rs` gained a `native_activity`
argument that flows into `rust_build` (alongside the profile-based
`prefer_dynamic` choice) and is plumbed through both APK build call sites.
Sanity-compile checks that must stay green:
```bash
# legacy Java cfg
cargo +nightly check -p makepad-platform --target aarch64-linux-android
# native-activity cfg
RUSTFLAGS="--cfg native_activity" cargo +nightly check -p makepad-platform --target aarch64-linux-android
```
## Device setup
```bash
export MAKEPAD_TEST_ADB="<repo>/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb"
export MAKEPAD_TEST_DEVICE="RF8Y103NERA" # your device serial
$MAKEPAD_TEST_ADB devices # must list the device
$MAKEPAD_TEST_ADB -s "$MAKEPAD_TEST_DEVICE" wait-for-device
```
- The device must be **authorized** (accept the USB debugging prompt).
- Keep the screen on during the run:
`adb -s <serial> shell svc power stayon true`.
- On some devices a screen-off can still slow or starve the app; test with the
screen awake.
## Running the tests
Legacy Java mode (default):
```bash
MAKEPAD_TEST_ANDROID=1 \
MAKEPAD_TEST_DEVICE="RF8Y103NERA" \
MAKEPAD_TEST_ADB="<repo>/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb" \
cargo test --release -p makepad-example-counter --test ui -- --test-threads=1
```
NativeActivity mode:
```bash
MAKEPAD_TEST_ANDROID=1 \
MAKEPAD_TEST_NATIVE_ACTIVITY=1 \
MAKEPAD_TEST_DEVICE="RF8Y103NERA" \
MAKEPAD_TEST_ADB="<repo>/tools/cargo_makepad/android_33_macos_x64/platform-tools/adb" \
cargo test --release -p makepad-example-counter --test ui -- --test-threads=1
```
`--test-threads=1` is required: each test owns the shared hub port and the
serialized app session.
Progress is printed to stderr with `[makepad-test] Android: ...` lines as each
phase completes (forward, build, install, force-stop, launch, connect,
responsive).
## Expected results
Recorded runs on the reconciled tree:
| Mode | Device | Result | Time |
|------|--------|--------|------|
| Native | RF8Y103NERA (SM-A165F) | 2 passed | 515.71s |
| Native | R28M52LJP2Y (SM-A6060), screen off | 2 passed | 179.65s |
| Legacy | R28M52LJP2Y (SM-A6060) | 2 passed | 361.76s |
| Legacy | RF8Y103NERA (SM-A165F) | 2 passed | 957.16s |
| Native | RF8Y103NERA (SM-A165F) | 2 passed | 520.70s |
The APK build dominates the wall time; app install and test execution are the
small remainder.
## The startup race fix (settle step)
**Symptom:** the app connected (`AppStarted`) but the first widget query or
click was lost, failing the test with a timeout on the first interaction.
**Root cause:** the websocket connects on a background thread before the app's
event loop is up. A cold start can answer the handshake well before it can
service hub requests. Legacy Java starts are the slowest: the first frame (and
with it the main loop that drains requests) only comes after the
`SurfaceView` surface materializes, which on a first launch after install can
exceed the per-request `ACTION_TIMEOUT` (10s).
**Fix:** after `wait_for_android_app_started`, call
`wait_for_android_app_responsive` (`runtime.rs:1754`). It repeatedly sends
`ClientToHub::WidgetTreeDump` and waits for a matching `WidgetTreeDump`
reply, with a per-attempt `ACTION_TIMEOUT` and an overall
`ANDROID_STARTUP_TIMEOUT` (120s) deadline. The test body's first query only
starts once the app has actually answered a request, closing the boot race.
## Debugging
Enable hub transport diagnostics, which echo every child stdout/stderr line
(APK build output, app logs, protocol messages):
```bash
MAKEPAD_STUDIO_HUB_DEBUG=1 ... cargo test --release -p makepad-example-counter --test ui -- --test-threads=1 --nocapture
```
Failure artifacts are written to `target/makepad_test/<package>/<test>/`
(`failure.txt`, `failure-screenshot.png`, logs), exactly like desktop mode.
## Troubleshooting
| Symptom | Likely cause / fix |
|---------|--------------------|
| `adb: error: device '<serial>' not found` | device disconnected; reconnect USB and `adb devices` |
| `device unauthorized` | accept the USB debugging prompt on the device |
| `timed out waiting for Android app to connect to hub` | wrong port / stale `adb reverse`; the hub binds a fallback port, so make sure adb and the app use the actually-bound port (already handled in code) |
| `timed out waiting for Android app to become responsive` | very slow cold start; raise `ANDROID_STARTUP_TIMEOUT`, keep the screen on, or rerun once warm |
| `cargo-makepad not found at ...` | `cargo build --release -p cargo-makepad` first |
| test fails only on the very first launch after install | known cold-start surface race; rerun warm, or run the legacy case twice |
## Current limitations
- one device at a time (`-s <serial>` targets a single device)
- one app session per test (serial suite with `--test-threads=1`)
- the APK build happens inside the test process, so the first test of a suite
is the slow one; subsequent tests reuse the built APK