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,338 @@
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
+ ZMR_BIN="${ZMR_BIN:-$(command -v zmr 2>/dev/null || printf '%s' "$ROOT/zig-out/bin/zmr")}"
16
+ MATRIX=""
17
+ TRACE_ROOT="${TRACE_ROOT:-$CALLER_CWD/traces/matrix-$(date +%Y%m%d-%H%M%S)}"
18
+ MIN_PASS_RATE="${MIN_PASS_RATE:-}"
19
+ MAX_FAILURES="${MAX_FAILURES:-}"
20
+
21
+ usage() {
22
+ cat <<'USAGE'
23
+ Usage:
24
+ scripts/device-matrix.sh --matrix <matrix.json> [--trace-root <dir>] [gate options]
25
+
26
+ Omit --trace-root to write under traces/matrix-<timestamp> in the caller directory.
27
+
28
+ Gate options:
29
+ --min-pass-rate <pct> Minimum total pass rate percentage.
30
+ --max-failures <n> Maximum total failed matrix runs.
31
+
32
+ Matrix format:
33
+ {
34
+ "runs": 2,
35
+ "appId": "com.example.mobiletest",
36
+ "devices": [
37
+ {
38
+ "name": "android-api-35",
39
+ "platform": "android",
40
+ "serial": "emulator-5554",
41
+ "scenario": ".zmr/android-smoke.json",
42
+ "adb": "adb",
43
+ "androidShim": ".zmr/android-shim"
44
+ },
45
+ {
46
+ "name": "ios-18",
47
+ "platform": "ios",
48
+ "iosDeviceType": "simulator",
49
+ "serial": "booted",
50
+ "scenario": ".zmr/ios-smoke.json",
51
+ "xcrun": "xcrun",
52
+ "iosShim": ".zmr/ios-shim"
53
+ },
54
+ {
55
+ "name": "ios-physical",
56
+ "platform": "ios",
57
+ "iosDeviceType": "physical",
58
+ "serial": "<physical-device-id>",
59
+ "scenario": ".zmr/ios-smoke.json",
60
+ "xcrun": "xcrun",
61
+ "iosShim": ".zmr/ios-shim"
62
+ }
63
+ ]
64
+ }
65
+ USAGE
66
+ }
67
+
68
+ die() {
69
+ echo "error: $*" >&2
70
+ exit 2
71
+ }
72
+
73
+ require_value() {
74
+ local flag="$1"
75
+ local value="${2-}"
76
+ if [[ -z "$value" || "$value" == --* ]]; then
77
+ die "$flag requires a value"
78
+ fi
79
+ printf '%s\n' "$value"
80
+ }
81
+
82
+ while [[ $# -gt 0 ]]; do
83
+ case "$1" in
84
+ --matrix)
85
+ MATRIX="$(require_value "$1" "${2-}")"
86
+ shift 2
87
+ ;;
88
+ --trace-root)
89
+ TRACE_ROOT="$(require_value "$1" "${2-}")"
90
+ shift 2
91
+ ;;
92
+ --min-pass-rate)
93
+ MIN_PASS_RATE="$(require_value "$1" "${2-}")"
94
+ shift 2
95
+ ;;
96
+ --max-failures)
97
+ MAX_FAILURES="$(require_value "$1" "${2-}")"
98
+ shift 2
99
+ ;;
100
+ -h|--help)
101
+ usage
102
+ exit 0
103
+ ;;
104
+ *)
105
+ die "unknown argument: $1"
106
+ ;;
107
+ esac
108
+ done
109
+
110
+ if [[ -z "$MATRIX" ]]; then
111
+ echo "error: --matrix is required" >&2
112
+ usage >&2
113
+ exit 2
114
+ fi
115
+
116
+ if [[ ! -f "$MATRIX" ]]; then
117
+ die "matrix file not found: $MATRIX"
118
+ fi
119
+
120
+ if [[ ! -x "$ZMR_BIN" ]]; then
121
+ die "zmr binary is not executable: $ZMR_BIN"
122
+ fi
123
+
124
+ validate_optional_number() {
125
+ local name="$1"
126
+ local value="$2"
127
+ if [[ -n "$value" && ! "$value" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
128
+ echo "$name must be a non-negative number" >&2
129
+ exit 2
130
+ fi
131
+ }
132
+
133
+ validate_optional_integer() {
134
+ local name="$1"
135
+ local value="$2"
136
+ if [[ -n "$value" && ! "$value" =~ ^[0-9]+$ ]]; then
137
+ echo "$name must be a non-negative integer" >&2
138
+ exit 2
139
+ fi
140
+ }
141
+
142
+ validate_optional_number "--min-pass-rate" "$MIN_PASS_RATE"
143
+ validate_optional_integer "--max-failures" "$MAX_FAILURES"
144
+
145
+ mkdir -p "$TRACE_ROOT"
146
+ ROWS="$TRACE_ROOT/matrix.rows.tsv"
147
+ RESULTS="$TRACE_ROOT/matrix.jsonl"
148
+ SUMMARY="$TRACE_ROOT/summary.json"
149
+ : > "$RESULTS"
150
+
151
+ python3 - "$MATRIX" > "$ROWS" <<'PY'
152
+ import json
153
+ import sys
154
+
155
+ path = sys.argv[1]
156
+ with open(path, "r", encoding="utf-8") as fh:
157
+ matrix = json.load(fh)
158
+
159
+ runs = int(matrix.get("runs", 1))
160
+ if runs < 1:
161
+ raise SystemExit("matrix.runs must be >= 1")
162
+
163
+ default_app_id = matrix.get("appId", "")
164
+ devices = matrix.get("devices")
165
+ if not isinstance(devices, list) or not devices:
166
+ raise SystemExit("matrix.devices must be a non-empty array")
167
+
168
+ fields = [
169
+ "name",
170
+ "platform",
171
+ "iosDeviceType",
172
+ "serial",
173
+ "scenario",
174
+ "appId",
175
+ "adb",
176
+ "androidShim",
177
+ "xcrun",
178
+ "iosShim",
179
+ ]
180
+
181
+ for index, device in enumerate(devices):
182
+ if not isinstance(device, dict):
183
+ raise SystemExit(f"matrix.devices[{index}] must be an object")
184
+ row = {}
185
+ row["name"] = device.get("name") or device.get("serial") or f"device-{index + 1}"
186
+ row["platform"] = device.get("platform", "android")
187
+ row["iosDeviceType"] = device.get("iosDeviceType", "")
188
+ row["serial"] = device.get("serial", "")
189
+ row["scenario"] = device.get("scenario", "")
190
+ row["appId"] = device.get("appId", default_app_id)
191
+ row["adb"] = device.get("adb", "")
192
+ row["androidShim"] = device.get("androidShim", "")
193
+ row["xcrun"] = device.get("xcrun", "")
194
+ row["iosShim"] = device.get("iosShim", "")
195
+ if row["platform"] not in {"android", "ios"}:
196
+ raise SystemExit(f"matrix.devices[{index}].platform must be android or ios")
197
+ if row["iosDeviceType"] and row["iosDeviceType"] not in {"simulator", "physical"}:
198
+ raise SystemExit(f"matrix.devices[{index}].iosDeviceType must be simulator or physical")
199
+ if not row["serial"]:
200
+ raise SystemExit(f"matrix.devices[{index}].serial is required")
201
+ if not row["scenario"]:
202
+ raise SystemExit(f"matrix.devices[{index}].scenario is required")
203
+ for run in range(1, runs + 1):
204
+ values = [str(run)]
205
+ for field in fields:
206
+ value = str(row[field]).replace("\t", " ")
207
+ values.append(value if value else "__ZMR_EMPTY__")
208
+ print("\t".join(values))
209
+ PY
210
+
211
+ slugify() {
212
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9._-]+/-/g; s/^-+//; s/-+$//'
213
+ }
214
+
215
+ decode_matrix_field() {
216
+ if [[ "$1" == "__ZMR_EMPTY__" ]]; then
217
+ printf ''
218
+ else
219
+ printf '%s' "$1"
220
+ fi
221
+ }
222
+
223
+ while IFS=$'\t' read -r run device_name platform ios_device_type serial scenario app_id adb android_shim xcrun ios_shim; do
224
+ device_name="$(decode_matrix_field "$device_name")"
225
+ platform="$(decode_matrix_field "$platform")"
226
+ ios_device_type="$(decode_matrix_field "$ios_device_type")"
227
+ serial="$(decode_matrix_field "$serial")"
228
+ scenario="$(decode_matrix_field "$scenario")"
229
+ app_id="$(decode_matrix_field "$app_id")"
230
+ adb="$(decode_matrix_field "$adb")"
231
+ android_shim="$(decode_matrix_field "$android_shim")"
232
+ xcrun="$(decode_matrix_field "$xcrun")"
233
+ ios_shim="$(decode_matrix_field "$ios_shim")"
234
+
235
+ safe_name="$(slugify "$device_name")"
236
+ if [[ -z "$safe_name" ]]; then
237
+ safe_name="device"
238
+ fi
239
+ trace_dir="$TRACE_ROOT/$safe_name-run-$run"
240
+ mkdir -p "$trace_dir"
241
+
242
+ zmr_args=(run "$scenario" --platform "$platform" --device "$serial" --trace-dir "$trace_dir")
243
+ if [[ -n "$ios_device_type" ]]; then
244
+ zmr_args+=(--ios-device-type "$ios_device_type")
245
+ fi
246
+ if [[ -n "$app_id" ]]; then
247
+ zmr_args+=(--app-id "$app_id")
248
+ fi
249
+ if [[ -n "$adb" ]]; then
250
+ zmr_args+=(--adb "$adb")
251
+ fi
252
+ if [[ -n "$android_shim" ]]; then
253
+ zmr_args+=(--android-shim "$android_shim")
254
+ fi
255
+ if [[ -n "$xcrun" ]]; then
256
+ zmr_args+=(--xcrun "$xcrun")
257
+ fi
258
+ if [[ -n "$ios_shim" ]]; then
259
+ zmr_args+=(--ios-shim "$ios_shim")
260
+ fi
261
+
262
+ command_status=0
263
+ start_ms="$(python3 -c 'import time; print(int(time.time() * 1000))')"
264
+ "$ZMR_BIN" "${zmr_args[@]}" || command_status=$?
265
+ end_ms="$(python3 -c 'import time; print(int(time.time() * 1000))')"
266
+ duration_ms=$((end_ms - start_ms))
267
+
268
+ row="$("$ROOT/scripts/benchmark_result_row.py" \
269
+ --tool zmr \
270
+ --run "$run" \
271
+ --command-status "$command_status" \
272
+ --duration-ms "$duration_ms" \
273
+ --trace-dir "$trace_dir")"
274
+ python3 - "$row" "$device_name" "$platform" "$serial" "$scenario" <<'PY' >> "$RESULTS"
275
+ import json
276
+ import sys
277
+
278
+ row = json.loads(sys.argv[1])
279
+ row["deviceName"] = sys.argv[2]
280
+ row["platform"] = sys.argv[3]
281
+ row["serial"] = sys.argv[4]
282
+ row["scenario"] = sys.argv[5]
283
+ print(json.dumps(row, separators=(",", ":")))
284
+ PY
285
+ done < "$ROWS"
286
+
287
+ python3 - "$RESULTS" "$SUMMARY" <<'PY'
288
+ import json
289
+ import statistics
290
+ import sys
291
+
292
+ results_path = sys.argv[1]
293
+ summary_path = sys.argv[2]
294
+ rows = []
295
+ with open(results_path, "r", encoding="utf-8") as fh:
296
+ rows = [json.loads(line) for line in fh if line.strip()]
297
+
298
+ total = len(rows)
299
+ failed = sum(1 for row in rows if row.get("status") != "ok" or row.get("traceStatus") == "failed")
300
+ passed = total - failed
301
+ durations = [int(row.get("durationMs", 0)) for row in rows]
302
+ pass_rate = (passed / total * 100.0) if total else 0.0
303
+ summary = {
304
+ "totalRuns": total,
305
+ "passed": passed,
306
+ "failed": failed,
307
+ "passRate": round(pass_rate, 2),
308
+ "meanMs": round(statistics.mean(durations), 2) if durations else 0,
309
+ "resultsPath": "matrix.jsonl",
310
+ }
311
+ with open(summary_path, "w", encoding="utf-8") as fh:
312
+ json.dump(summary, fh, separators=(",", ":"))
313
+ fh.write("\n")
314
+ print(f"matrix: runs={total} passRate={pass_rate:.2f}% failures={failed}")
315
+ PY
316
+
317
+ gate_failed=0
318
+ python3 - "$SUMMARY" "$MIN_PASS_RATE" "$MAX_FAILURES" <<'PY' || gate_failed=$?
319
+ import json
320
+ import sys
321
+
322
+ summary_path, min_pass_rate, max_failures = sys.argv[1:4]
323
+ with open(summary_path, "r", encoding="utf-8") as fh:
324
+ summary = json.load(fh)
325
+
326
+ failed = False
327
+ if min_pass_rate and summary["passRate"] < float(min_pass_rate):
328
+ print(f"matrix gate failed: passRate={summary['passRate']:.2f}% < {float(min_pass_rate):.2f}%")
329
+ failed = True
330
+ if max_failures and summary["failed"] > int(max_failures):
331
+ print(f"matrix gate failed: failures={summary['failed']} > {int(max_failures)}")
332
+ failed = True
333
+ raise SystemExit(1 if failed else 0)
334
+ PY
335
+
336
+ if [[ "$gate_failed" -ne 0 ]]; then
337
+ exit "$gate_failed"
338
+ fi
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'cgi'
5
+ require 'fileutils'
6
+ require 'optparse'
7
+ require 'pathname'
8
+
9
+ options = {
10
+ sources: [],
11
+ deployment_target: '15.0',
12
+ }
13
+
14
+ OptionParser.new do |parser|
15
+ parser.banner = 'Usage: ensure-ios-shim-target.rb --project ios/App.xcodeproj --app-target App --test-target AppZMRUITests --scheme AppZMRUITests --bundle-id com.example.app [options]'
16
+ parser.on('--project PATH', 'Xcode project path') { |value| options[:project] = value }
17
+ parser.on('--app-target NAME', 'App target under test') { |value| options[:app_target] = value }
18
+ parser.on('--test-target NAME', 'UI test target to create/update') { |value| options[:test_target] = value }
19
+ parser.on('--scheme NAME', 'Shared UI test scheme to create/update') { |value| options[:scheme] = value }
20
+ parser.on('--bundle-id ID', 'App bundle id under test') { |value| options[:bundle_id] = value }
21
+ parser.on('--test-bundle-id ID', 'UI test bundle id') { |value| options[:test_bundle_id] = value }
22
+ parser.on('--deployment-target VERSION', 'iOS deployment target') { |value| options[:deployment_target] = value }
23
+ parser.on('--source PATH', 'Shim source path relative to app root; repeatable') { |value| options[:sources] << value }
24
+ parser.on('--info-plist PATH', 'Info.plist path relative to app root') { |value| options[:info_plist] = value }
25
+ parser.on('-h', '--help', 'Show help') do
26
+ puts parser
27
+ exit 0
28
+ end
29
+ end.parse!
30
+
31
+ required = %i[project app_target test_target scheme bundle_id test_bundle_id info_plist]
32
+ missing = required.select { |key| options[key].to_s.empty? }
33
+ missing << :source if options[:sources].empty?
34
+ unless missing.empty?
35
+ abort "error: missing required option(s): #{missing.map { |key| "--#{key.to_s.tr('_', '-')}" }.join(', ')}"
36
+ end
37
+
38
+ begin
39
+ require 'xcodeproj'
40
+ rescue LoadError
41
+ abort 'error: missing Ruby gem xcodeproj. Install with `gem install xcodeproj` or add it to the app Gemfile.'
42
+ end
43
+
44
+ def app_root
45
+ Pathname.new(Dir.pwd)
46
+ end
47
+
48
+ def project_dir(project)
49
+ Pathname.new(File.dirname(File.expand_path(project.path)))
50
+ end
51
+
52
+ def source_root_relative(project, relative_path)
53
+ absolute = app_root.join(relative_path).expand_path
54
+ absolute.relative_path_from(project_dir(project)).to_s
55
+ end
56
+
57
+ def ensure_file_reference(project, relative_path)
58
+ xcode_path = source_root_relative(project, relative_path)
59
+ existing = project.files.find { |file| file.path == xcode_path || file.path == relative_path }
60
+ if existing
61
+ existing.path = xcode_path
62
+ existing.source_tree = 'SOURCE_ROOT'
63
+ return existing
64
+ end
65
+
66
+ file_ref = project.main_group.new_file(xcode_path)
67
+ file_ref.source_tree = 'SOURCE_ROOT'
68
+ file_ref
69
+ end
70
+
71
+ def ensure_source(target, file_ref)
72
+ return if target.source_build_phase.files_references.include?(file_ref)
73
+
74
+ target.source_build_phase.add_file_reference(file_ref, true)
75
+ end
76
+
77
+ def ensure_dependency(target, dependency)
78
+ return if target.dependencies.any? { |candidate| candidate.target == dependency }
79
+
80
+ target.add_dependency(dependency)
81
+ end
82
+
83
+ def product_name(target)
84
+ target.product_reference&.display_name || "#{target.name}.app"
85
+ end
86
+
87
+ def buildable_reference(target, container)
88
+ <<~XML
89
+ <BuildableReference
90
+ BuildableIdentifier = "primary"
91
+ BlueprintIdentifier = "#{CGI.escapeHTML(target.uuid)}"
92
+ BuildableName = "#{CGI.escapeHTML(product_name(target))}"
93
+ BlueprintName = "#{CGI.escapeHTML(target.name)}"
94
+ ReferencedContainer = "#{CGI.escapeHTML(container)}">
95
+ </BuildableReference>
96
+ XML
97
+ end
98
+
99
+ def write_scheme(project, app_target, test_target, scheme_name)
100
+ scheme_dir = File.join(project.path, 'xcshareddata/xcschemes')
101
+ FileUtils.mkdir_p(scheme_dir)
102
+ scheme_path = File.join(scheme_dir, "#{scheme_name}.xcscheme")
103
+ container = "container:#{File.basename(project.path)}"
104
+ app_ref = buildable_reference(app_target, container)
105
+ test_ref = buildable_reference(test_target, container)
106
+
107
+ xml = <<~XML
108
+ <?xml version="1.0" encoding="UTF-8"?>
109
+ <Scheme
110
+ LastUpgradeVersion = "1600"
111
+ version = "1.7">
112
+ <BuildAction
113
+ parallelizeBuildables = "YES"
114
+ buildImplicitDependencies = "YES">
115
+ <BuildActionEntries>
116
+ <BuildActionEntry
117
+ buildForTesting = "YES"
118
+ buildForRunning = "YES"
119
+ buildForProfiling = "YES"
120
+ buildForArchiving = "YES"
121
+ buildForAnalyzing = "YES">
122
+ #{app_ref.rstrip}
123
+ </BuildActionEntry>
124
+ <BuildActionEntry
125
+ buildForTesting = "YES"
126
+ buildForRunning = "NO"
127
+ buildForProfiling = "NO"
128
+ buildForArchiving = "NO"
129
+ buildForAnalyzing = "YES">
130
+ #{test_ref.rstrip}
131
+ </BuildActionEntry>
132
+ </BuildActionEntries>
133
+ </BuildAction>
134
+ <TestAction
135
+ buildConfiguration = "Debug"
136
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
137
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
138
+ shouldUseLaunchSchemeArgsEnv = "YES">
139
+ <Testables>
140
+ <TestableReference skipped = "NO">
141
+ #{test_ref.rstrip}
142
+ <SelectedTests>
143
+ <Test Identifier = "ZMRShimUITestCase/testRunZMRCommand">
144
+ </Test>
145
+ </SelectedTests>
146
+ </TestableReference>
147
+ </Testables>
148
+ <MacroExpansion>
149
+ #{app_ref.rstrip}
150
+ </MacroExpansion>
151
+ <EnvironmentVariables>
152
+ <EnvironmentVariable key = "ZMR_SHIM_REQUEST_FILE" value = "$(ZMR_SHIM_REQUEST_FILE)" isEnabled = "YES">
153
+ </EnvironmentVariable>
154
+ <EnvironmentVariable key = "ZMR_SHIM_RESPONSE_FILE" value = "$(ZMR_SHIM_RESPONSE_FILE)" isEnabled = "YES">
155
+ </EnvironmentVariable>
156
+ <EnvironmentVariable key = "ZMR_SHIM_MODE" value = "$(ZMR_SHIM_MODE)" isEnabled = "YES">
157
+ </EnvironmentVariable>
158
+ <EnvironmentVariable key = "ZMR_SHIM_SERVER_DIR" value = "$(ZMR_SHIM_SERVER_DIR)" isEnabled = "YES">
159
+ </EnvironmentVariable>
160
+ <EnvironmentVariable key = "ZMR_APP_BUNDLE_ID" value = "$(ZMR_APP_BUNDLE_ID)" isEnabled = "YES">
161
+ </EnvironmentVariable>
162
+ </EnvironmentVariables>
163
+ </TestAction>
164
+ <LaunchAction
165
+ buildConfiguration = "Debug"
166
+ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
167
+ selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
168
+ launchStyle = "0"
169
+ useCustomWorkingDirectory = "NO"
170
+ ignoresPersistentStateOnLaunch = "NO"
171
+ debugDocumentVersioning = "YES"
172
+ debugServiceExtension = "internal"
173
+ allowLocationSimulation = "YES">
174
+ <BuildableProductRunnable runnableDebuggingMode = "0">
175
+ #{app_ref.rstrip}
176
+ </BuildableProductRunnable>
177
+ </LaunchAction>
178
+ <ProfileAction
179
+ buildConfiguration = "Release"
180
+ shouldUseLaunchSchemeArgsEnv = "YES"
181
+ savedToolIdentifier = ""
182
+ useCustomWorkingDirectory = "NO"
183
+ debugDocumentVersioning = "YES">
184
+ <BuildableProductRunnable runnableDebuggingMode = "0">
185
+ #{app_ref.rstrip}
186
+ </BuildableProductRunnable>
187
+ </ProfileAction>
188
+ <AnalyzeAction buildConfiguration = "Debug">
189
+ </AnalyzeAction>
190
+ <ArchiveAction
191
+ buildConfiguration = "Release"
192
+ revealArchiveInOrganizer = "YES">
193
+ </ArchiveAction>
194
+ </Scheme>
195
+ XML
196
+
197
+ File.write(scheme_path, xml)
198
+ end
199
+
200
+ project_path = File.expand_path(options[:project], Dir.pwd)
201
+ project = Xcodeproj::Project.open(project_path)
202
+ app_target = project.targets.find { |target| target.name == options[:app_target] }
203
+ abort "error: missing app target #{options[:app_target]}" unless app_target
204
+
205
+ test_target = project.targets.find { |target| target.name == options[:test_target] }
206
+ test_target ||= project.new_target(:ui_test_bundle, options[:test_target], :ios, options[:deployment_target])
207
+
208
+ ensure_dependency(test_target, app_target)
209
+
210
+ options[:sources].each do |source_path|
211
+ abort "error: missing #{source_path}; run install-ios-shim first" unless File.exist?(app_root.join(source_path))
212
+
213
+ ensure_source(test_target, ensure_file_reference(project, source_path))
214
+ end
215
+
216
+ info_plist = source_root_relative(project, options[:info_plist])
217
+ team = app_target.build_configurations.map { |config| config.build_settings['DEVELOPMENT_TEAM'] }.find { |value| !value.to_s.empty? }
218
+
219
+ test_target.build_configurations.each do |configuration|
220
+ settings = configuration.build_settings
221
+ settings['CODE_SIGN_STYLE'] = 'Automatic'
222
+ settings['DEVELOPMENT_TEAM'] = team if team
223
+ settings['GENERATE_INFOPLIST_FILE'] = 'NO'
224
+ settings['INFOPLIST_FILE'] = info_plist
225
+ settings['IPHONEOS_DEPLOYMENT_TARGET'] = options[:deployment_target]
226
+ settings['PRODUCT_BUNDLE_IDENTIFIER'] = options[:test_bundle_id]
227
+ settings['PRODUCT_MODULE_NAME'] = '$(TARGET_NAME)'
228
+ settings['PRODUCT_NAME'] = '$(TARGET_NAME)'
229
+ settings['SWIFT_VERSION'] = '5.0'
230
+ settings['TARGETED_DEVICE_FAMILY'] = '1,2'
231
+ settings['TEST_TARGET_NAME'] = options[:app_target]
232
+ end
233
+
234
+ project.save
235
+ write_scheme(project, app_target, test_target, options[:scheme])
236
+
237
+ puts "ensured #{options[:test_target]} and #{options[:scheme]}.xcscheme"