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,126 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ AVD="${AVD:-}"
5
+ DEVICE="${DEVICE:-emulator-5554}"
6
+ EMULATOR="${EMULATOR:-emulator}"
7
+ ADB="${ADB:-adb}"
8
+ SNAPSHOT="${SNAPSHOT:-zmr-clean}"
9
+ DRY_RUN=0
10
+
11
+ usage() {
12
+ cat <<'USAGE'
13
+ Usage:
14
+ scripts/android-emulator.sh boot --avd <name> [--device emulator-5554] [--dry-run]
15
+ scripts/android-emulator.sh wait-ready [--device emulator-5554] [--dry-run]
16
+ scripts/android-emulator.sh snapshot-save [--device emulator-5554] [--name zmr-clean] [--dry-run]
17
+ scripts/android-emulator.sh snapshot-load --avd <name> [--name zmr-clean] [--dry-run]
18
+ scripts/android-emulator.sh kill [--device emulator-5554] [--dry-run]
19
+
20
+ Environment:
21
+ EMULATOR emulator binary. Defaults to emulator.
22
+ ADB adb binary. Defaults to adb.
23
+ AVD default AVD name.
24
+ DEVICE default adb serial.
25
+ SNAPSHOT default snapshot name.
26
+ USAGE
27
+ }
28
+
29
+ quote_cmd() {
30
+ local quoted=()
31
+ local arg
32
+ for arg in "$@"; do
33
+ quoted+=("$(printf '%q' "$arg")")
34
+ done
35
+ printf '%s\n' "${quoted[*]}"
36
+ }
37
+
38
+ run() {
39
+ echo "+ $(quote_cmd "$@")"
40
+ if [[ "$DRY_RUN" -eq 0 ]]; then
41
+ "$@"
42
+ fi
43
+ }
44
+
45
+ die() {
46
+ echo "error: $*" >&2
47
+ exit 2
48
+ }
49
+
50
+ require_value() {
51
+ local flag="$1"
52
+ local value="${2-}"
53
+ if [[ -z "$value" || "$value" == --* ]]; then
54
+ die "$flag requires a value"
55
+ fi
56
+ printf '%s\n' "$value"
57
+ }
58
+
59
+ [[ $# -gt 0 ]] || {
60
+ usage >&2
61
+ exit 2
62
+ }
63
+
64
+ COMMAND="$1"
65
+ shift
66
+
67
+ while [[ $# -gt 0 ]]; do
68
+ case "$1" in
69
+ --avd)
70
+ AVD="$(require_value "$1" "${2-}")"
71
+ shift 2
72
+ ;;
73
+ --device)
74
+ DEVICE="$(require_value "$1" "${2-}")"
75
+ shift 2
76
+ ;;
77
+ --name)
78
+ SNAPSHOT="$(require_value "$1" "${2-}")"
79
+ shift 2
80
+ ;;
81
+ --dry-run)
82
+ DRY_RUN=1
83
+ shift
84
+ ;;
85
+ -h|--help)
86
+ usage
87
+ exit 0
88
+ ;;
89
+ *)
90
+ die "unknown argument: $1"
91
+ ;;
92
+ esac
93
+ done
94
+
95
+ case "$COMMAND" in
96
+ boot)
97
+ [[ -n "$AVD" ]] || die "--avd or AVD is required"
98
+ run "$EMULATOR" -avd "$AVD" -no-snapshot-load -netdelay none -netspeed full
99
+ ;;
100
+ wait-ready)
101
+ run "$ADB" -s "$DEVICE" wait-for-device
102
+ if [[ "$DRY_RUN" -eq 1 ]]; then
103
+ echo "+ wait until sys.boot_completed is 1"
104
+ exit 0
105
+ fi
106
+ for _ in $(seq 1 120); do
107
+ value="$("$ADB" -s "$DEVICE" shell getprop sys.boot_completed 2>/dev/null | tr -d '\r')"
108
+ [[ "$value" == "1" ]] && exit 0
109
+ sleep 2
110
+ done
111
+ die "emulator did not finish booting"
112
+ ;;
113
+ snapshot-save)
114
+ run "$ADB" -s "$DEVICE" emu avd snapshot save "$SNAPSHOT"
115
+ ;;
116
+ snapshot-load)
117
+ [[ -n "$AVD" ]] || die "--avd or AVD is required"
118
+ run "$EMULATOR" -avd "$AVD" -snapshot "$SNAPSHOT" -netdelay none -netspeed full
119
+ ;;
120
+ kill)
121
+ run "$ADB" -s "$DEVICE" emu kill
122
+ ;;
123
+ *)
124
+ die "unknown command: $COMMAND"
125
+ ;;
126
+ esac
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SOURCE="${BASH_SOURCE[0]}"
5
+ while [[ -h "$SOURCE" ]]; do
6
+ SOURCE_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
7
+ SOURCE="$(readlink "$SOURCE")"
8
+ if [[ "$SOURCE" != /* ]]; then
9
+ SOURCE="$SOURCE_DIR/$SOURCE"
10
+ fi
11
+ done
12
+
13
+ ROOT="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
14
+ cd "$ROOT"
15
+
16
+ if [[ -n "${ZMR_BIN:-}" ]]; then
17
+ ZMR="$ZMR_BIN"
18
+ elif [[ -x "$ROOT/zig-out/bin/zmr" ]]; then
19
+ ZMR="$ROOT/zig-out/bin/zmr"
20
+ else
21
+ ZMR="zmr"
22
+ fi
23
+ DEVICE=""
24
+ XCRUN=""
25
+ XCRUN_PROVIDED=0
26
+ EVIDENCE_OUT=""
27
+ ATTEMPTS="${ZMR_IOS_READY_ATTEMPTS:-3}"
28
+ RETRY_DELAY_SECONDS="${ZMR_IOS_READY_RETRY_DELAY_SECONDS:-1}"
29
+ START_MS="$(python3 - <<'PY'
30
+ import time
31
+ print(int(time.time() * 1000))
32
+ PY
33
+ )"
34
+
35
+ usage() {
36
+ cat <<'USAGE'
37
+ Usage:
38
+ scripts/assert-ios-physical-ready.sh [--zmr <path>] [--xcrun <path>] [--device <identifier>] [--evidence-out <path>]
39
+
40
+ Fails unless zmr reports at least one ready physical iOS device. When --device
41
+ is supplied, that exact CoreDevice identifier from `zmr devices --json
42
+ --platform ios --ios-device-type physical` must be present and ready.
43
+
44
+ When --evidence-out is supplied, a successful check appends a JSONL row that
45
+ can be consumed by zmr-release-readiness.
46
+ USAGE
47
+ }
48
+
49
+ die() {
50
+ echo "error: $*" >&2
51
+ exit 2
52
+ }
53
+
54
+ require_value() {
55
+ local flag="$1"
56
+ local value="${2-}"
57
+ if [[ -z "$value" || "$value" == --* ]]; then
58
+ die "$flag requires a value"
59
+ fi
60
+ printf '%s\n' "$value"
61
+ }
62
+
63
+ append_evidence() {
64
+ [[ -n "$EVIDENCE_OUT" ]] || return 0
65
+
66
+ local end_ms duration_ms command
67
+ end_ms="$(python3 - <<'PY'
68
+ import time
69
+ print(int(time.time() * 1000))
70
+ PY
71
+ )"
72
+ duration_ms="$((end_ms - START_MS))"
73
+ if [[ -n "$DEVICE" ]]; then
74
+ command="scripts/assert-ios-physical-ready.sh --device $DEVICE"
75
+ else
76
+ command="scripts/assert-ios-physical-ready.sh"
77
+ fi
78
+ if [[ -n "$XCRUN" ]]; then
79
+ command="$command --xcrun $XCRUN"
80
+ fi
81
+
82
+ mkdir -p "$(dirname "$EVIDENCE_OUT")"
83
+ python3 - "$EVIDENCE_OUT" "physical iOS readiness" "ios-physical-ready" "passed" "$duration_ms" "$command" "$DEVICE" <<'PY'
84
+ import json
85
+ import sys
86
+
87
+ path, name, mode, status, duration_ms, command, device_id = sys.argv[1:]
88
+ row = {
89
+ "name": name,
90
+ "mode": mode,
91
+ "status": status,
92
+ "durationMs": int(duration_ms),
93
+ "command": command,
94
+ }
95
+ if device_id:
96
+ row["deviceId"] = device_id
97
+ with open(path, "a", encoding="utf-8") as fh:
98
+ fh.write(json.dumps(row, separators=(",", ":")) + "\n")
99
+ PY
100
+ }
101
+
102
+ while [[ $# -gt 0 ]]; do
103
+ case "$1" in
104
+ --zmr)
105
+ ZMR="$(require_value "$1" "${2-}")"
106
+ shift 2
107
+ ;;
108
+ --xcrun)
109
+ XCRUN="$(require_value "$1" "${2-}")"
110
+ XCRUN_PROVIDED=1
111
+ shift 2
112
+ ;;
113
+ --device)
114
+ DEVICE="$(require_value "$1" "${2-}")"
115
+ shift 2
116
+ ;;
117
+ --evidence-out)
118
+ EVIDENCE_OUT="$(require_value "$1" "${2-}")"
119
+ shift 2
120
+ ;;
121
+ -h|--help)
122
+ usage
123
+ exit 0
124
+ ;;
125
+ *)
126
+ die "unknown argument: $1"
127
+ ;;
128
+ esac
129
+ done
130
+
131
+ [[ -n "$ZMR" ]] || die "--zmr must be non-empty"
132
+ if [[ "$XCRUN_PROVIDED" -eq 1 && -z "$XCRUN" ]]; then
133
+ die "--xcrun must be non-empty"
134
+ fi
135
+ [[ "$ATTEMPTS" =~ ^[0-9]+$ && "$ATTEMPTS" -ge 1 ]] || die "ZMR_IOS_READY_ATTEMPTS must be a positive integer"
136
+ [[ "$RETRY_DELAY_SECONDS" =~ ^[0-9]+$ ]] || die "ZMR_IOS_READY_RETRY_DELAY_SECONDS must be a non-negative integer"
137
+
138
+ devices_json=""
139
+ last_error=""
140
+ attempt=1
141
+ zmr_devices_args=(devices --json --platform ios --ios-device-type physical)
142
+ if [[ -n "$XCRUN" ]]; then
143
+ zmr_devices_args+=(--xcrun "$XCRUN")
144
+ fi
145
+ while [[ "$attempt" -le "$ATTEMPTS" ]]; do
146
+ error_file="$(mktemp)"
147
+ if devices_json="$("$ZMR" "${zmr_devices_args[@]}" 2>"$error_file")"; then
148
+ rm -f "$error_file"
149
+ break
150
+ fi
151
+ last_error="$(cat "$error_file")"
152
+ rm -f "$error_file"
153
+ if [[ "$attempt" -lt "$ATTEMPTS" ]]; then
154
+ sleep "$RETRY_DELAY_SECONDS"
155
+ fi
156
+ attempt="$((attempt + 1))"
157
+ done
158
+
159
+ if [[ -z "$devices_json" ]]; then
160
+ if [[ -n "$last_error" ]]; then
161
+ printf '%s\n' "$last_error" >&2
162
+ fi
163
+ echo "error[setup.ios.physical_devices_unavailable]: unable to list physical iOS devices after $ATTEMPTS attempt(s)" >&2
164
+ exit 3
165
+ fi
166
+
167
+ if ZMR_DEVICES_JSON="$devices_json" python3 - "$DEVICE" <<'PY'
168
+ import json
169
+ import os
170
+ import sys
171
+
172
+ target = sys.argv[1] or None
173
+
174
+ try:
175
+ data = json.loads(os.environ["ZMR_DEVICES_JSON"])
176
+ except Exception as exc:
177
+ print(f"error[setup.ios.devices_json_invalid]: failed to parse zmr devices JSON: {exc}", file=sys.stderr)
178
+ sys.exit(3)
179
+
180
+ devices = data.get("devices")
181
+ if not isinstance(devices, list):
182
+ print("error[setup.ios.devices_json_invalid]: zmr devices JSON is missing devices[]", file=sys.stderr)
183
+ sys.exit(3)
184
+
185
+ if target:
186
+ for device in devices:
187
+ if device.get("serial") == target:
188
+ if device.get("ready") is True:
189
+ print(f"physical iOS device ready: {target}")
190
+ sys.exit(0)
191
+ state = device.get("state") or "unknown"
192
+ print(
193
+ f"error[setup.ios.physical_device_not_ready]: physical iOS device is not ready: {target} (state: {state})",
194
+ file=sys.stderr,
195
+ )
196
+ sys.exit(3)
197
+ print(f"error[setup.ios.physical_device_not_found]: physical iOS device was not found: {target}", file=sys.stderr)
198
+ sys.exit(3)
199
+
200
+ for device in devices:
201
+ if device.get("ready") is True:
202
+ print(f"physical iOS device ready: {device.get('serial', '<unknown>')}")
203
+ sys.exit(0)
204
+
205
+ states = ", ".join(f"{d.get('serial', '<unknown>')}:{d.get('state', 'unknown')}" for d in devices) or "none"
206
+ print(f"error[setup.ios.no_ready_physical_devices]: no ready physical iOS devices found ({states})", file=sys.stderr)
207
+ sys.exit(3)
208
+ PY
209
+ then
210
+ append_evidence
211
+ else
212
+ exit $?
213
+ fi
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ SOURCE="${BASH_SOURCE[0]}"
5
+ while [[ -h "$SOURCE" ]]; do
6
+ SOURCE_DIR="$(cd -P "$(dirname "$SOURCE")" && pwd)"
7
+ SOURCE="$(readlink "$SOURCE")"
8
+ if [[ "$SOURCE" != /* ]]; then
9
+ SOURCE="$SOURCE_DIR/$SOURCE"
10
+ fi
11
+ done
12
+
13
+ ROOT="$(cd -P "$(dirname "$SOURCE")/.." && pwd)"
14
+ CALLER_CWD="$(pwd -P)"
15
+
16
+ # Some sandboxed environments do not allow writing to the default temp directory
17
+ # (/var/folders, /tmp). Use a repo-local TMPDIR so adb/xcrun/mktemp/heredocs work.
18
+ if [[ -z "${TMPDIR:-}" || ! -w "${TMPDIR:-/nonexistent}" ]]; then
19
+ TMPDIR="$ROOT/traces/tmp"
20
+ mkdir -p "$TMPDIR"
21
+ export TMPDIR
22
+ fi
23
+
24
+ TOOL="${TOOL:-baseline}"
25
+ RUNS="${RUNS:-5}"
26
+ TRACE_ROOT="${TRACE_ROOT:-$CALLER_CWD/traces/bench-command-$(date +%Y%m%d-%H%M%S)}"
27
+ RESULTS=""
28
+ CWD=""
29
+ REPLACE=0
30
+ PLATFORM="${PLATFORM:-}"
31
+ DEVICE="${DEVICE:-}"
32
+ APP_ID="${APP_ID:-}"
33
+ SCENARIO="${SCENARIO:-}"
34
+ APP_BUILD="${APP_BUILD:-}"
35
+ MIN_PASS_RATE="${MIN_PASS_RATE:-}"
36
+ MAX_FAILURES="${MAX_FAILURES:-}"
37
+ MAX_MEAN_MS="${MAX_MEAN_MS:-}"
38
+ MAX_P95_MS="${MAX_P95_MS:-}"
39
+
40
+ usage() {
41
+ cat <<'USAGE'
42
+ Usage:
43
+ scripts/benchmark-command.sh --tool <label> [options] -- <command> [args...]
44
+
45
+ Runs any local command repeatedly and appends normalized benchmark rows that can
46
+ be compared with ZMR rows through zmr-compare-benchmarks.
47
+
48
+ Options:
49
+ --tool <label> Baseline tool label, for example runner-a or runner-b.
50
+ --runs <n> Number of command runs. Default: 5.
51
+ --trace-root <dir> Directory for stdout/stderr logs. Default: traces/bench-command-<timestamp> in the caller directory.
52
+ --results <path> Results JSONL path. Defaults to <trace-root>/results.jsonl.
53
+ Explicit results paths are appended by default.
54
+ --replace Truncate --results before writing.
55
+ --cwd <dir> Run the command from this working directory.
56
+ --platform <name> Platform context, for example android or ios.
57
+ --device <id> Device context shared with candidate rows.
58
+ --app-id <id> App id/bundle id context shared with candidate rows.
59
+ --scenario <path> Scenario or flow identifier used by this command.
60
+ --app-build <id> App build fingerprint, artifact path, or CI build id.
61
+ --min-pass-rate <pct> Optional gate minimum.
62
+ --max-failures <n> Optional gate maximum.
63
+ --max-mean-ms <ms> Optional mean duration maximum.
64
+ --max-p95-ms <ms> Optional p95 duration maximum.
65
+ -h, --help Show this help.
66
+
67
+ Example:
68
+ zmr-benchmark-command \
69
+ --tool runner-a \
70
+ --runs 20 \
71
+ --trace-root traces/runner-a-login \
72
+ --results traces/comparison/results.jsonl \
73
+ -- runner-a test .runner-a/login.yaml
74
+ USAGE
75
+ }
76
+
77
+ die() {
78
+ echo "error: $*" >&2
79
+ exit 2
80
+ }
81
+
82
+ require_value() {
83
+ local flag="$1"
84
+ local value="${2-}"
85
+ if [[ -z "$value" || "$value" == --* ]]; then
86
+ die "$flag requires a value"
87
+ fi
88
+ printf '%s\n' "$value"
89
+ }
90
+
91
+ quote_cmd() {
92
+ local quoted=()
93
+ local arg
94
+ for arg in "$@"; do
95
+ quoted+=("$(printf '%q' "$arg")")
96
+ done
97
+ printf '%s\n' "${quoted[*]}"
98
+ }
99
+
100
+ RESULTS_EXPLICIT=0
101
+ while [[ $# -gt 0 ]]; do
102
+ case "$1" in
103
+ --tool)
104
+ TOOL="$(require_value "$1" "${2-}")"
105
+ shift 2
106
+ ;;
107
+ --runs)
108
+ RUNS="$(require_value "$1" "${2-}")"
109
+ shift 2
110
+ ;;
111
+ --trace-root)
112
+ TRACE_ROOT="$(require_value "$1" "${2-}")"
113
+ shift 2
114
+ ;;
115
+ --results)
116
+ RESULTS="$(require_value "$1" "${2-}")"
117
+ RESULTS_EXPLICIT=1
118
+ shift 2
119
+ ;;
120
+ --replace)
121
+ REPLACE=1
122
+ shift
123
+ ;;
124
+ --cwd)
125
+ CWD="$(require_value "$1" "${2-}")"
126
+ shift 2
127
+ ;;
128
+ --platform)
129
+ PLATFORM="$(require_value "$1" "${2-}")"
130
+ shift 2
131
+ ;;
132
+ --device)
133
+ DEVICE="$(require_value "$1" "${2-}")"
134
+ shift 2
135
+ ;;
136
+ --app-id)
137
+ APP_ID="$(require_value "$1" "${2-}")"
138
+ shift 2
139
+ ;;
140
+ --scenario)
141
+ SCENARIO="$(require_value "$1" "${2-}")"
142
+ shift 2
143
+ ;;
144
+ --app-build)
145
+ APP_BUILD="$(require_value "$1" "${2-}")"
146
+ shift 2
147
+ ;;
148
+ --min-pass-rate)
149
+ MIN_PASS_RATE="$(require_value "$1" "${2-}")"
150
+ shift 2
151
+ ;;
152
+ --max-failures)
153
+ MAX_FAILURES="$(require_value "$1" "${2-}")"
154
+ shift 2
155
+ ;;
156
+ --max-mean-ms)
157
+ MAX_MEAN_MS="$(require_value "$1" "${2-}")"
158
+ shift 2
159
+ ;;
160
+ --max-p95-ms)
161
+ MAX_P95_MS="$(require_value "$1" "${2-}")"
162
+ shift 2
163
+ ;;
164
+ --)
165
+ shift
166
+ break
167
+ ;;
168
+ -h|--help)
169
+ usage
170
+ exit 0
171
+ ;;
172
+ *)
173
+ die "unknown argument before --: $1"
174
+ ;;
175
+ esac
176
+ done
177
+
178
+ [[ -n "$TOOL" ]] || die "--tool cannot be empty"
179
+ [[ "$RUNS" =~ ^[0-9]+$ && "$RUNS" -ge 1 ]] || die "--runs must be a positive integer"
180
+ [[ $# -gt 0 ]] || die "command is required after --"
181
+ if [[ -n "$CWD" && ! -d "$CWD" ]]; then
182
+ die "--cwd directory not found: $CWD"
183
+ fi
184
+
185
+ validate_optional_number() {
186
+ local name="$1"
187
+ local value="$2"
188
+ if [[ -n "$value" && ! "$value" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
189
+ die "$name must be a non-negative number"
190
+ fi
191
+ }
192
+
193
+ validate_optional_integer() {
194
+ local name="$1"
195
+ local value="$2"
196
+ if [[ -n "$value" && ! "$value" =~ ^[0-9]+$ ]]; then
197
+ die "$name must be a non-negative integer"
198
+ fi
199
+ }
200
+
201
+ validate_optional_number "--min-pass-rate" "$MIN_PASS_RATE"
202
+ validate_optional_integer "--max-failures" "$MAX_FAILURES"
203
+ validate_optional_integer "--max-mean-ms" "$MAX_MEAN_MS"
204
+ validate_optional_integer "--max-p95-ms" "$MAX_P95_MS"
205
+
206
+ mkdir -p "$TRACE_ROOT"
207
+ if [[ -z "$RESULTS" ]]; then
208
+ RESULTS="$TRACE_ROOT/results.jsonl"
209
+ fi
210
+ mkdir -p "$(dirname "$RESULTS")"
211
+ if [[ "$REPLACE" -eq 1 || "$RESULTS_EXPLICIT" -eq 0 ]]; then
212
+ : > "$RESULTS"
213
+ else
214
+ touch "$RESULTS"
215
+ fi
216
+
217
+ COMMAND=("$@")
218
+ metadata_args=()
219
+ if [[ -n "$PLATFORM" ]]; then
220
+ metadata_args+=(--platform "$PLATFORM")
221
+ fi
222
+ if [[ -n "$DEVICE" ]]; then
223
+ metadata_args+=(--device "$DEVICE")
224
+ fi
225
+ if [[ -n "$APP_ID" ]]; then
226
+ metadata_args+=(--app-id "$APP_ID")
227
+ fi
228
+ if [[ -n "$SCENARIO" ]]; then
229
+ metadata_args+=(--scenario "$SCENARIO")
230
+ fi
231
+ if [[ -n "$APP_BUILD" ]]; then
232
+ metadata_args+=(--app-build "$APP_BUILD")
233
+ fi
234
+ echo "Benchmark command output: $TRACE_ROOT"
235
+ echo "Results: $RESULTS"
236
+ echo "Tool: $TOOL"
237
+ echo "+ $(quote_cmd "${COMMAND[@]}")"
238
+
239
+ for run in $(seq 1 "$RUNS"); do
240
+ run_dir="$TRACE_ROOT/$TOOL-$run"
241
+ mkdir -p "$run_dir"
242
+ printf '%s\n' "$(quote_cmd "${COMMAND[@]}")" > "$run_dir/command.txt"
243
+
244
+ command_status=0
245
+ start_ms="$(python3 -c 'import time; print(int(time.time() * 1000))')"
246
+ if [[ -n "$CWD" ]]; then
247
+ (cd "$CWD" && "${COMMAND[@]}") > "$run_dir/stdout.log" 2> "$run_dir/stderr.log" || command_status=$?
248
+ else
249
+ "${COMMAND[@]}" > "$run_dir/stdout.log" 2> "$run_dir/stderr.log" || command_status=$?
250
+ fi
251
+ end_ms="$(python3 -c 'import time; print(int(time.time() * 1000))')"
252
+ duration_ms=$((end_ms - start_ms))
253
+
254
+ if [[ "${#metadata_args[@]}" -gt 0 ]]; then
255
+ "$ROOT/scripts/benchmark_result_row.py" \
256
+ --tool "$TOOL" \
257
+ --run "$run" \
258
+ --command-status "$command_status" \
259
+ --duration-ms "$duration_ms" \
260
+ --trace-dir "$run_dir" \
261
+ "${metadata_args[@]}" >> "$RESULTS"
262
+ else
263
+ "$ROOT/scripts/benchmark_result_row.py" \
264
+ --tool "$TOOL" \
265
+ --run "$run" \
266
+ --command-status "$command_status" \
267
+ --duration-ms "$duration_ms" \
268
+ --trace-dir "$run_dir" >> "$RESULTS"
269
+ fi
270
+ done
271
+
272
+ python3 - "$RESULTS" "$TOOL" <<'PY'
273
+ import json
274
+ import math
275
+ import statistics
276
+ import sys
277
+
278
+ path, tool = sys.argv[1], sys.argv[2]
279
+ rows = [
280
+ json.loads(line)
281
+ for line in open(path, encoding="utf-8")
282
+ if line.strip() and json.loads(line).get("tool") == tool
283
+ ]
284
+ durations = [int(row.get("durationMs", 0)) for row in rows]
285
+ failures = sum(1 for row in rows if row.get("status") != "ok")
286
+ mean = round(statistics.mean(durations)) if durations else 0
287
+ p95 = sorted(durations)[max(0, math.ceil(len(durations) * 0.95) - 1)] if durations else 0
288
+ print(f"{tool}: runs={len(rows)} failures={failures} meanMs={mean} p95Ms={p95}")
289
+ PY
290
+
291
+ gate_args=()
292
+ if [[ -n "$MIN_PASS_RATE" ]]; then
293
+ gate_args+=(--min-pass-rate "$MIN_PASS_RATE")
294
+ fi
295
+ if [[ -n "$MAX_FAILURES" ]]; then
296
+ gate_args+=(--max-failures "$MAX_FAILURES")
297
+ fi
298
+ if [[ -n "$MAX_MEAN_MS" ]]; then
299
+ gate_args+=(--max-mean-ms "$MAX_MEAN_MS")
300
+ fi
301
+ if [[ -n "$MAX_P95_MS" ]]; then
302
+ gate_args+=(--max-p95-ms "$MAX_P95_MS")
303
+ fi
304
+
305
+ if [[ "${#gate_args[@]}" -gt 0 ]]; then
306
+ "$ROOT/scripts/benchmark_gate.py" --results "$RESULTS" "${gate_args[@]}"
307
+ fi