zig-mobile-runner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +484 -0
- package/CONTRIBUTING.md +42 -0
- package/FEATURES.md +112 -0
- package/LICENSE +21 -0
- package/README.md +255 -0
- package/SECURITY.md +34 -0
- package/build.zig +38 -0
- package/build.zig.zon +7 -0
- package/clients/README.md +144 -0
- package/clients/go/README.md +24 -0
- package/clients/go/examples/fake-session/main.go +93 -0
- package/clients/go/go.mod +3 -0
- package/clients/go/zmr/client.go +432 -0
- package/clients/kotlin/README.md +35 -0
- package/clients/kotlin/build.gradle.kts +35 -0
- package/clients/kotlin/settings.gradle.kts +15 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
- package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
- package/clients/python/README.md +29 -0
- package/clients/python/examples/fake_session.py +48 -0
- package/clients/python/pyproject.toml +13 -0
- package/clients/python/zmr_client.py +202 -0
- package/clients/rust/Cargo.lock +107 -0
- package/clients/rust/Cargo.toml +10 -0
- package/clients/rust/README.md +19 -0
- package/clients/rust/examples/fake_session.rs +70 -0
- package/clients/rust/src/lib.rs +461 -0
- package/clients/swift/Package.swift +16 -0
- package/clients/swift/README.md +36 -0
- package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
- package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
- package/clients/typescript/README.md +34 -0
- package/clients/typescript/examples/fake-session.mjs +36 -0
- package/clients/typescript/index.d.ts +144 -0
- package/clients/typescript/index.mjs +192 -0
- package/clients/typescript/package.json +8 -0
- package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
- package/docs/adr/0002-app-local-zmr-contract.md +39 -0
- package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
- package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
- package/docs/adr/README.md +12 -0
- package/docs/ai-agents.md +156 -0
- package/docs/app-integration.md +316 -0
- package/docs/benchmarking.md +275 -0
- package/docs/client-installation.md +141 -0
- package/docs/clients.md +98 -0
- package/docs/config.md +175 -0
- package/docs/demo.md +259 -0
- package/docs/dsl.md +57 -0
- package/docs/install.md +233 -0
- package/docs/market-positioning.md +70 -0
- package/docs/npm.md +359 -0
- package/docs/protocol-fixtures/README.md +8 -0
- package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
- package/docs/protocol-versioning.md +65 -0
- package/docs/protocol.md +560 -0
- package/docs/publication.md +77 -0
- package/docs/release-audit.md +99 -0
- package/docs/release-candidate.md +111 -0
- package/docs/release-evidence.md +188 -0
- package/docs/release-notes-template.md +58 -0
- package/docs/roadmap.md +334 -0
- package/docs/scenario-authoring.md +88 -0
- package/docs/shipping.md +170 -0
- package/docs/trace-privacy.md +88 -0
- package/docs/troubleshooting.md +256 -0
- package/examples/android-app-auth-probe.json +89 -0
- package/examples/android-app-error-state.json +13 -0
- package/examples/android-app-login-smoke.json +192 -0
- package/examples/android-app-onboarding.json +12 -0
- package/examples/android-app-referral-deep-link.json +12 -0
- package/examples/android-shim-smoke.json +19 -0
- package/examples/demo-failure.json +12 -0
- package/examples/demo-fake.json +14 -0
- package/examples/ios-dev-client-open-link.json +26 -0
- package/examples/ios-dev-client-route-snapshot.json +24 -0
- package/examples/ios-shim-smoke.json +23 -0
- package/examples/ios-smoke.json +9 -0
- package/go.work +3 -0
- package/npm/agents.mjs +183 -0
- package/npm/app-config.mjs +95 -0
- package/npm/build-zmr.mjs +21 -0
- package/npm/commands.mjs +104 -0
- package/npm/generated-files.mjs +50 -0
- package/npm/index.mjs +75 -0
- package/npm/init-app.mjs +80 -0
- package/npm/package-scripts.mjs +72 -0
- package/npm/postinstall.mjs +21 -0
- package/npm/scaffold.mjs +179 -0
- package/npm/scenarios.mjs +93 -0
- package/npm/setup.mjs +69 -0
- package/npm/wizard.mjs +117 -0
- package/npm/zmr.mjs +23 -0
- package/package.json +114 -0
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +26 -0
- package/schemas/action-result.schema.json +27 -0
- package/schemas/capabilities-output.schema.json +98 -0
- package/schemas/devices-output.schema.json +25 -0
- package/schemas/doctor-output.schema.json +51 -0
- package/schemas/explain-output.schema.json +51 -0
- package/schemas/import-output.schema.json +23 -0
- package/schemas/init-output.schema.json +71 -0
- package/schemas/json-rpc.schema.json +55 -0
- package/schemas/release-manifest.schema.json +43 -0
- package/schemas/release-readiness-output.schema.json +127 -0
- package/schemas/run-output.schema.json +43 -0
- package/schemas/scenario.schema.json +128 -0
- package/schemas/schemas-output.schema.json +26 -0
- package/schemas/semantic-snapshot.schema.json +116 -0
- package/schemas/snapshot.schema.json +60 -0
- package/schemas/trace-event.schema.json +14 -0
- package/schemas/trace-manifest.schema.json +59 -0
- package/schemas/validate-output.schema.json +42 -0
- package/schemas/version-output.schema.json +23 -0
- package/schemas/zmr-config.schema.json +75 -0
- package/scripts/android-emulator.sh +126 -0
- package/scripts/assert-ios-physical-ready.sh +213 -0
- package/scripts/benchmark-command.sh +307 -0
- package/scripts/benchmark.sh +359 -0
- package/scripts/benchmark_gate.py +117 -0
- package/scripts/benchmark_result_row.py +88 -0
- package/scripts/compare-benchmarks.py +288 -0
- package/scripts/create-android-demo-app.sh +342 -0
- package/scripts/create-ios-demo-app.sh +261 -0
- package/scripts/demo-android-real.sh +232 -0
- package/scripts/demo-ios-real.sh +270 -0
- package/scripts/demo.sh +464 -0
- package/scripts/device-matrix.sh +338 -0
- package/scripts/ensure-ios-shim-target.rb +237 -0
- package/scripts/install-android-shim.sh +281 -0
- package/scripts/install-ios-shim.sh +589 -0
- package/scripts/pilot-gate.sh +560 -0
- package/scripts/release-readiness.py +838 -0
- package/scripts/release-readiness.sh +91 -0
- package/scripts/run-android-pilot.sh +561 -0
- package/scripts/run-ios-pilot.sh +509 -0
- package/shims/android/README.md +21 -0
- package/shims/android/ZMRShimInstrumentedTest.java +152 -0
- package/shims/android/protocol.md +18 -0
- package/shims/ios/README.md +50 -0
- package/shims/ios/ZMRShim.swift +110 -0
- package/shims/ios/ZMRShimUITestCase.swift +475 -0
- package/shims/ios/protocol.md +74 -0
- package/skills/zmr-mobile-testing/SKILL.md +127 -0
- package/src/android.zig +344 -0
- package/src/android_device_info.zig +99 -0
- package/src/android_emulator.zig +154 -0
- package/src/android_screen_recording.zig +112 -0
- package/src/android_shell.zig +112 -0
- package/src/bundle.zig +124 -0
- package/src/bundle_redaction.zig +272 -0
- package/src/bundle_tar.zig +123 -0
- package/src/cli_devices.zig +97 -0
- package/src/cli_doctor.zig +114 -0
- package/src/cli_import.zig +70 -0
- package/src/cli_info.zig +39 -0
- package/src/cli_init.zig +72 -0
- package/src/cli_output.zig +467 -0
- package/src/cli_run.zig +259 -0
- package/src/cli_serve.zig +287 -0
- package/src/cli_trace.zig +111 -0
- package/src/cli_validate.zig +41 -0
- package/src/command.zig +211 -0
- package/src/config.zig +305 -0
- package/src/config_diagnostics.zig +212 -0
- package/src/config_paths.zig +49 -0
- package/src/device_registry.zig +37 -0
- package/src/doctor.zig +412 -0
- package/src/doctor_hints.zig +52 -0
- package/src/errors.zig +55 -0
- package/src/fake_device.zig +163 -0
- package/src/health.zig +28 -0
- package/src/importer.zig +343 -0
- package/src/importer_json.zig +100 -0
- package/src/importer_model.zig +103 -0
- package/src/ios.zig +399 -0
- package/src/ios_devices.zig +219 -0
- package/src/ios_lifecycle.zig +72 -0
- package/src/ios_shim.zig +242 -0
- package/src/ios_snapshot.zig +20 -0
- package/src/json_fields.zig +80 -0
- package/src/json_rpc.zig +150 -0
- package/src/json_rpc_methods.zig +318 -0
- package/src/json_rpc_observation.zig +31 -0
- package/src/json_rpc_params.zig +52 -0
- package/src/json_rpc_protocol.zig +110 -0
- package/src/json_rpc_trace.zig +73 -0
- package/src/main.zig +135 -0
- package/src/mcp.zig +234 -0
- package/src/mcp_protocol.zig +64 -0
- package/src/mcp_trace.zig +83 -0
- package/src/report.zig +346 -0
- package/src/report_html.zig +63 -0
- package/src/report_values.zig +27 -0
- package/src/run_options.zig +152 -0
- package/src/runner.zig +280 -0
- package/src/runner_actions.zig +109 -0
- package/src/runner_config.zig +6 -0
- package/src/runner_diagnostics.zig +268 -0
- package/src/runner_events.zig +170 -0
- package/src/runner_native.zig +88 -0
- package/src/runner_waits.zig +300 -0
- package/src/scaffold.zig +472 -0
- package/src/scenario.zig +346 -0
- package/src/scenario_fields.zig +50 -0
- package/src/schema_registry.zig +53 -0
- package/src/selector.zig +84 -0
- package/src/semantic.zig +171 -0
- package/src/trace.zig +315 -0
- package/src/trace_json.zig +340 -0
- package/src/trace_summary.zig +218 -0
- package/src/trace_summary_diagnostic.zig +202 -0
- package/src/types.zig +120 -0
- package/src/uiautomator.zig +164 -0
- package/src/validation.zig +187 -0
- package/src/version.zig +22 -0
- package/viewer/app.js +373 -0
- package/viewer/index.html +126 -0
- package/viewer/parser.js +233 -0
- package/viewer/styles.css +585 -0
|
@@ -0,0 +1,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()
|