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.
Files changed (225) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +112 -0
  4. package/LICENSE +21 -0
  5. package/README.md +255 -0
  6. package/SECURITY.md +34 -0
  7. package/build.zig +38 -0
  8. package/build.zig.zon +7 -0
  9. package/clients/README.md +144 -0
  10. package/clients/go/README.md +24 -0
  11. package/clients/go/examples/fake-session/main.go +93 -0
  12. package/clients/go/go.mod +3 -0
  13. package/clients/go/zmr/client.go +432 -0
  14. package/clients/kotlin/README.md +35 -0
  15. package/clients/kotlin/build.gradle.kts +35 -0
  16. package/clients/kotlin/settings.gradle.kts +15 -0
  17. package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
  18. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
  19. package/clients/python/README.md +29 -0
  20. package/clients/python/examples/fake_session.py +48 -0
  21. package/clients/python/pyproject.toml +13 -0
  22. package/clients/python/zmr_client.py +202 -0
  23. package/clients/rust/Cargo.lock +107 -0
  24. package/clients/rust/Cargo.toml +10 -0
  25. package/clients/rust/README.md +19 -0
  26. package/clients/rust/examples/fake_session.rs +70 -0
  27. package/clients/rust/src/lib.rs +461 -0
  28. package/clients/swift/Package.swift +16 -0
  29. package/clients/swift/README.md +36 -0
  30. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
  31. package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
  32. package/clients/typescript/README.md +34 -0
  33. package/clients/typescript/examples/fake-session.mjs +36 -0
  34. package/clients/typescript/index.d.ts +144 -0
  35. package/clients/typescript/index.mjs +192 -0
  36. package/clients/typescript/package.json +8 -0
  37. package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
  38. package/docs/adr/0002-app-local-zmr-contract.md +39 -0
  39. package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
  40. package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
  41. package/docs/adr/README.md +12 -0
  42. package/docs/ai-agents.md +156 -0
  43. package/docs/app-integration.md +316 -0
  44. package/docs/benchmarking.md +275 -0
  45. package/docs/client-installation.md +141 -0
  46. package/docs/clients.md +98 -0
  47. package/docs/config.md +175 -0
  48. package/docs/demo.md +259 -0
  49. package/docs/dsl.md +57 -0
  50. package/docs/install.md +233 -0
  51. package/docs/market-positioning.md +70 -0
  52. package/docs/npm.md +359 -0
  53. package/docs/protocol-fixtures/README.md +8 -0
  54. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  55. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  56. package/docs/protocol-versioning.md +65 -0
  57. package/docs/protocol.md +560 -0
  58. package/docs/publication.md +77 -0
  59. package/docs/release-audit.md +99 -0
  60. package/docs/release-candidate.md +111 -0
  61. package/docs/release-evidence.md +188 -0
  62. package/docs/release-notes-template.md +58 -0
  63. package/docs/roadmap.md +334 -0
  64. package/docs/scenario-authoring.md +88 -0
  65. package/docs/shipping.md +170 -0
  66. package/docs/trace-privacy.md +88 -0
  67. package/docs/troubleshooting.md +256 -0
  68. package/examples/android-app-auth-probe.json +89 -0
  69. package/examples/android-app-error-state.json +13 -0
  70. package/examples/android-app-login-smoke.json +192 -0
  71. package/examples/android-app-onboarding.json +12 -0
  72. package/examples/android-app-referral-deep-link.json +12 -0
  73. package/examples/android-shim-smoke.json +19 -0
  74. package/examples/demo-failure.json +12 -0
  75. package/examples/demo-fake.json +14 -0
  76. package/examples/ios-dev-client-open-link.json +26 -0
  77. package/examples/ios-dev-client-route-snapshot.json +24 -0
  78. package/examples/ios-shim-smoke.json +23 -0
  79. package/examples/ios-smoke.json +9 -0
  80. package/go.work +3 -0
  81. package/npm/agents.mjs +183 -0
  82. package/npm/app-config.mjs +95 -0
  83. package/npm/build-zmr.mjs +21 -0
  84. package/npm/commands.mjs +104 -0
  85. package/npm/generated-files.mjs +50 -0
  86. package/npm/index.mjs +75 -0
  87. package/npm/init-app.mjs +80 -0
  88. package/npm/package-scripts.mjs +72 -0
  89. package/npm/postinstall.mjs +21 -0
  90. package/npm/scaffold.mjs +179 -0
  91. package/npm/scenarios.mjs +93 -0
  92. package/npm/setup.mjs +69 -0
  93. package/npm/wizard.mjs +117 -0
  94. package/npm/zmr.mjs +23 -0
  95. package/package.json +114 -0
  96. package/prebuilds/darwin-arm64/zmr +0 -0
  97. package/prebuilds/darwin-x64/zmr +0 -0
  98. package/prebuilds/linux-arm64/zmr +0 -0
  99. package/prebuilds/linux-x64/zmr +0 -0
  100. package/schemas/README.md +26 -0
  101. package/schemas/action-result.schema.json +27 -0
  102. package/schemas/capabilities-output.schema.json +98 -0
  103. package/schemas/devices-output.schema.json +25 -0
  104. package/schemas/doctor-output.schema.json +51 -0
  105. package/schemas/explain-output.schema.json +51 -0
  106. package/schemas/import-output.schema.json +23 -0
  107. package/schemas/init-output.schema.json +71 -0
  108. package/schemas/json-rpc.schema.json +55 -0
  109. package/schemas/release-manifest.schema.json +43 -0
  110. package/schemas/release-readiness-output.schema.json +127 -0
  111. package/schemas/run-output.schema.json +43 -0
  112. package/schemas/scenario.schema.json +128 -0
  113. package/schemas/schemas-output.schema.json +26 -0
  114. package/schemas/semantic-snapshot.schema.json +116 -0
  115. package/schemas/snapshot.schema.json +60 -0
  116. package/schemas/trace-event.schema.json +14 -0
  117. package/schemas/trace-manifest.schema.json +59 -0
  118. package/schemas/validate-output.schema.json +42 -0
  119. package/schemas/version-output.schema.json +23 -0
  120. package/schemas/zmr-config.schema.json +75 -0
  121. package/scripts/android-emulator.sh +126 -0
  122. package/scripts/assert-ios-physical-ready.sh +213 -0
  123. package/scripts/benchmark-command.sh +307 -0
  124. package/scripts/benchmark.sh +359 -0
  125. package/scripts/benchmark_gate.py +117 -0
  126. package/scripts/benchmark_result_row.py +88 -0
  127. package/scripts/compare-benchmarks.py +288 -0
  128. package/scripts/create-android-demo-app.sh +342 -0
  129. package/scripts/create-ios-demo-app.sh +261 -0
  130. package/scripts/demo-android-real.sh +232 -0
  131. package/scripts/demo-ios-real.sh +270 -0
  132. package/scripts/demo.sh +464 -0
  133. package/scripts/device-matrix.sh +338 -0
  134. package/scripts/ensure-ios-shim-target.rb +237 -0
  135. package/scripts/install-android-shim.sh +281 -0
  136. package/scripts/install-ios-shim.sh +589 -0
  137. package/scripts/pilot-gate.sh +560 -0
  138. package/scripts/release-readiness.py +838 -0
  139. package/scripts/release-readiness.sh +91 -0
  140. package/scripts/run-android-pilot.sh +561 -0
  141. package/scripts/run-ios-pilot.sh +509 -0
  142. package/shims/android/README.md +21 -0
  143. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  144. package/shims/android/protocol.md +18 -0
  145. package/shims/ios/README.md +50 -0
  146. package/shims/ios/ZMRShim.swift +110 -0
  147. package/shims/ios/ZMRShimUITestCase.swift +475 -0
  148. package/shims/ios/protocol.md +74 -0
  149. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  150. package/src/android.zig +344 -0
  151. package/src/android_device_info.zig +99 -0
  152. package/src/android_emulator.zig +154 -0
  153. package/src/android_screen_recording.zig +112 -0
  154. package/src/android_shell.zig +112 -0
  155. package/src/bundle.zig +124 -0
  156. package/src/bundle_redaction.zig +272 -0
  157. package/src/bundle_tar.zig +123 -0
  158. package/src/cli_devices.zig +97 -0
  159. package/src/cli_doctor.zig +114 -0
  160. package/src/cli_import.zig +70 -0
  161. package/src/cli_info.zig +39 -0
  162. package/src/cli_init.zig +72 -0
  163. package/src/cli_output.zig +467 -0
  164. package/src/cli_run.zig +259 -0
  165. package/src/cli_serve.zig +287 -0
  166. package/src/cli_trace.zig +111 -0
  167. package/src/cli_validate.zig +41 -0
  168. package/src/command.zig +211 -0
  169. package/src/config.zig +305 -0
  170. package/src/config_diagnostics.zig +212 -0
  171. package/src/config_paths.zig +49 -0
  172. package/src/device_registry.zig +37 -0
  173. package/src/doctor.zig +412 -0
  174. package/src/doctor_hints.zig +52 -0
  175. package/src/errors.zig +55 -0
  176. package/src/fake_device.zig +163 -0
  177. package/src/health.zig +28 -0
  178. package/src/importer.zig +343 -0
  179. package/src/importer_json.zig +100 -0
  180. package/src/importer_model.zig +103 -0
  181. package/src/ios.zig +399 -0
  182. package/src/ios_devices.zig +219 -0
  183. package/src/ios_lifecycle.zig +72 -0
  184. package/src/ios_shim.zig +242 -0
  185. package/src/ios_snapshot.zig +20 -0
  186. package/src/json_fields.zig +80 -0
  187. package/src/json_rpc.zig +150 -0
  188. package/src/json_rpc_methods.zig +318 -0
  189. package/src/json_rpc_observation.zig +31 -0
  190. package/src/json_rpc_params.zig +52 -0
  191. package/src/json_rpc_protocol.zig +110 -0
  192. package/src/json_rpc_trace.zig +73 -0
  193. package/src/main.zig +135 -0
  194. package/src/mcp.zig +234 -0
  195. package/src/mcp_protocol.zig +64 -0
  196. package/src/mcp_trace.zig +83 -0
  197. package/src/report.zig +346 -0
  198. package/src/report_html.zig +63 -0
  199. package/src/report_values.zig +27 -0
  200. package/src/run_options.zig +152 -0
  201. package/src/runner.zig +280 -0
  202. package/src/runner_actions.zig +109 -0
  203. package/src/runner_config.zig +6 -0
  204. package/src/runner_diagnostics.zig +268 -0
  205. package/src/runner_events.zig +170 -0
  206. package/src/runner_native.zig +88 -0
  207. package/src/runner_waits.zig +300 -0
  208. package/src/scaffold.zig +472 -0
  209. package/src/scenario.zig +346 -0
  210. package/src/scenario_fields.zig +50 -0
  211. package/src/schema_registry.zig +53 -0
  212. package/src/selector.zig +84 -0
  213. package/src/semantic.zig +171 -0
  214. package/src/trace.zig +315 -0
  215. package/src/trace_json.zig +340 -0
  216. package/src/trace_summary.zig +218 -0
  217. package/src/trace_summary_diagnostic.zig +202 -0
  218. package/src/types.zig +120 -0
  219. package/src/uiautomator.zig +164 -0
  220. package/src/validation.zig +187 -0
  221. package/src/version.zig +22 -0
  222. package/viewer/app.js +373 -0
  223. package/viewer/index.html +126 -0
  224. package/viewer/parser.js +233 -0
  225. 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
+ }