zig-mobile-runner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
CALLER_CWD="$(pwd -P)"
|
|
6
|
+
cd "$ROOT"
|
|
7
|
+
|
|
8
|
+
# Some sandboxed environments do not allow writing to the default temp directory
|
|
9
|
+
# (/var/folders, /tmp). Use a repo-local TMPDIR so xcrun/mktemp/heredocs work.
|
|
10
|
+
if [[ -z "${TMPDIR:-}" || ! -w "${TMPDIR:-/nonexistent}" ]]; then
|
|
11
|
+
TMPDIR="$ROOT/traces/tmp"
|
|
12
|
+
mkdir -p "$TMPDIR"
|
|
13
|
+
export TMPDIR
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
APP_ROOT="${APP_ROOT:-}"
|
|
17
|
+
APP_PATH="${APP_PATH:-}"
|
|
18
|
+
DEVICE="${DEVICE:-booted}"
|
|
19
|
+
IOS_DEVICE_TYPE="${IOS_DEVICE_TYPE:-simulator}"
|
|
20
|
+
TRACE_ROOT="${TRACE_ROOT:-$CALLER_CWD/traces/ios-app-pilot-$(date +%Y%m%d-%H%M%S)}"
|
|
21
|
+
ZMR_BIN="${ZMR_BIN:-$(command -v zmr 2>/dev/null || printf '%s' "$ROOT/zig-out/bin/zmr")}"
|
|
22
|
+
XCRUN="${XCRUN:-xcrun}"
|
|
23
|
+
APP_ID="${APP_ID:-com.example.mobiletest}"
|
|
24
|
+
IOS_SHIM="${IOS_SHIM:-}"
|
|
25
|
+
PREWARM_IOS_SHIM="${PREWARM_IOS_SHIM:-1}"
|
|
26
|
+
RUNS="${RUNS:-1}"
|
|
27
|
+
MIN_PASS_RATE="${MIN_PASS_RATE:-100}"
|
|
28
|
+
MAX_FAILURES="${MAX_FAILURES:-0}"
|
|
29
|
+
MAX_MEAN_MS="${MAX_MEAN_MS:-}"
|
|
30
|
+
MAX_P95_MS="${MAX_P95_MS:-}"
|
|
31
|
+
DRY_RUN=0
|
|
32
|
+
|
|
33
|
+
usage() {
|
|
34
|
+
cat <<'USAGE'
|
|
35
|
+
Usage:
|
|
36
|
+
scripts/run-ios-pilot.sh --app-path <Sample.app|Sample.ipa> [options]
|
|
37
|
+
|
|
38
|
+
Runs a configurable iOS smoke pilot:
|
|
39
|
+
1. build/validate zmr
|
|
40
|
+
2. install a simulator .app or signed physical-device .app/.ipa
|
|
41
|
+
3. launch/open a deep link through examples/ios-smoke.json
|
|
42
|
+
4. capture screenshot/log snapshot artifacts
|
|
43
|
+
5. generate report and normal/redacted .zmrtrace bundles
|
|
44
|
+
|
|
45
|
+
Options:
|
|
46
|
+
--app-root <dir> Optional app repo root, used only for output context.
|
|
47
|
+
--app-path <path> Built simulator .app, or signed physical-device .app/.ipa. Required.
|
|
48
|
+
--device <id> Simulator UDID/booted or physical device identifier. Default: booted.
|
|
49
|
+
--ios-device-type <simulator|physical>
|
|
50
|
+
Target type. Default: simulator.
|
|
51
|
+
--app-id <bundle> Bundle id. Default: com.example.mobiletest.
|
|
52
|
+
--trace-root <dir> Output directory. Default: traces/ios-app-pilot-<timestamp>.
|
|
53
|
+
--zmr-bin <path> zmr binary. Default: ZMR_BIN, PATH zmr, then zig-out/bin/zmr.
|
|
54
|
+
--xcrun <path> xcrun path. Default: xcrun.
|
|
55
|
+
--ios-shim <path> Optional XCTest shim command for selector hierarchy/actions.
|
|
56
|
+
--skip-shim-prewarm Do not prewarm the XCTest shim before scenario timing.
|
|
57
|
+
--runs <n> Run each flow n times. n=1 writes trace bundles; n>1 writes benchmark reports.
|
|
58
|
+
--min-pass-rate <pct>
|
|
59
|
+
Repeated-run gate minimum. Default: 100.
|
|
60
|
+
--max-failures <n> Repeated-run gate maximum. Default: 0.
|
|
61
|
+
--max-mean-ms <ms> Optional repeated-run mean duration maximum.
|
|
62
|
+
--max-p95-ms <ms> Optional repeated-run p95 duration maximum.
|
|
63
|
+
--dry-run Print commands without executing them.
|
|
64
|
+
-h, --help Show this help.
|
|
65
|
+
|
|
66
|
+
Notes:
|
|
67
|
+
Without --ios-shim, iOS V1 is a simulator smoke path: install, launch,
|
|
68
|
+
deep link, screenshot, logs, and trace export. With --ios-shim, ZMR also
|
|
69
|
+
uses the shim for hierarchy and selector-grade actions. Physical-device
|
|
70
|
+
runs require --ios-device-type physical, a concrete --device identifier from
|
|
71
|
+
`zmr devices`, a signed app artifact, and an app-local XCTest shim for
|
|
72
|
+
selector-grade flows.
|
|
73
|
+
USAGE
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
die() {
|
|
77
|
+
echo "error: $*" >&2
|
|
78
|
+
exit 2
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
require_value() {
|
|
82
|
+
local flag="$1"
|
|
83
|
+
local value="${2-}"
|
|
84
|
+
if [[ -z "$value" || "$value" == --* ]]; then
|
|
85
|
+
die "$flag requires a value"
|
|
86
|
+
fi
|
|
87
|
+
printf '%s\n' "$value"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
resolve_path_from_cwd() {
|
|
91
|
+
local value="$1"
|
|
92
|
+
local absolute dir base probe suffix
|
|
93
|
+
if [[ -z "$value" ]]; then
|
|
94
|
+
printf '\n'
|
|
95
|
+
return 0
|
|
96
|
+
fi
|
|
97
|
+
if [[ "$value" == /* ]]; then
|
|
98
|
+
absolute="$value"
|
|
99
|
+
else
|
|
100
|
+
absolute="$CALLER_CWD/$value"
|
|
101
|
+
fi
|
|
102
|
+
if [[ -d "$absolute" ]]; then
|
|
103
|
+
cd "$absolute" && pwd -P
|
|
104
|
+
return 0
|
|
105
|
+
fi
|
|
106
|
+
if [[ -e "$absolute" ]]; then
|
|
107
|
+
dir="$(dirname "$absolute")"
|
|
108
|
+
base="$(basename "$absolute")"
|
|
109
|
+
printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$base"
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
dir="$(dirname "$absolute")"
|
|
113
|
+
base="$(basename "$absolute")"
|
|
114
|
+
if [[ -d "$dir" ]]; then
|
|
115
|
+
printf '%s/%s\n' "$(cd "$dir" && pwd -P)" "$base"
|
|
116
|
+
return 0
|
|
117
|
+
fi
|
|
118
|
+
probe="$absolute"
|
|
119
|
+
suffix=""
|
|
120
|
+
while [[ "$probe" != "/" && ! -e "$probe" ]]; do
|
|
121
|
+
suffix="/$(basename "$probe")$suffix"
|
|
122
|
+
probe="$(dirname "$probe")"
|
|
123
|
+
done
|
|
124
|
+
if [[ -d "$probe" ]]; then
|
|
125
|
+
printf '%s%s\n' "$(cd "$probe" && pwd -P)" "$suffix"
|
|
126
|
+
return 0
|
|
127
|
+
fi
|
|
128
|
+
while [[ "$absolute" == *"/./"* ]]; do
|
|
129
|
+
absolute="${absolute//\/.\//\/}"
|
|
130
|
+
done
|
|
131
|
+
printf '%s\n' "$absolute"
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
resolve_command_path_from_cwd() {
|
|
135
|
+
local value="$1"
|
|
136
|
+
if [[ -z "$value" || "$value" != */* ]]; then
|
|
137
|
+
printf '%s\n' "$value"
|
|
138
|
+
else
|
|
139
|
+
resolve_path_from_cwd "$value"
|
|
140
|
+
fi
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
quote_cmd() {
|
|
144
|
+
local quoted=()
|
|
145
|
+
local arg
|
|
146
|
+
for arg in "$@"; do
|
|
147
|
+
quoted+=("$(printf '%q' "$arg")")
|
|
148
|
+
done
|
|
149
|
+
printf '%s\n' "${quoted[*]}"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
run() {
|
|
153
|
+
echo "+ $(quote_cmd "$@")"
|
|
154
|
+
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
155
|
+
"$@"
|
|
156
|
+
fi
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
is_retryable_simctl_text() {
|
|
160
|
+
local text="$1"
|
|
161
|
+
[[ "$text" == *"CoreSimulatorService connection became invalid"* ]] ||
|
|
162
|
+
[[ "$text" == *"Failed to initialize simulator device set"* ]] ||
|
|
163
|
+
[[ "$text" == *"simdiskimaged"* ]] ||
|
|
164
|
+
[[ "$text" == *"Connection refused"* ]]
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
run_ios_install() {
|
|
168
|
+
if [[ "$IOS_DEVICE_TYPE" == "physical" ]]; then
|
|
169
|
+
run "$XCRUN" devicectl device install app --device "$DEVICE" "$APP_PATH"
|
|
170
|
+
return 0
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
echo "+ $(quote_cmd "$XCRUN" simctl install "$DEVICE" "$APP_PATH")"
|
|
174
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
175
|
+
return 0
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
local attempt status err_file err_text
|
|
179
|
+
err_file="$(mktemp)"
|
|
180
|
+
for attempt in 1 2 3 4 5 6; do
|
|
181
|
+
: > "$err_file"
|
|
182
|
+
set +e
|
|
183
|
+
"$XCRUN" simctl install "$DEVICE" "$APP_PATH" 2>"$err_file"
|
|
184
|
+
status=$?
|
|
185
|
+
set -e
|
|
186
|
+
if [[ "$status" -eq 0 ]]; then
|
|
187
|
+
rm -f "$err_file"
|
|
188
|
+
return 0
|
|
189
|
+
fi
|
|
190
|
+
err_text="$(cat "$err_file")"
|
|
191
|
+
if [[ "$attempt" == "6" ]] || ! is_retryable_simctl_text "$err_text"; then
|
|
192
|
+
cat "$err_file" >&2
|
|
193
|
+
rm -f "$err_file"
|
|
194
|
+
return "$status"
|
|
195
|
+
fi
|
|
196
|
+
echo "warning: simctl install hit a transient CoreSimulator error; retrying ($attempt/6)" >&2
|
|
197
|
+
sleep 0.5
|
|
198
|
+
done
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
prewarm_ios_shim() {
|
|
202
|
+
if [[ -z "$IOS_SHIM" || "$PREWARM_IOS_SHIM" -eq 0 ]]; then
|
|
203
|
+
return 0
|
|
204
|
+
fi
|
|
205
|
+
|
|
206
|
+
echo "+ printf '{\"cmd\":\"appState\"}\\n' | $(printf '%q' "$IOS_SHIM")"
|
|
207
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
208
|
+
return 0
|
|
209
|
+
fi
|
|
210
|
+
|
|
211
|
+
if ! printf '{"cmd":"appState"}\n' | "$IOS_SHIM" >/dev/null; then
|
|
212
|
+
echo "error: iOS XCTest shim prewarm failed" >&2
|
|
213
|
+
echo "hint: run the printed appState command directly, inspect .zmr/ios-shim-state/xcodebuild.build.log, and rerun with ZMR_IOS_SHIM_FORCE_REBUILD=1 after changing Xcode targets." >&2
|
|
214
|
+
exit 1
|
|
215
|
+
fi
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
physical_device_state_from_json() {
|
|
219
|
+
local devices_json="$1"
|
|
220
|
+
local serial="$2"
|
|
221
|
+
local state
|
|
222
|
+
for state in connected available disconnected unavailable paired; do
|
|
223
|
+
if [[ "$devices_json" == *'"serial":"'"$serial"'","state":"'"$state"'"'* ]]; then
|
|
224
|
+
printf '%s\n' "$state"
|
|
225
|
+
return 0
|
|
226
|
+
fi
|
|
227
|
+
done
|
|
228
|
+
printf 'unknown\n'
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
preflight_ios_device() {
|
|
232
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
233
|
+
return 0
|
|
234
|
+
fi
|
|
235
|
+
|
|
236
|
+
local devices_json
|
|
237
|
+
devices_json="$("$ZMR_BIN" devices --json --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --xcrun "$XCRUN" 2>/dev/null || true)"
|
|
238
|
+
if [[ "$IOS_DEVICE_TYPE" == "physical" ]]; then
|
|
239
|
+
if [[ "$DEVICE" == "booted" ]]; then
|
|
240
|
+
echo "error: --device must be a physical iOS identifier when --ios-device-type physical is used" >&2
|
|
241
|
+
echo "errorCode: setup.ios.physical_device_required" >&2
|
|
242
|
+
exit 2
|
|
243
|
+
fi
|
|
244
|
+
if [[ "$devices_json" != *'"serial":"'"$DEVICE"'"'* ]]; then
|
|
245
|
+
echo "error: physical iOS device not found: $DEVICE" >&2
|
|
246
|
+
echo "errorCode: setup.ios.physical_device_not_found" >&2
|
|
247
|
+
echo "hint: connect and trust the device, enable Developer Mode, then run zmr devices --json --platform ios --ios-device-type physical --xcrun $(printf '%q' "$XCRUN")." >&2
|
|
248
|
+
exit 2
|
|
249
|
+
fi
|
|
250
|
+
if [[ "$devices_json" != *'"serial":"'"$DEVICE"'","state":"connected"'* && "$devices_json" != *'"serial":"'"$DEVICE"'","state":"available"'* ]]; then
|
|
251
|
+
local device_state
|
|
252
|
+
device_state="$(physical_device_state_from_json "$devices_json" "$DEVICE")"
|
|
253
|
+
echo "error: physical iOS device is not ready: $DEVICE" >&2
|
|
254
|
+
echo "state: $device_state" >&2
|
|
255
|
+
echo "errorCode: setup.ios.physical_device_not_ready" >&2
|
|
256
|
+
echo "hint: connect and trust the device, enable Developer Mode, confirm zmr devices reports state connected or available, then retry." >&2
|
|
257
|
+
exit 2
|
|
258
|
+
fi
|
|
259
|
+
return 0
|
|
260
|
+
fi
|
|
261
|
+
if [[ "$DEVICE" == "booted" ]]; then
|
|
262
|
+
if [[ "$devices_json" == *'"count":0'* || -z "$devices_json" ]]; then
|
|
263
|
+
if simctl_has_booted_device ""; then
|
|
264
|
+
return 0
|
|
265
|
+
fi
|
|
266
|
+
if [[ -z "$devices_json" ]]; then
|
|
267
|
+
echo "warning: could not verify booted iOS simulator during preflight; continuing to simctl install" >&2
|
|
268
|
+
return 0
|
|
269
|
+
fi
|
|
270
|
+
echo "error: no booted iOS simulator found" >&2
|
|
271
|
+
echo "errorCode: setup.ios.no_booted_simulators" >&2
|
|
272
|
+
echo "hint: boot a simulator, then run zmr doctor --json --xcrun $(printf '%q' "$XCRUN")." >&2
|
|
273
|
+
"$ZMR_BIN" doctor --json --xcrun "$XCRUN" >&2 || true
|
|
274
|
+
exit 2
|
|
275
|
+
fi
|
|
276
|
+
return 0
|
|
277
|
+
fi
|
|
278
|
+
|
|
279
|
+
if [[ "$devices_json" != *'"serial":"'"$DEVICE"'"'* ]]; then
|
|
280
|
+
if simctl_has_booted_device "$DEVICE"; then
|
|
281
|
+
return 0
|
|
282
|
+
fi
|
|
283
|
+
if [[ -z "$devices_json" ]]; then
|
|
284
|
+
echo "warning: could not verify iOS simulator during preflight; continuing to simctl install" >&2
|
|
285
|
+
return 0
|
|
286
|
+
fi
|
|
287
|
+
echo "error: iOS simulator not found or not booted: $DEVICE" >&2
|
|
288
|
+
echo "errorCode: setup.ios.no_booted_simulators" >&2
|
|
289
|
+
echo "hint: boot the requested simulator, then run zmr doctor --json --xcrun $(printf '%q' "$XCRUN")." >&2
|
|
290
|
+
"$ZMR_BIN" doctor --json --xcrun "$XCRUN" >&2 || true
|
|
291
|
+
exit 2
|
|
292
|
+
fi
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
simctl_has_booted_device() {
|
|
296
|
+
local wanted="${1:-}"
|
|
297
|
+
local booted_text
|
|
298
|
+
local attempt
|
|
299
|
+
for attempt in 1 2 3 4 5 6; do
|
|
300
|
+
booted_text="$("$XCRUN" simctl list devices booted 2>/dev/null || true)"
|
|
301
|
+
if [[ -n "$booted_text" ]]; then
|
|
302
|
+
if [[ -z "$wanted" && "$booted_text" == *"(Booted)"* ]]; then
|
|
303
|
+
return 0
|
|
304
|
+
fi
|
|
305
|
+
if [[ -n "$wanted" && "$booted_text" == *"$wanted"* && "$booted_text" == *"(Booted)"* ]]; then
|
|
306
|
+
return 0
|
|
307
|
+
fi
|
|
308
|
+
fi
|
|
309
|
+
sleep 0.5
|
|
310
|
+
done
|
|
311
|
+
return 1
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
while [[ $# -gt 0 ]]; do
|
|
315
|
+
case "$1" in
|
|
316
|
+
--app-root)
|
|
317
|
+
APP_ROOT="$(require_value "$1" "${2-}")"
|
|
318
|
+
shift 2
|
|
319
|
+
;;
|
|
320
|
+
--app-path)
|
|
321
|
+
APP_PATH="$(require_value "$1" "${2-}")"
|
|
322
|
+
shift 2
|
|
323
|
+
;;
|
|
324
|
+
--device)
|
|
325
|
+
DEVICE="$(require_value "$1" "${2-}")"
|
|
326
|
+
shift 2
|
|
327
|
+
;;
|
|
328
|
+
--ios-device-type)
|
|
329
|
+
IOS_DEVICE_TYPE="$(require_value "$1" "${2-}")"
|
|
330
|
+
shift 2
|
|
331
|
+
;;
|
|
332
|
+
--app-id)
|
|
333
|
+
APP_ID="$(require_value "$1" "${2-}")"
|
|
334
|
+
shift 2
|
|
335
|
+
;;
|
|
336
|
+
--trace-root)
|
|
337
|
+
TRACE_ROOT="$(require_value "$1" "${2-}")"
|
|
338
|
+
shift 2
|
|
339
|
+
;;
|
|
340
|
+
--zmr-bin)
|
|
341
|
+
ZMR_BIN="$(require_value "$1" "${2-}")"
|
|
342
|
+
shift 2
|
|
343
|
+
;;
|
|
344
|
+
--xcrun)
|
|
345
|
+
XCRUN="$(require_value "$1" "${2-}")"
|
|
346
|
+
shift 2
|
|
347
|
+
;;
|
|
348
|
+
--ios-shim)
|
|
349
|
+
IOS_SHIM="$(require_value "$1" "${2-}")"
|
|
350
|
+
shift 2
|
|
351
|
+
;;
|
|
352
|
+
--skip-shim-prewarm)
|
|
353
|
+
PREWARM_IOS_SHIM=0
|
|
354
|
+
shift
|
|
355
|
+
;;
|
|
356
|
+
--runs)
|
|
357
|
+
RUNS="$(require_value "$1" "${2-}")"
|
|
358
|
+
shift 2
|
|
359
|
+
;;
|
|
360
|
+
--min-pass-rate)
|
|
361
|
+
MIN_PASS_RATE="$(require_value "$1" "${2-}")"
|
|
362
|
+
shift 2
|
|
363
|
+
;;
|
|
364
|
+
--max-failures)
|
|
365
|
+
MAX_FAILURES="$(require_value "$1" "${2-}")"
|
|
366
|
+
shift 2
|
|
367
|
+
;;
|
|
368
|
+
--max-mean-ms)
|
|
369
|
+
MAX_MEAN_MS="$(require_value "$1" "${2-}")"
|
|
370
|
+
shift 2
|
|
371
|
+
;;
|
|
372
|
+
--max-p95-ms)
|
|
373
|
+
MAX_P95_MS="$(require_value "$1" "${2-}")"
|
|
374
|
+
shift 2
|
|
375
|
+
;;
|
|
376
|
+
--dry-run)
|
|
377
|
+
DRY_RUN=1
|
|
378
|
+
shift
|
|
379
|
+
;;
|
|
380
|
+
-h|--help)
|
|
381
|
+
usage
|
|
382
|
+
exit 0
|
|
383
|
+
;;
|
|
384
|
+
*)
|
|
385
|
+
die "unknown argument: $1"
|
|
386
|
+
;;
|
|
387
|
+
esac
|
|
388
|
+
done
|
|
389
|
+
|
|
390
|
+
[[ -n "$APP_PATH" ]] || die "--app-path is required"
|
|
391
|
+
[[ "$RUNS" =~ ^[0-9]+$ && "$RUNS" -ge 1 ]] || die "--runs must be a positive integer"
|
|
392
|
+
[[ "$IOS_DEVICE_TYPE" == "simulator" || "$IOS_DEVICE_TYPE" == "physical" ]] || die "--ios-device-type must be simulator or physical"
|
|
393
|
+
[[ "$MIN_PASS_RATE" =~ ^[0-9]+([.][0-9]+)?$ ]] || die "--min-pass-rate must be a non-negative number"
|
|
394
|
+
[[ "$MAX_FAILURES" =~ ^[0-9]+$ ]] || die "--max-failures must be a non-negative integer"
|
|
395
|
+
[[ -z "$MAX_MEAN_MS" || "$MAX_MEAN_MS" =~ ^[0-9]+$ ]] || die "--max-mean-ms must be a non-negative integer"
|
|
396
|
+
[[ -z "$MAX_P95_MS" || "$MAX_P95_MS" =~ ^[0-9]+$ ]] || die "--max-p95-ms must be a non-negative integer"
|
|
397
|
+
|
|
398
|
+
APP_PATH="$(resolve_path_from_cwd "$APP_PATH")"
|
|
399
|
+
TRACE_ROOT="$(resolve_path_from_cwd "$TRACE_ROOT")"
|
|
400
|
+
ZMR_BIN="$(resolve_command_path_from_cwd "$ZMR_BIN")"
|
|
401
|
+
XCRUN="$(resolve_command_path_from_cwd "$XCRUN")"
|
|
402
|
+
if [[ -n "$APP_ROOT" ]]; then
|
|
403
|
+
APP_ROOT="$(resolve_path_from_cwd "$APP_ROOT")"
|
|
404
|
+
fi
|
|
405
|
+
if [[ -n "$IOS_SHIM" ]]; then
|
|
406
|
+
IOS_SHIM="$(resolve_command_path_from_cwd "$IOS_SHIM")"
|
|
407
|
+
fi
|
|
408
|
+
|
|
409
|
+
if [[ "$DRY_RUN" -eq 0 ]]; then
|
|
410
|
+
if [[ "$IOS_DEVICE_TYPE" == "physical" ]]; then
|
|
411
|
+
[[ -e "$APP_PATH" ]] || die "iOS physical app artifact not found: $APP_PATH"
|
|
412
|
+
else
|
|
413
|
+
if [[ "${APP_PATH##*.}" == "ipa" ]]; then
|
|
414
|
+
die "setup.ios.simulator_app_required: simulator runs require an iphonesimulator .app directory, but got an .ipa. Use a simulator-compatible .app build, or run a device IPA with --ios-device-type physical."
|
|
415
|
+
fi
|
|
416
|
+
[[ -d "$APP_PATH" ]] || die "setup.ios.simulator_app_required: simulator runs require an iphonesimulator .app directory: $APP_PATH"
|
|
417
|
+
fi
|
|
418
|
+
if [[ -n "$APP_ROOT" ]]; then
|
|
419
|
+
[[ -d "$APP_ROOT" ]] || die "app repo not found: $APP_ROOT"
|
|
420
|
+
fi
|
|
421
|
+
fi
|
|
422
|
+
|
|
423
|
+
echo "iOS pilot output: $TRACE_ROOT"
|
|
424
|
+
if [[ -n "$APP_ROOT" ]]; then
|
|
425
|
+
echo "App root: $APP_ROOT"
|
|
426
|
+
fi
|
|
427
|
+
if [[ "$DRY_RUN" -eq 1 ]]; then
|
|
428
|
+
echo "DRY RUN: commands will be printed but not executed"
|
|
429
|
+
fi
|
|
430
|
+
|
|
431
|
+
run mkdir -p "$TRACE_ROOT" "$(dirname "$ZMR_BIN")"
|
|
432
|
+
|
|
433
|
+
if [[ ! -x "$ZMR_BIN" ]]; then
|
|
434
|
+
target_args=()
|
|
435
|
+
if [[ "$(uname -s)" == "Darwin" && "$(uname -m)" == "arm64" ]]; then
|
|
436
|
+
target_args=(-target aarch64-macos.15.0)
|
|
437
|
+
fi
|
|
438
|
+
run zig build-exe src/main.zig "${target_args[@]}" -O Debug -femit-bin="$ZMR_BIN"
|
|
439
|
+
fi
|
|
440
|
+
|
|
441
|
+
run "$ZMR_BIN" version
|
|
442
|
+
run "$ZMR_BIN" validate examples/ios-smoke.json
|
|
443
|
+
if [[ -n "$IOS_SHIM" ]]; then
|
|
444
|
+
run "$ZMR_BIN" validate examples/ios-shim-smoke.json
|
|
445
|
+
fi
|
|
446
|
+
preflight_ios_device
|
|
447
|
+
run_ios_install
|
|
448
|
+
prewarm_ios_shim
|
|
449
|
+
|
|
450
|
+
if [[ "$RUNS" -eq 1 ]]; then
|
|
451
|
+
TRACE_DIR="$TRACE_ROOT/ios-smoke"
|
|
452
|
+
run rm -rf "$TRACE_DIR"
|
|
453
|
+
if [[ -n "$IOS_SHIM" ]]; then
|
|
454
|
+
run "$ZMR_BIN" run examples/ios-smoke.json --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --device "$DEVICE" --app-id "$APP_ID" --xcrun "$XCRUN" --ios-shim "$IOS_SHIM" --trace-dir "$TRACE_DIR"
|
|
455
|
+
else
|
|
456
|
+
run "$ZMR_BIN" run examples/ios-smoke.json --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --device "$DEVICE" --app-id "$APP_ID" --xcrun "$XCRUN" --trace-dir "$TRACE_DIR"
|
|
457
|
+
fi
|
|
458
|
+
run "$ZMR_BIN" report "$TRACE_DIR" --out "$TRACE_DIR/report.html"
|
|
459
|
+
run "$ZMR_BIN" export "$TRACE_DIR" --out "$TRACE_ROOT/ios-smoke.zmrtrace"
|
|
460
|
+
run "$ZMR_BIN" export "$TRACE_DIR" --out "$TRACE_ROOT/ios-smoke-redacted.zmrtrace" --redact
|
|
461
|
+
|
|
462
|
+
if [[ -n "$IOS_SHIM" ]]; then
|
|
463
|
+
SHIM_TRACE_DIR="$TRACE_ROOT/ios-shim-smoke"
|
|
464
|
+
run rm -rf "$SHIM_TRACE_DIR"
|
|
465
|
+
run "$ZMR_BIN" run examples/ios-shim-smoke.json --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --device "$DEVICE" --app-id "$APP_ID" --xcrun "$XCRUN" --ios-shim "$IOS_SHIM" --trace-dir "$SHIM_TRACE_DIR"
|
|
466
|
+
run "$ZMR_BIN" report "$SHIM_TRACE_DIR" --out "$SHIM_TRACE_DIR/report.html"
|
|
467
|
+
run "$ZMR_BIN" export "$SHIM_TRACE_DIR" --out "$TRACE_ROOT/ios-shim-smoke.zmrtrace"
|
|
468
|
+
run "$ZMR_BIN" export "$SHIM_TRACE_DIR" --out "$TRACE_ROOT/ios-shim-smoke-redacted.zmrtrace" --redact
|
|
469
|
+
fi
|
|
470
|
+
else
|
|
471
|
+
benchmark_gate_args=(--min-pass-rate "$MIN_PASS_RATE" --max-failures "$MAX_FAILURES")
|
|
472
|
+
if [[ -n "$MAX_MEAN_MS" ]]; then
|
|
473
|
+
benchmark_gate_args+=(--max-mean-ms "$MAX_MEAN_MS")
|
|
474
|
+
fi
|
|
475
|
+
if [[ -n "$MAX_P95_MS" ]]; then
|
|
476
|
+
benchmark_gate_args+=(--max-p95-ms "$MAX_P95_MS")
|
|
477
|
+
fi
|
|
478
|
+
|
|
479
|
+
if [[ -n "$IOS_SHIM" ]]; then
|
|
480
|
+
ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" --zmr examples/ios-smoke.json --device "$DEVICE" --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --app-id "$APP_ID" --xcrun "$XCRUN" --ios-shim "$IOS_SHIM" --runs "$RUNS" --trace-root "$TRACE_ROOT/ios-smoke-benchmark" "${benchmark_gate_args[@]}"
|
|
481
|
+
else
|
|
482
|
+
ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" --zmr examples/ios-smoke.json --device "$DEVICE" --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --app-id "$APP_ID" --xcrun "$XCRUN" --runs "$RUNS" --trace-root "$TRACE_ROOT/ios-smoke-benchmark" "${benchmark_gate_args[@]}"
|
|
483
|
+
fi
|
|
484
|
+
run "$ZMR_BIN" report "$TRACE_ROOT/ios-smoke-benchmark" --out "$TRACE_ROOT/ios-smoke-benchmark/report.html"
|
|
485
|
+
|
|
486
|
+
if [[ -n "$IOS_SHIM" ]]; then
|
|
487
|
+
ZMR_BIN="$ZMR_BIN" run "$ROOT/scripts/benchmark.sh" --zmr examples/ios-shim-smoke.json --device "$DEVICE" --platform ios --ios-device-type "$IOS_DEVICE_TYPE" --app-id "$APP_ID" --xcrun "$XCRUN" --ios-shim "$IOS_SHIM" --runs "$RUNS" --trace-root "$TRACE_ROOT/ios-shim-smoke-benchmark" "${benchmark_gate_args[@]}"
|
|
488
|
+
run "$ZMR_BIN" report "$TRACE_ROOT/ios-shim-smoke-benchmark" --out "$TRACE_ROOT/ios-shim-smoke-benchmark/report.html"
|
|
489
|
+
fi
|
|
490
|
+
fi
|
|
491
|
+
|
|
492
|
+
echo
|
|
493
|
+
echo "iOS pilot complete."
|
|
494
|
+
echo "Output directory: $TRACE_ROOT"
|
|
495
|
+
if [[ "$RUNS" -eq 1 ]]; then
|
|
496
|
+
echo "Shareable bundle:"
|
|
497
|
+
echo " $TRACE_ROOT/ios-smoke-redacted.zmrtrace"
|
|
498
|
+
if [[ -n "$IOS_SHIM" ]]; then
|
|
499
|
+
echo " $TRACE_ROOT/ios-shim-smoke-redacted.zmrtrace"
|
|
500
|
+
fi
|
|
501
|
+
else
|
|
502
|
+
echo "Benchmark reports:"
|
|
503
|
+
echo " $TRACE_ROOT/ios-smoke-benchmark/report.html"
|
|
504
|
+
if [[ -n "$IOS_SHIM" ]]; then
|
|
505
|
+
echo " $TRACE_ROOT/ios-shim-smoke-benchmark/report.html"
|
|
506
|
+
fi
|
|
507
|
+
fi
|
|
508
|
+
echo "Viewer:"
|
|
509
|
+
echo " $ROOT/viewer/index.html"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# ZMR Android Shim
|
|
2
|
+
|
|
3
|
+
This directory contains the native Android instrumentation shim scaffold that
|
|
4
|
+
supplements the current ADB/UI Automator adapter.
|
|
5
|
+
|
|
6
|
+
Current status:
|
|
7
|
+
|
|
8
|
+
- `src/android.zig` provides the production preview path through ADB, shell
|
|
9
|
+
input, screenshots, logcat, and UI Automator XML.
|
|
10
|
+
- `src/android.zig` can run a configured shim command with one JSON request on
|
|
11
|
+
stdin and one JSON response on stdout.
|
|
12
|
+
- `scripts/install-android-shim.sh` writes an app-local `.zmr/android-shim`
|
|
13
|
+
command and copies the instrumentation source file into the app repo for
|
|
14
|
+
inclusion in `androidTest`.
|
|
15
|
+
|
|
16
|
+
V1 target:
|
|
17
|
+
|
|
18
|
+
- Faster hierarchy retrieval than repeated shell UI Automator dumps.
|
|
19
|
+
- Reliable tap, type, swipe, and key actions with ADB fallback.
|
|
20
|
+
- App idle/settle signals where Android APIs expose them reliably.
|
|
21
|
+
- Clean error envelopes that flow into existing ZMR traces.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
package dev.zmr.shim;
|
|
2
|
+
|
|
3
|
+
import android.graphics.Rect;
|
|
4
|
+
import android.os.Bundle;
|
|
5
|
+
import android.view.KeyEvent;
|
|
6
|
+
|
|
7
|
+
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
|
8
|
+
import androidx.test.platform.app.InstrumentationRegistry;
|
|
9
|
+
import androidx.test.uiautomator.By;
|
|
10
|
+
import androidx.test.uiautomator.UiDevice;
|
|
11
|
+
import androidx.test.uiautomator.UiObject2;
|
|
12
|
+
import androidx.test.uiautomator.Until;
|
|
13
|
+
|
|
14
|
+
import org.json.JSONArray;
|
|
15
|
+
import org.json.JSONObject;
|
|
16
|
+
import org.junit.Test;
|
|
17
|
+
import org.junit.runner.RunWith;
|
|
18
|
+
|
|
19
|
+
import java.io.File;
|
|
20
|
+
import java.nio.charset.StandardCharsets;
|
|
21
|
+
import java.nio.file.Files;
|
|
22
|
+
import java.util.List;
|
|
23
|
+
|
|
24
|
+
@RunWith(AndroidJUnit4.class)
|
|
25
|
+
public final class ZMRShimInstrumentedTest {
|
|
26
|
+
@Test
|
|
27
|
+
public void testRunZMRCommand() throws Exception {
|
|
28
|
+
Bundle args = InstrumentationRegistry.getArguments();
|
|
29
|
+
String requestFile = args.getString("zmrRequestFile");
|
|
30
|
+
String responseFile = args.getString("zmrResponseFile");
|
|
31
|
+
if (requestFile == null || responseFile == null) {
|
|
32
|
+
throw new IllegalArgumentException("zmrRequestFile and zmrResponseFile are required");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
String request = new String(Files.readAllBytes(new File(requestFile).toPath()), StandardCharsets.UTF_8);
|
|
36
|
+
JSONObject command = new JSONObject(request);
|
|
37
|
+
JSONObject response = run(command, UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()));
|
|
38
|
+
Files.write(new File(responseFile).toPath(), response.toString().getBytes(StandardCharsets.UTF_8));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private JSONObject run(JSONObject command, UiDevice device) throws Exception {
|
|
42
|
+
String cmd = command.optString("cmd", "");
|
|
43
|
+
switch (cmd) {
|
|
44
|
+
case "snapshot":
|
|
45
|
+
return ok().put("nodes", snapshot(device));
|
|
46
|
+
case "tap":
|
|
47
|
+
device.click(command.getInt("x"), command.getInt("y"));
|
|
48
|
+
return ok();
|
|
49
|
+
case "type":
|
|
50
|
+
device.executeShellCommand("input text " + escapeInputText(command.optString("text", "")));
|
|
51
|
+
return ok();
|
|
52
|
+
case "eraseText":
|
|
53
|
+
int count = command.optInt("maxChars", 0);
|
|
54
|
+
for (int i = 0; i < count; i += 1) {
|
|
55
|
+
device.pressKeyCode(KeyEvent.KEYCODE_DEL);
|
|
56
|
+
}
|
|
57
|
+
return ok();
|
|
58
|
+
case "hideKeyboard":
|
|
59
|
+
case "pressBack":
|
|
60
|
+
device.pressBack();
|
|
61
|
+
return ok();
|
|
62
|
+
case "swipe":
|
|
63
|
+
device.swipe(
|
|
64
|
+
command.getInt("x1"),
|
|
65
|
+
command.getInt("y1"),
|
|
66
|
+
command.getInt("x2"),
|
|
67
|
+
command.getInt("y2"),
|
|
68
|
+
Math.max(1, command.optInt("durationMs", 250) / 5)
|
|
69
|
+
);
|
|
70
|
+
return ok();
|
|
71
|
+
case "settle":
|
|
72
|
+
device.waitForIdle(command.optLong("durationMs", 1000));
|
|
73
|
+
return ok();
|
|
74
|
+
case "appState":
|
|
75
|
+
return ok().put("state", "ready");
|
|
76
|
+
default:
|
|
77
|
+
return error("unknown.command", "unsupported command: " + cmd);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private JSONArray snapshot(UiDevice device) throws Exception {
|
|
82
|
+
device.waitForIdle();
|
|
83
|
+
List<UiObject2> objects = device.wait(Until.findObjects(By.depth(0)), 1000);
|
|
84
|
+
JSONArray nodes = new JSONArray();
|
|
85
|
+
appendChildren(nodes, device.findObject(By.depth(0)), "root");
|
|
86
|
+
if (nodes.length() == 0 && objects != null) {
|
|
87
|
+
for (int i = 0; i < objects.size(); i += 1) {
|
|
88
|
+
appendNode(nodes, objects.get(i), "node:" + i);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return nodes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
private void appendChildren(JSONArray nodes, UiObject2 object, String prefix) throws Exception {
|
|
95
|
+
if (object == null) return;
|
|
96
|
+
appendNode(nodes, object, prefix);
|
|
97
|
+
List<UiObject2> children = object.getChildren();
|
|
98
|
+
for (int i = 0; i < children.size(); i += 1) {
|
|
99
|
+
appendChildren(nodes, children.get(i), prefix + ":" + i);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private void appendNode(JSONArray nodes, UiObject2 object, String fallbackId) throws Exception {
|
|
104
|
+
Rect bounds = object.getVisibleBounds();
|
|
105
|
+
String resourceName = object.getResourceName();
|
|
106
|
+
String text = object.getText();
|
|
107
|
+
String description = object.getContentDescription();
|
|
108
|
+
String id = resourceName != null && !resourceName.isEmpty() ? "id:" + resourceName : fallbackId;
|
|
109
|
+
|
|
110
|
+
JSONObject node = new JSONObject();
|
|
111
|
+
node.put("id", id);
|
|
112
|
+
node.put("type", object.getClassName());
|
|
113
|
+
node.put("label", text == null ? "" : text);
|
|
114
|
+
node.put("identifier", resourceName == null ? "" : resourceName);
|
|
115
|
+
node.put("contentDescription", description == null ? "" : description);
|
|
116
|
+
node.put("enabled", object.isEnabled());
|
|
117
|
+
node.put("visible", bounds.width() > 0 && bounds.height() > 0);
|
|
118
|
+
node.put("selected", object.isSelected());
|
|
119
|
+
node.put("bounds", new JSONObject()
|
|
120
|
+
.put("x", bounds.left)
|
|
121
|
+
.put("y", bounds.top)
|
|
122
|
+
.put("width", bounds.width())
|
|
123
|
+
.put("height", bounds.height()));
|
|
124
|
+
nodes.put(node);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private JSONObject ok() throws Exception {
|
|
128
|
+
return new JSONObject().put("status", "ok");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private JSONObject error(String code, String message) throws Exception {
|
|
132
|
+
return new JSONObject()
|
|
133
|
+
.put("status", "error")
|
|
134
|
+
.put("code", code)
|
|
135
|
+
.put("message", message);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private String escapeInputText(String value) {
|
|
139
|
+
return value
|
|
140
|
+
.replace("\\", "\\\\")
|
|
141
|
+
.replace(" ", "%s")
|
|
142
|
+
.replace("&", "\\&")
|
|
143
|
+
.replace("<", "\\<")
|
|
144
|
+
.replace(">", "\\>")
|
|
145
|
+
.replace(";", "\\;")
|
|
146
|
+
.replace("|", "\\|")
|
|
147
|
+
.replace("*", "\\*")
|
|
148
|
+
.replace("~", "\\~")
|
|
149
|
+
.replace("\"", "\\\"")
|
|
150
|
+
.replace("'", "\\'");
|
|
151
|
+
}
|
|
152
|
+
}
|