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,359 @@
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
+ ZMR_BIN="${ZMR_BIN:-$(command -v zmr 2>/dev/null || printf '%s' "$ROOT/zig-out/bin/zmr")}"
25
+ RUNS="${RUNS:-5}"
26
+ DEVICE="${DEVICE:-}"
27
+ TRACE_ROOT="${TRACE_ROOT:-$CALLER_CWD/traces/bench-$(date +%Y%m%d-%H%M%S)}"
28
+ RESULTS=""
29
+ RESULTS_EXPLICIT=0
30
+ REPLACE=0
31
+ ZMR_SCENARIO=""
32
+ PLATFORM="${PLATFORM:-}"
33
+ APP_ID="${APP_ID:-}"
34
+ ADB="${ADB:-}"
35
+ ANDROID_SHIM="${ANDROID_SHIM:-}"
36
+ XCRUN="${XCRUN:-}"
37
+ IOS_SHIM="${IOS_SHIM:-}"
38
+ IOS_DEVICE_TYPE="${IOS_DEVICE_TYPE:-}"
39
+ APP_BUILD="${APP_BUILD:-}"
40
+ MIN_PASS_RATE="${MIN_PASS_RATE:-}"
41
+ MAX_FAILURES="${MAX_FAILURES:-}"
42
+ MAX_MEAN_MS="${MAX_MEAN_MS:-}"
43
+ MAX_P95_MS="${MAX_P95_MS:-}"
44
+
45
+ usage() {
46
+ cat <<'USAGE'
47
+ Usage:
48
+ scripts/benchmark.sh --zmr <scenario.json> --device <serial> [--runs 10] [--trace-root <dir>] [--results <path>] [gate options]
49
+
50
+ Gate options:
51
+ --min-pass-rate <pct> Minimum pass rate percentage, for example 100.
52
+ --max-failures <n> Maximum allowed failed runs.
53
+ --max-mean-ms <ms> Maximum allowed mean run duration.
54
+ --max-p95-ms <ms> Maximum allowed p95 run duration.
55
+
56
+ Output options:
57
+ --results <path> Results JSONL path. Defaults to <trace-root>/results.jsonl.
58
+ Explicit results paths are appended by default.
59
+ --replace Truncate --results before writing.
60
+
61
+ Forwarded ZMR options:
62
+ --platform <android|ios>
63
+ --app-id <id>
64
+ --adb <path>
65
+ --android-shim <path>
66
+ --xcrun <path>
67
+ --ios-shim <path>
68
+ --ios-device-type <simulator|physical>
69
+ --app-build <id> App build fingerprint, artifact path, or CI build id for comparison context.
70
+
71
+ Environment:
72
+ ZMR_BIN Path to zmr binary. Defaults to ./zig-out/bin/zmr.
73
+ RUNS Default run count when --runs is omitted.
74
+ DEVICE Default Android serial when --device is omitted.
75
+ TRACE_ROOT Default benchmark output root. Otherwise traces/bench-<timestamp> in the caller directory.
76
+ PLATFORM, APP_ID, ADB, ANDROID_SHIM, XCRUN, IOS_SHIM, IOS_DEVICE_TYPE, APP_BUILD
77
+ Default forwarded ZMR options when matching flags are omitted.
78
+ MIN_PASS_RATE, MAX_FAILURES, MAX_MEAN_MS, MAX_P95_MS
79
+ Default gate thresholds when matching flags are omitted.
80
+ USAGE
81
+ }
82
+
83
+ die() {
84
+ echo "error: $*" >&2
85
+ exit 2
86
+ }
87
+
88
+ require_value() {
89
+ local flag="$1"
90
+ local value="${2-}"
91
+ if [[ -z "$value" || "$value" == --* ]]; then
92
+ die "$flag requires a value"
93
+ fi
94
+ printf '%s\n' "$value"
95
+ }
96
+
97
+ while [[ $# -gt 0 ]]; do
98
+ case "$1" in
99
+ --zmr)
100
+ ZMR_SCENARIO="$(require_value "$1" "${2-}")"
101
+ shift 2
102
+ ;;
103
+ --device)
104
+ DEVICE="$(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
+ --platform)
125
+ PLATFORM="$(require_value "$1" "${2-}")"
126
+ shift 2
127
+ ;;
128
+ --app-id)
129
+ APP_ID="$(require_value "$1" "${2-}")"
130
+ shift 2
131
+ ;;
132
+ --adb)
133
+ ADB="$(require_value "$1" "${2-}")"
134
+ shift 2
135
+ ;;
136
+ --android-shim)
137
+ ANDROID_SHIM="$(require_value "$1" "${2-}")"
138
+ shift 2
139
+ ;;
140
+ --xcrun)
141
+ XCRUN="$(require_value "$1" "${2-}")"
142
+ shift 2
143
+ ;;
144
+ --ios-shim)
145
+ IOS_SHIM="$(require_value "$1" "${2-}")"
146
+ shift 2
147
+ ;;
148
+ --ios-device-type)
149
+ IOS_DEVICE_TYPE="$(require_value "$1" "${2-}")"
150
+ shift 2
151
+ ;;
152
+ --app-build)
153
+ APP_BUILD="$(require_value "$1" "${2-}")"
154
+ shift 2
155
+ ;;
156
+ --min-pass-rate)
157
+ MIN_PASS_RATE="$(require_value "$1" "${2-}")"
158
+ shift 2
159
+ ;;
160
+ --max-failures)
161
+ MAX_FAILURES="$(require_value "$1" "${2-}")"
162
+ shift 2
163
+ ;;
164
+ --max-mean-ms)
165
+ MAX_MEAN_MS="$(require_value "$1" "${2-}")"
166
+ shift 2
167
+ ;;
168
+ --max-p95-ms)
169
+ MAX_P95_MS="$(require_value "$1" "${2-}")"
170
+ shift 2
171
+ ;;
172
+ -h|--help)
173
+ usage
174
+ exit 0
175
+ ;;
176
+ *)
177
+ die "unknown argument: $1"
178
+ ;;
179
+ esac
180
+ done
181
+
182
+ if [[ -z "$ZMR_SCENARIO" ]]; then
183
+ echo "error: --zmr is required" >&2
184
+ usage >&2
185
+ exit 2
186
+ fi
187
+
188
+ if [[ -z "$DEVICE" ]]; then
189
+ echo "error: --device or DEVICE is required" >&2
190
+ usage >&2
191
+ exit 2
192
+ fi
193
+
194
+ if [[ ! "$RUNS" =~ ^[0-9]+$ || "$RUNS" -lt 1 ]]; then
195
+ die "--runs must be a positive integer"
196
+ fi
197
+
198
+ if [[ ! -x "$ZMR_BIN" ]]; then
199
+ die "zmr binary is not executable: $ZMR_BIN"
200
+ fi
201
+
202
+ validate_optional_number() {
203
+ local name="$1"
204
+ local value="$2"
205
+ if [[ -n "$value" && ! "$value" =~ ^[0-9]+([.][0-9]+)?$ ]]; then
206
+ echo "$name must be a non-negative number" >&2
207
+ exit 2
208
+ fi
209
+ }
210
+
211
+ validate_optional_integer() {
212
+ local name="$1"
213
+ local value="$2"
214
+ if [[ -n "$value" && ! "$value" =~ ^[0-9]+$ ]]; then
215
+ echo "$name must be a non-negative integer" >&2
216
+ exit 2
217
+ fi
218
+ }
219
+
220
+ validate_optional_number "--min-pass-rate" "$MIN_PASS_RATE"
221
+ validate_optional_integer "--max-failures" "$MAX_FAILURES"
222
+ validate_optional_integer "--max-mean-ms" "$MAX_MEAN_MS"
223
+ validate_optional_integer "--max-p95-ms" "$MAX_P95_MS"
224
+ if [[ -n "$IOS_DEVICE_TYPE" && "$IOS_DEVICE_TYPE" != "simulator" && "$IOS_DEVICE_TYPE" != "physical" ]]; then
225
+ echo "--ios-device-type must be simulator or physical" >&2
226
+ exit 2
227
+ fi
228
+
229
+ mkdir -p "$TRACE_ROOT"
230
+ if [[ -z "$RESULTS" ]]; then
231
+ RESULTS="$TRACE_ROOT/results.jsonl"
232
+ fi
233
+ mkdir -p "$(dirname "$RESULTS")"
234
+ if [[ "$REPLACE" -eq 1 || "$RESULTS_EXPLICIT" -eq 0 ]]; then
235
+ : > "$RESULTS"
236
+ else
237
+ touch "$RESULTS"
238
+ fi
239
+
240
+ run_one() {
241
+ local tool="$1"
242
+ local run="$2"
243
+ local command_status=0
244
+ local start_ms end_ms duration_ms trace_dir
245
+ local -a zmr_args=()
246
+ local -a metadata_args=()
247
+
248
+ trace_dir="$TRACE_ROOT/$tool-$run"
249
+ mkdir -p "$trace_dir"
250
+ if [[ -n "$PLATFORM" ]]; then
251
+ zmr_args+=(--platform "$PLATFORM")
252
+ fi
253
+ if [[ -n "$APP_ID" ]]; then
254
+ zmr_args+=(--app-id "$APP_ID")
255
+ fi
256
+ if [[ -n "$ADB" ]]; then
257
+ zmr_args+=(--adb "$ADB")
258
+ fi
259
+ if [[ -n "$ANDROID_SHIM" ]]; then
260
+ zmr_args+=(--android-shim "$ANDROID_SHIM")
261
+ fi
262
+ if [[ -n "$XCRUN" ]]; then
263
+ zmr_args+=(--xcrun "$XCRUN")
264
+ fi
265
+ if [[ -n "$IOS_SHIM" ]]; then
266
+ zmr_args+=(--ios-shim "$IOS_SHIM")
267
+ fi
268
+ if [[ -n "$IOS_DEVICE_TYPE" ]]; then
269
+ zmr_args+=(--ios-device-type "$IOS_DEVICE_TYPE")
270
+ fi
271
+ if [[ -n "$PLATFORM" ]]; then
272
+ metadata_args+=(--platform "$PLATFORM")
273
+ fi
274
+ if [[ -n "$DEVICE" ]]; then
275
+ metadata_args+=(--device "$DEVICE")
276
+ fi
277
+ if [[ -n "$APP_ID" ]]; then
278
+ metadata_args+=(--app-id "$APP_ID")
279
+ fi
280
+ if [[ -n "$ZMR_SCENARIO" ]]; then
281
+ metadata_args+=(--scenario "$ZMR_SCENARIO")
282
+ fi
283
+ if [[ -n "$APP_BUILD" ]]; then
284
+ metadata_args+=(--app-build "$APP_BUILD")
285
+ fi
286
+ start_ms="$(python3 -c 'import time; print(int(time.time() * 1000))')"
287
+ if [[ "${#zmr_args[@]}" -gt 0 ]]; then
288
+ "$ZMR_BIN" run "$ZMR_SCENARIO" --device "$DEVICE" "${zmr_args[@]}" --trace-dir "$trace_dir" || command_status=$?
289
+ else
290
+ "$ZMR_BIN" run "$ZMR_SCENARIO" --device "$DEVICE" --trace-dir "$trace_dir" || command_status=$?
291
+ fi
292
+
293
+ end_ms="$(python3 -c 'import time; print(int(time.time() * 1000))')"
294
+ duration_ms=$((end_ms - start_ms))
295
+
296
+ if [[ "${#metadata_args[@]}" -gt 0 ]]; then
297
+ "$ROOT/scripts/benchmark_result_row.py" \
298
+ --tool "$tool" \
299
+ --run "$run" \
300
+ --command-status "$command_status" \
301
+ --duration-ms "$duration_ms" \
302
+ --trace-dir "$trace_dir" \
303
+ "${metadata_args[@]}" >> "$RESULTS"
304
+ else
305
+ "$ROOT/scripts/benchmark_result_row.py" \
306
+ --tool "$tool" \
307
+ --run "$run" \
308
+ --command-status "$command_status" \
309
+ --duration-ms "$duration_ms" \
310
+ --trace-dir "$trace_dir" >> "$RESULTS"
311
+ fi
312
+
313
+ return "$command_status"
314
+ }
315
+
316
+ for run in $(seq 1 "$RUNS"); do
317
+ run_one zmr "$run" || true
318
+ done
319
+
320
+ python3 - "$RESULTS" <<'PY'
321
+ import json
322
+ import math
323
+ import statistics
324
+ import sys
325
+ from collections import defaultdict
326
+
327
+ path = sys.argv[1]
328
+ rows = [json.loads(line) for line in open(path, encoding="utf-8") if line.strip()]
329
+ by_tool = defaultdict(list)
330
+ for row in rows:
331
+ by_tool[row["tool"]].append(row)
332
+
333
+ for tool, items in sorted(by_tool.items()):
334
+ durations = [item["durationMs"] for item in items]
335
+ failures = sum(1 for item in items if item["status"] != "ok")
336
+ mean = round(statistics.mean(durations)) if durations else 0
337
+ p95 = sorted(durations)[max(0, math.ceil(len(durations) * 0.95) - 1)] if durations else 0
338
+ print(f"{tool}: runs={len(items)} failures={failures} meanMs={mean} p95Ms={p95}")
339
+
340
+ print(f"results={path}")
341
+ PY
342
+
343
+ gate_args=()
344
+ if [[ -n "$MIN_PASS_RATE" ]]; then
345
+ gate_args+=(--min-pass-rate "$MIN_PASS_RATE")
346
+ fi
347
+ if [[ -n "$MAX_FAILURES" ]]; then
348
+ gate_args+=(--max-failures "$MAX_FAILURES")
349
+ fi
350
+ if [[ -n "$MAX_MEAN_MS" ]]; then
351
+ gate_args+=(--max-mean-ms "$MAX_MEAN_MS")
352
+ fi
353
+ if [[ -n "$MAX_P95_MS" ]]; then
354
+ gate_args+=(--max-p95-ms "$MAX_P95_MS")
355
+ fi
356
+
357
+ if [[ "${#gate_args[@]}" -gt 0 ]]; then
358
+ "$ROOT/scripts/benchmark_gate.py" --results "$RESULTS" "${gate_args[@]}"
359
+ fi
@@ -0,0 +1,117 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ import math
5
+ import statistics
6
+ import sys
7
+ from collections import defaultdict
8
+ from pathlib import Path
9
+
10
+
11
+ def parse_args():
12
+ parser = argparse.ArgumentParser(description="Gate benchmark results by pass rate and duration thresholds.")
13
+ parser.add_argument("--results", required=True, help="Path to benchmark results.jsonl.")
14
+ parser.add_argument("--min-pass-rate", type=float, default=None, help="Minimum pass rate percentage, for example 100.")
15
+ parser.add_argument("--max-failures", type=int, default=None, help="Maximum allowed failed runs.")
16
+ parser.add_argument("--max-mean-ms", type=int, default=None, help="Maximum allowed mean duration in ms.")
17
+ parser.add_argument("--max-p95-ms", type=int, default=None, help="Maximum allowed p95 duration in ms.")
18
+ return parser.parse_args()
19
+
20
+
21
+ def is_pass(row):
22
+ if row.get("status") != "ok":
23
+ return False
24
+ trace_status = row.get("traceStatus")
25
+ return trace_status in (None, "passed")
26
+
27
+
28
+ def p95(durations):
29
+ if not durations:
30
+ return 0
31
+ ordered = sorted(durations)
32
+ index = max(0, math.ceil(len(ordered) * 0.95) - 1)
33
+ return ordered[index]
34
+
35
+
36
+ def read_rows(path):
37
+ rows = []
38
+ with Path(path).open(encoding="utf-8") as handle:
39
+ for line_number, line in enumerate(handle, start=1):
40
+ line = line.strip()
41
+ if not line:
42
+ continue
43
+ try:
44
+ row = json.loads(line)
45
+ except json.JSONDecodeError as exc:
46
+ raise SystemExit(f"{path}:{line_number}: invalid json: {exc}") from exc
47
+ if not isinstance(row, dict):
48
+ raise SystemExit(f"{path}:{line_number}: expected object row")
49
+ rows.append(row)
50
+ return rows
51
+
52
+
53
+ def summarize(tool, rows):
54
+ durations = [int(row.get("durationMs", 0)) for row in rows]
55
+ failures = [row for row in rows if not is_pass(row)]
56
+ passed = len(rows) - len(failures)
57
+ pass_rate = (passed / len(rows) * 100.0) if rows else 0.0
58
+ mean_ms = round(statistics.mean(durations)) if durations else 0
59
+ p95_ms = p95(durations)
60
+ return {
61
+ "tool": tool,
62
+ "runs": len(rows),
63
+ "passed": passed,
64
+ "failures": len(failures),
65
+ "passRate": pass_rate,
66
+ "meanMs": mean_ms,
67
+ "p95Ms": p95_ms,
68
+ "failureRows": failures,
69
+ }
70
+
71
+
72
+ def format_summary(summary):
73
+ return (
74
+ f"{summary['tool']}: runs={summary['runs']} "
75
+ f"passRate={summary['passRate']:.2f}% failures={summary['failures']} "
76
+ f"meanMs={summary['meanMs']} p95Ms={summary['p95Ms']}"
77
+ )
78
+
79
+
80
+ def violations(summary, args):
81
+ problems = []
82
+ if args.min_pass_rate is not None and summary["passRate"] < args.min_pass_rate:
83
+ problems.append(f"passRate {summary['passRate']:.2f}% < {args.min_pass_rate:.2f}%")
84
+ if args.max_failures is not None and summary["failures"] > args.max_failures:
85
+ problems.append(f"failures {summary['failures']} > {args.max_failures}")
86
+ if args.max_mean_ms is not None and summary["meanMs"] > args.max_mean_ms:
87
+ problems.append(f"meanMs {summary['meanMs']} > {args.max_mean_ms}")
88
+ if args.max_p95_ms is not None and summary["p95Ms"] > args.max_p95_ms:
89
+ problems.append(f"p95Ms {summary['p95Ms']} > {args.max_p95_ms}")
90
+ return problems
91
+
92
+
93
+ def main():
94
+ args = parse_args()
95
+ rows = read_rows(args.results)
96
+ if not rows:
97
+ print(f"no benchmark rows found: {args.results}", file=sys.stderr)
98
+ return 2
99
+
100
+ by_tool = defaultdict(list)
101
+ for row in rows:
102
+ by_tool[str(row.get("tool", "unknown"))].append(row)
103
+
104
+ failed = False
105
+ for tool in sorted(by_tool):
106
+ summary = summarize(tool, by_tool[tool])
107
+ print(format_summary(summary))
108
+ problems = violations(summary, args)
109
+ for problem in problems:
110
+ print(f"gate failed for {tool}: {problem}", file=sys.stderr)
111
+ failed = failed or bool(problems)
112
+
113
+ return 1 if failed else 0
114
+
115
+
116
+ if __name__ == "__main__":
117
+ raise SystemExit(main())
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ from pathlib import Path
5
+
6
+
7
+ def parse_args():
8
+ parser = argparse.ArgumentParser(description="Build one benchmark results JSON row.")
9
+ parser.add_argument("--tool", required=True)
10
+ parser.add_argument("--run", required=True, type=int)
11
+ parser.add_argument("--command-status", required=True, type=int)
12
+ parser.add_argument("--duration-ms", required=True, type=int)
13
+ parser.add_argument("--trace-dir", required=True)
14
+ parser.add_argument("--platform")
15
+ parser.add_argument("--device")
16
+ parser.add_argument("--app-id")
17
+ parser.add_argument("--scenario")
18
+ parser.add_argument("--app-build")
19
+ return parser.parse_args()
20
+
21
+
22
+ def read_zmr_trace(trace_dir):
23
+ events_path = Path(trace_dir) / "events.jsonl"
24
+ if not events_path.exists():
25
+ return {}
26
+
27
+ last_step_error = {}
28
+ last_scenario_end = {}
29
+
30
+ with events_path.open(encoding="utf-8") as events:
31
+ for line in events:
32
+ line = line.strip()
33
+ if not line:
34
+ continue
35
+ try:
36
+ event = json.loads(line)
37
+ except json.JSONDecodeError:
38
+ continue
39
+
40
+ payload = event.get("payload")
41
+ if not isinstance(payload, dict):
42
+ payload = {}
43
+
44
+ if event.get("kind") == "step.error":
45
+ last_step_error = payload
46
+ elif event.get("kind") == "scenario.end":
47
+ last_scenario_end = payload
48
+
49
+ trace = {}
50
+ if "status" in last_scenario_end:
51
+ trace["traceStatus"] = last_scenario_end["status"]
52
+ if "error" in last_scenario_end:
53
+ trace["traceError"] = last_scenario_end["error"]
54
+ elif "error" in last_step_error:
55
+ trace["traceError"] = last_step_error["error"]
56
+ if "failedStepIndex" in last_scenario_end:
57
+ trace["failedStepIndex"] = last_scenario_end["failedStepIndex"]
58
+ elif "index" in last_step_error:
59
+ trace["failedStepIndex"] = last_step_error["index"]
60
+ return trace
61
+
62
+
63
+ def main():
64
+ args = parse_args()
65
+ row = {
66
+ "tool": args.tool,
67
+ "run": args.run,
68
+ "status": "ok" if args.command_status == 0 else "failed",
69
+ "durationMs": args.duration_ms,
70
+ "traceDir": args.trace_dir,
71
+ }
72
+ metadata = {
73
+ "platform": args.platform,
74
+ "device": args.device,
75
+ "appId": args.app_id,
76
+ "scenario": args.scenario,
77
+ "appBuild": args.app_build,
78
+ }
79
+ row.update({key: value for key, value in metadata.items() if value})
80
+
81
+ if args.tool == "zmr":
82
+ row.update(read_zmr_trace(args.trace_dir))
83
+
84
+ print(json.dumps(row, separators=(",", ":")))
85
+
86
+
87
+ if __name__ == "__main__":
88
+ main()