zeno-mobile-runner 0.2.16 → 0.2.17
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 +33 -0
- package/CONTRIBUTING.md +20 -7
- package/FEATURES.md +29 -20
- package/README.md +73 -57
- package/SECURITY.md +11 -6
- package/clients/README.md +8 -7
- package/clients/go/README.md +2 -2
- package/clients/kotlin/README.md +2 -2
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/python/README.md +2 -1
- package/clients/python/pyproject.toml +1 -1
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/rust/README.md +2 -2
- package/clients/swift/README.md +2 -2
- package/clients/typescript/README.md +2 -1
- package/clients/typescript/package.json +1 -1
- package/docs/adr/0001-agent-native-runner-boundary.md +1 -1
- package/docs/adr/README.md +7 -5
- package/docs/agent-discovery.md +15 -15
- package/docs/ai-agents.md +30 -20
- package/docs/app-integration.md +59 -27
- package/docs/benchmarking.md +16 -8
- package/docs/benchmarks/README.md +3 -1
- package/docs/benchmarks/benchmark-lab-v1.md +1 -1
- package/docs/client-installation.md +18 -9
- package/docs/clients.md +7 -6
- package/docs/config.md +29 -15
- package/docs/demo.md +14 -9
- package/docs/expo-smoke.md +12 -18
- package/docs/frameworks.md +30 -21
- package/docs/install.md +63 -13
- package/docs/npm.md +45 -27
- package/docs/production-readiness.md +32 -17
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol-versioning.md +5 -3
- package/docs/protocol.md +33 -18
- package/docs/scenario-authoring.md +15 -8
- package/docs/support-matrix.md +38 -0
- package/docs/trace-privacy.md +5 -3
- package/docs/troubleshooting.md +17 -14
- package/npm/app-config.mjs +2 -0
- package/npm/commands.mjs +4 -4
- package/npm/scaffold.mjs +2 -2
- package/package.json +2 -2
- 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 +6 -3
- package/schemas/import-output.schema.json +1 -1
- package/schemas/scenario.schema.json +2 -0
- package/schemas/zmr-config.schema.json +2 -1
- package/scripts/public-metadata-guard.sh +101 -0
- package/shims/android/README.md +4 -3
- package/shims/android/protocol.md +3 -2
- package/shims/ios/README.md +5 -5
- package/shims/ios/protocol.md +2 -1
- package/skills/zmr-mobile-testing/SKILL.md +9 -8
- package/src/android_emulator.zig +54 -5
- package/src/cli_import.zig +15 -2
- package/src/cli_output.zig +2 -0
- package/src/cli_run.zig +8 -0
- package/src/config.zig +3 -0
- package/src/errors.zig +3 -0
- package/src/ios_devices.zig +100 -0
- package/src/main.zig +1 -1
- package/src/mcp_protocol.zig +12 -9
- package/src/run_options.zig +4 -0
- package/src/scaffold.zig +10 -8
- package/src/scenario.zig +43 -0
- package/src/selector.zig +53 -9
- package/src/trace_json.zig +4 -0
- package/src/validation.zig +5 -0
- package/src/version.zig +1 -1
package/src/main.zig
CHANGED
|
@@ -154,7 +154,7 @@ fn usage() !void {
|
|
|
154
154
|
\\ zmr init --app [--dir <app-root>] [--app-id <id>] [--force] [--json]
|
|
155
155
|
\\ zmr import flow-yaml <flow.yaml> --out <scenario.json> [--name <name>] [--app-id <id>] [--force] [--json]
|
|
156
156
|
\\ zmr inspect [--json] [--dir <app-root>] [--config <path>]
|
|
157
|
-
\\ zmr run [scenario.json] [--json] [--config <path>] [--platform android|ios] [--ios-device-type simulator|physical] [--device <serial>] [--app-id <id>] [--trace-dir <path>] [--discover-out <scenario.json>] [--android-avd <name>] [--create-avd-if-missing] [--avd-system-image <pkg>] [--avd-device <profile>] [--restore-snapshot <name>] [--reset-emulator] [--wait-emulator] [--screen-record] [--no-screen-record] [--adb <path>] [--emulator <path>] [--avdmanager <path>] [--android-shim <path>] [--xcrun <path>] [--ios-shim <path>]
|
|
157
|
+
\\ zmr run [scenario.json] [--json] [--config <path>] [--platform android|ios] [--ios-device-type simulator|physical] [--device <serial>] [--app-id <id>] [--trace-dir <path>] [--discover-out <scenario.json>] [--android-avd <name>] [--create-avd-if-missing] [--avd-system-image <pkg>] [--avd-device <profile>] [--restore-snapshot <name>] [--reset-emulator] [--wait-emulator] [--ensure-device] [--no-ensure-device] [--screen-record] [--no-screen-record] [--adb <path>] [--emulator <path>] [--avdmanager <path>] [--android-shim <path>] [--xcrun <path>] [--ios-shim <path>]
|
|
158
158
|
\\ zmr report <trace-or-benchmark-dir> --out <report.html> [--junit <report.xml>]
|
|
159
159
|
\\ zmr explain <trace-dir> [--json]
|
|
160
160
|
\\ zmr export <trace-dir> --out <bundle.zmrtrace> [--redact] [--omit-screenshots]
|
package/src/mcp_protocol.zig
CHANGED
|
@@ -61,6 +61,9 @@ pub fn writeId(writer: anytype, id: ?std.json.Value) !void {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
const selector_schema_json =
|
|
65
|
+
"{\"type\":\"object\",\"additionalProperties\":false,\"minProperties\":1,\"properties\":{\"id\":{\"type\":\"string\"},\"resourceId\":{\"type\":\"string\"},\"stableId\":{\"type\":\"string\"},\"text\":{\"type\":\"string\"},\"textContains\":{\"type\":\"string\"},\"contentDesc\":{\"type\":\"string\"},\"contentDescContains\":{\"type\":\"string\"},\"className\":{\"type\":\"string\"}}}";
|
|
66
|
+
|
|
64
67
|
const tool_list_json =
|
|
65
68
|
"{\"tools\":[" ++
|
|
66
69
|
"{\"name\":\"snapshot\",\"description\":\"Capture the current mobile observation snapshot as JSON.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
@@ -69,19 +72,19 @@ const tool_list_json =
|
|
|
69
72
|
"{\"name\":\"launch_app\",\"description\":\"Launch the configured app on the selected device.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
70
73
|
"{\"name\":\"stop_app\",\"description\":\"Stop the configured app on the selected device.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
71
74
|
"{\"name\":\"clear_state\",\"description\":\"Clear the configured app state where the platform supports it.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
72
|
-
"{\"name\":\"tap\",\"description\":\"Tap a visible element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":
|
|
73
|
-
"{\"name\":\"type\",\"description\":\"Type text, optionally after focusing an element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"text\"],\"properties\":{\"selector\":
|
|
75
|
+
"{\"name\":\"tap\",\"description\":\"Tap a visible element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":" ++ selector_schema_json ++ "}}}," ++
|
|
76
|
+
"{\"name\":\"type\",\"description\":\"Type text, optionally after focusing an element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"text\"],\"properties\":{\"selector\":" ++ selector_schema_json ++ ",\"text\":{\"type\":\"string\"}}}}," ++
|
|
74
77
|
"{\"name\":\"hide_keyboard\",\"description\":\"Dismiss the software keyboard when the platform can do so.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
75
|
-
"{\"name\":\"erase_text\",\"description\":\"Erase text, optionally after focusing an element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"selector\":
|
|
78
|
+
"{\"name\":\"erase_text\",\"description\":\"Erase text, optionally after focusing an element by selector.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"selector\":" ++ selector_schema_json ++ ",\"maxChars\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
76
79
|
"{\"name\":\"swipe\",\"description\":\"Swipe between absolute screen coordinates.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"x1\",\"y1\",\"x2\",\"y2\"],\"properties\":{\"x1\":{\"type\":\"integer\"},\"y1\":{\"type\":\"integer\"},\"x2\":{\"type\":\"integer\"},\"y2\":{\"type\":\"integer\"},\"durationMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
77
80
|
"{\"name\":\"press_back\",\"description\":\"Press Android back or the platform-equivalent navigation action.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{}}}," ++
|
|
78
81
|
"{\"name\":\"open_link\",\"description\":\"Open a deep link URL in the target app.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"url\"],\"properties\":{\"url\":{\"type\":\"string\"}}}}," ++
|
|
79
|
-
"{\"name\":\"wait_visible\",\"description\":\"Wait for an element selector to become visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":
|
|
80
|
-
"{\"name\":\"wait_not_visible\",\"description\":\"Wait for an element selector to no longer be visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":
|
|
81
|
-
"{\"name\":\"wait_any\",\"description\":\"Wait for the first visible selector from an ordered selector list.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selectors\"],\"properties\":{\"selectors\":{\"type\":\"array\",\"minItems\":1,\"items\":
|
|
82
|
-
"{\"name\":\"scroll_until_visible\",\"description\":\"Scroll until an element selector is visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":
|
|
83
|
-
"{\"name\":\"assert_visible\",\"description\":\"Assert that an element selector is visible within the timeout.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":
|
|
84
|
-
"{\"name\":\"assert_not_visible\",\"description\":\"Assert that an element selector is not visible within the timeout.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":
|
|
82
|
+
"{\"name\":\"wait_visible\",\"description\":\"Wait for an element selector to become visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":" ++ selector_schema_json ++ ",\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
83
|
+
"{\"name\":\"wait_not_visible\",\"description\":\"Wait for an element selector to no longer be visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":" ++ selector_schema_json ++ ",\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
84
|
+
"{\"name\":\"wait_any\",\"description\":\"Wait for the first visible selector from an ordered selector list.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selectors\"],\"properties\":{\"selectors\":{\"type\":\"array\",\"minItems\":1,\"items\":" ++ selector_schema_json ++ "},\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
85
|
+
"{\"name\":\"scroll_until_visible\",\"description\":\"Scroll until an element selector is visible.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":" ++ selector_schema_json ++ ",\"direction\":{\"type\":\"string\",\"enum\":[\"down\",\"up\"]},\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
86
|
+
"{\"name\":\"assert_visible\",\"description\":\"Assert that an element selector is visible within the timeout.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":" ++ selector_schema_json ++ ",\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
87
|
+
"{\"name\":\"assert_not_visible\",\"description\":\"Assert that an element selector is not visible within the timeout.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"selector\"],\"properties\":{\"selector\":" ++ selector_schema_json ++ ",\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
85
88
|
"{\"name\":\"assert_healthy\",\"description\":\"Assert that the app is free of common crash and error overlays.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"timeoutMs\":{\"type\":\"integer\",\"minimum\":0}}}}," ++
|
|
86
89
|
"{\"name\":\"scenario_validate\",\"description\":\"Validate a ZMR scenario file and return structured diagnostics.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"required\":[\"path\"],\"properties\":{\"path\":{\"type\":\"string\"}}}}," ++
|
|
87
90
|
"{\"name\":\"trace_events\",\"description\":\"Read live trace events from a traced MCP session.\",\"inputSchema\":{\"type\":\"object\",\"additionalProperties\":false,\"properties\":{\"afterSeq\":{\"type\":\"integer\",\"minimum\":0},\"limit\":{\"type\":\"integer\",\"minimum\":1}}}}," ++
|
package/src/run_options.zig
CHANGED
|
@@ -28,6 +28,7 @@ pub const RawRunOptions = struct {
|
|
|
28
28
|
android_avd_device_profile: ?[]const u8 = null,
|
|
29
29
|
android_reset_before_run: ?bool = null,
|
|
30
30
|
android_wait_ready: ?bool = null,
|
|
31
|
+
ensure_device: ?bool = null,
|
|
31
32
|
platform: Platform = .android,
|
|
32
33
|
ios_device_type: IosDeviceType = .simulator,
|
|
33
34
|
};
|
|
@@ -46,6 +47,7 @@ pub const ResolvedRunOptions = struct {
|
|
|
46
47
|
android_avd_device_profile: ?[]const u8,
|
|
47
48
|
android_reset_before_run: bool,
|
|
48
49
|
android_wait_ready: bool,
|
|
50
|
+
ensure_device: bool,
|
|
49
51
|
platform: Platform,
|
|
50
52
|
ios_device_type: IosDeviceType,
|
|
51
53
|
};
|
|
@@ -86,6 +88,7 @@ pub fn resolveRun(raw: RawRunOptions, cfg: ?config.Config) ResolvedRunOptions {
|
|
|
86
88
|
.android_avd_device_profile = raw.android_avd_device_profile orelse if (platform_cfg) |pc| pc.avd_device_profile else null,
|
|
87
89
|
.android_reset_before_run = raw.android_reset_before_run orelse if (platform_cfg) |pc| pc.reset_before_run else false,
|
|
88
90
|
.android_wait_ready = raw.android_wait_ready orelse if (platform_cfg) |pc| pc.wait_ready else false,
|
|
91
|
+
.ensure_device = raw.ensure_device orelse if (platform_cfg) |pc| pc.ensure_device else false,
|
|
89
92
|
.platform = raw.platform,
|
|
90
93
|
.ios_device_type = raw.ios_device_type,
|
|
91
94
|
};
|
|
@@ -122,6 +125,7 @@ pub fn androidPreflight(
|
|
|
122
125
|
.avd_device_profile = resolved.android_avd_device_profile,
|
|
123
126
|
.reset_before_run = resolved.android_reset_before_run,
|
|
124
127
|
.wait_ready = resolved.android_wait_ready,
|
|
128
|
+
.ensure_ready = resolved.ensure_device,
|
|
125
129
|
};
|
|
126
130
|
return if (android_emulator.hasWork(options)) options else null;
|
|
127
131
|
}
|
package/src/scaffold.zig
CHANGED
|
@@ -131,13 +131,15 @@ fn writeAppConfig(path: []const u8, app_id: []const u8, force: bool) !void {
|
|
|
131
131
|
\\ "enabled": true,
|
|
132
132
|
\\ "defaultDevice": "emulator-5554",
|
|
133
133
|
\\ "smokeScenario": ".zmr/android-smoke.json",
|
|
134
|
-
\\ "traceDir": "traces/zmr-android"
|
|
134
|
+
\\ "traceDir": "traces/zmr-android",
|
|
135
|
+
\\ "ensureDevice": true
|
|
135
136
|
\\ },
|
|
136
137
|
\\ "ios": {
|
|
137
138
|
\\ "enabled": true,
|
|
138
139
|
\\ "defaultDevice": "booted",
|
|
139
140
|
\\ "smokeScenario": ".zmr/ios-smoke.json",
|
|
140
|
-
\\ "traceDir": "traces/zmr-ios"
|
|
141
|
+
\\ "traceDir": "traces/zmr-ios",
|
|
142
|
+
\\ "ensureDevice": true
|
|
141
143
|
\\ },
|
|
142
144
|
\\ "artifacts": {
|
|
143
145
|
\\ "screenshots": true,
|
|
@@ -149,7 +151,7 @@ fn writeAppConfig(path: []const u8, app_id: []const u8, force: bool) !void {
|
|
|
149
151
|
\\ "doctor": "zmr doctor --strict --json --config .zmr/config.json",
|
|
150
152
|
\\ "schemas": "zmr schemas --json",
|
|
151
153
|
\\ "validate": "zmr validate --json .zmr/android-smoke.json && zmr validate --json .zmr/ios-smoke.json",
|
|
152
|
-
\\ "android": "zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android",
|
|
154
|
+
\\ "android": "zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android --ensure-device",
|
|
153
155
|
\\ "androidReport": "zmr report traces/zmr-android --out traces/zmr-android/report.html --junit traces/zmr-android/junit.xml",
|
|
154
156
|
\\ "androidReliability": "export ZMR_BIN=\"${ZMR_BIN:-zmr}\"; zmr-benchmark --zmr .zmr/android-smoke.json --device emulator-5554 --app-id
|
|
155
157
|
);
|
|
@@ -157,7 +159,7 @@ fn writeAppConfig(path: []const u8, app_id: []const u8, force: bool) !void {
|
|
|
157
159
|
try writeJsonShellArg(writer, app_id);
|
|
158
160
|
try writer.writeAll(
|
|
159
161
|
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && \"$ZMR_BIN\" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html --junit traces/zmr-android-reliability/junit.xml",
|
|
160
|
-
\\ "ios": "zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios",
|
|
162
|
+
\\ "ios": "zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios --ensure-device",
|
|
161
163
|
\\ "iosReport": "zmr report traces/zmr-ios --out traces/zmr-ios/report.html --junit traces/zmr-ios/junit.xml",
|
|
162
164
|
\\ "iosReliability": "export ZMR_BIN=\"${ZMR_BIN:-zmr}\"; zmr-benchmark --zmr .zmr/ios-smoke.json --platform ios --device booted --app-id
|
|
163
165
|
);
|
|
@@ -366,7 +368,7 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
366
368
|
\\## Direct Smoke Runs
|
|
367
369
|
\\
|
|
368
370
|
\\```bash
|
|
369
|
-
\\zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android
|
|
371
|
+
\\zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android --ensure-device
|
|
370
372
|
\\zmr report traces/zmr-android --out traces/zmr-android/report.html --junit traces/zmr-android/junit.xml
|
|
371
373
|
\\export ZMR_BIN="${ZMR_BIN:-zmr}"; zmr-benchmark --zmr .zmr/android-smoke.json --device emulator-5554 --app-id
|
|
372
374
|
);
|
|
@@ -374,7 +376,7 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
374
376
|
try writeShellArg(writer, app_id);
|
|
375
377
|
try writer.writeAll(
|
|
376
378
|
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && "$ZMR_BIN" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html --junit traces/zmr-android-reliability/junit.xml
|
|
377
|
-
\\zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios
|
|
379
|
+
\\zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios --ensure-device
|
|
378
380
|
\\zmr report traces/zmr-ios --out traces/zmr-ios/report.html --junit traces/zmr-ios/junit.xml
|
|
379
381
|
\\export ZMR_BIN="${ZMR_BIN:-zmr}"; zmr-benchmark --zmr .zmr/ios-smoke.json --platform ios --device booted --app-id
|
|
380
382
|
);
|
|
@@ -399,7 +401,7 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
399
401
|
\\zmr doctor --strict --json --config .zmr/config.json
|
|
400
402
|
\\zmr schemas --json
|
|
401
403
|
\\zmr validate --json .zmr/android-smoke.json && zmr validate --json .zmr/ios-smoke.json
|
|
402
|
-
\\zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android
|
|
404
|
+
\\zmr run .zmr/android-smoke.json --device emulator-5554 --trace-dir traces/zmr-android --ensure-device
|
|
403
405
|
\\zmr report traces/zmr-android --out traces/zmr-android/report.html --junit traces/zmr-android/junit.xml
|
|
404
406
|
\\export ZMR_BIN="${ZMR_BIN:-zmr}"; zmr-benchmark --zmr .zmr/android-smoke.json --device emulator-5554 --app-id
|
|
405
407
|
);
|
|
@@ -407,7 +409,7 @@ fn writeAgentInstructions(path: []const u8, app_id: []const u8, force: bool) !vo
|
|
|
407
409
|
try writeShellArg(writer, app_id);
|
|
408
410
|
try writer.writeAll(
|
|
409
411
|
\\ --runs 20 --trace-root traces/zmr-android-reliability --min-pass-rate 100 --max-failures 0 --max-p95-ms 30000 && "$ZMR_BIN" report traces/zmr-android-reliability --out traces/zmr-android-reliability/report.html --junit traces/zmr-android-reliability/junit.xml
|
|
410
|
-
\\zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios
|
|
412
|
+
\\zmr run .zmr/ios-smoke.json --platform ios --device booted --trace-dir traces/zmr-ios --ensure-device
|
|
411
413
|
\\zmr report traces/zmr-ios --out traces/zmr-ios/report.html --junit traces/zmr-ios/junit.xml
|
|
412
414
|
\\export ZMR_BIN="${ZMR_BIN:-zmr}"; zmr-benchmark --zmr .zmr/ios-smoke.json --platform ios --device booted --app-id
|
|
413
415
|
);
|
package/src/scenario.zig
CHANGED
|
@@ -179,6 +179,7 @@ pub fn parseSlice(allocator: std.mem.Allocator, content: []const u8) !Scenario {
|
|
|
179
179
|
defer parsed.deinit();
|
|
180
180
|
if (parsed.value != .object) return error.ScenarioMustBeObject;
|
|
181
181
|
const root = parsed.value.object;
|
|
182
|
+
try rejectUnknownRootFields(root);
|
|
182
183
|
|
|
183
184
|
const name = try fields.requiredString(allocator, root, "name");
|
|
184
185
|
errdefer allocator.free(name);
|
|
@@ -204,6 +205,7 @@ pub fn parseSlice(allocator: std.mem.Allocator, content: []const u8) !Scenario {
|
|
|
204
205
|
fn parseStep(allocator: std.mem.Allocator, value: std.json.Value) anyerror!Step {
|
|
205
206
|
if (value != .object) return error.StepMustBeObject;
|
|
206
207
|
const object = value.object;
|
|
208
|
+
try rejectUnknownStepFields(object);
|
|
207
209
|
var parsed = try parseRawStep(allocator, object);
|
|
208
210
|
errdefer parsed.deinit(allocator);
|
|
209
211
|
|
|
@@ -352,6 +354,47 @@ fn parseRawStep(allocator: std.mem.Allocator, object: std.json.ObjectMap) anyerr
|
|
|
352
354
|
return error.unknownScenarioAction;
|
|
353
355
|
}
|
|
354
356
|
|
|
357
|
+
fn rejectUnknownRootFields(object: std.json.ObjectMap) !void {
|
|
358
|
+
var iterator = object.iterator();
|
|
359
|
+
while (iterator.next()) |entry| {
|
|
360
|
+
const key = entry.key_ptr.*;
|
|
361
|
+
if (std.mem.eql(u8, key, "name") or
|
|
362
|
+
std.mem.eql(u8, key, "appId") or
|
|
363
|
+
std.mem.eql(u8, key, "steps")) continue;
|
|
364
|
+
return error.UnknownScenarioField;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
fn rejectUnknownStepFields(object: std.json.ObjectMap) !void {
|
|
369
|
+
var iterator = object.iterator();
|
|
370
|
+
while (iterator.next()) |entry| {
|
|
371
|
+
if (!isKnownStepField(entry.key_ptr.*)) return error.UnknownScenarioStepField;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
fn isKnownStepField(key: []const u8) bool {
|
|
376
|
+
return std.mem.eql(u8, key, "action") or
|
|
377
|
+
std.mem.eql(u8, key, "optional") or
|
|
378
|
+
std.mem.eql(u8, key, "url") or
|
|
379
|
+
std.mem.eql(u8, key, "latitude") or
|
|
380
|
+
std.mem.eql(u8, key, "longitude") or
|
|
381
|
+
std.mem.eql(u8, key, "selector") or
|
|
382
|
+
std.mem.eql(u8, key, "selectors") or
|
|
383
|
+
std.mem.eql(u8, key, "text") or
|
|
384
|
+
std.mem.eql(u8, key, "maxChars") or
|
|
385
|
+
std.mem.eql(u8, key, "x1") or
|
|
386
|
+
std.mem.eql(u8, key, "y1") or
|
|
387
|
+
std.mem.eql(u8, key, "x2") or
|
|
388
|
+
std.mem.eql(u8, key, "y2") or
|
|
389
|
+
std.mem.eql(u8, key, "durationMs") or
|
|
390
|
+
std.mem.eql(u8, key, "timeoutMs") or
|
|
391
|
+
std.mem.eql(u8, key, "direction") or
|
|
392
|
+
std.mem.eql(u8, key, "times") or
|
|
393
|
+
std.mem.eql(u8, key, "steps") or
|
|
394
|
+
std.mem.eql(u8, key, "step") or
|
|
395
|
+
std.mem.eql(u8, key, "ms");
|
|
396
|
+
}
|
|
397
|
+
|
|
355
398
|
fn parseLatitude(object: std.json.ObjectMap) !f64 {
|
|
356
399
|
const latitude = try fields.requiredF64OrError(object, "latitude", error.StepMissingLatitude, error.StepLatitudeMustBeNumber);
|
|
357
400
|
if (latitude < -90.0 or latitude > 90.0) return error.StepLatitudeOutOfRange;
|
package/src/selector.zig
CHANGED
|
@@ -3,6 +3,7 @@ const types = @import("types.zig");
|
|
|
3
3
|
|
|
4
4
|
pub const Selector = struct {
|
|
5
5
|
id: ?[]const u8 = null,
|
|
6
|
+
stable_id: ?[]const u8 = null,
|
|
6
7
|
text: ?[]const u8 = null,
|
|
7
8
|
text_contains: ?[]const u8 = null,
|
|
8
9
|
content_desc: ?[]const u8 = null,
|
|
@@ -11,6 +12,7 @@ pub const Selector = struct {
|
|
|
11
12
|
|
|
12
13
|
pub fn deinit(self: Selector, allocator: std.mem.Allocator) void {
|
|
13
14
|
if (self.id) |value| allocator.free(value);
|
|
15
|
+
if (self.stable_id) |value| allocator.free(value);
|
|
14
16
|
if (self.text) |value| allocator.free(value);
|
|
15
17
|
if (self.text_contains) |value| allocator.free(value);
|
|
16
18
|
if (self.content_desc) |value| allocator.free(value);
|
|
@@ -21,6 +23,7 @@ pub const Selector = struct {
|
|
|
21
23
|
pub fn clone(self: Selector, allocator: std.mem.Allocator) !Selector {
|
|
22
24
|
return .{
|
|
23
25
|
.id = try types.dupeOptional(allocator, self.id),
|
|
26
|
+
.stable_id = try types.dupeOptional(allocator, self.stable_id),
|
|
24
27
|
.text = try types.dupeOptional(allocator, self.text),
|
|
25
28
|
.text_contains = try types.dupeOptional(allocator, self.text_contains),
|
|
26
29
|
.content_desc = try types.dupeOptional(allocator, self.content_desc),
|
|
@@ -28,12 +31,26 @@ pub const Selector = struct {
|
|
|
28
31
|
.class_name = try types.dupeOptional(allocator, self.class_name),
|
|
29
32
|
};
|
|
30
33
|
}
|
|
34
|
+
|
|
35
|
+
pub fn hasAny(self: Selector) bool {
|
|
36
|
+
return self.id != null or
|
|
37
|
+
self.stable_id != null or
|
|
38
|
+
self.text != null or
|
|
39
|
+
self.text_contains != null or
|
|
40
|
+
self.content_desc != null or
|
|
41
|
+
self.content_desc_contains != null or
|
|
42
|
+
self.class_name != null;
|
|
43
|
+
}
|
|
31
44
|
};
|
|
32
45
|
|
|
33
46
|
pub fn matches(node: types.UiNode, wanted: Selector) bool {
|
|
47
|
+
if (!wanted.hasAny()) return false;
|
|
34
48
|
if (wanted.id) |id| {
|
|
35
49
|
if (node.resource_id == null or !std.mem.eql(u8, node.resource_id.?, id)) return false;
|
|
36
50
|
}
|
|
51
|
+
if (wanted.stable_id) |stable_id| {
|
|
52
|
+
if (!std.mem.eql(u8, node.stable_id, stable_id)) return false;
|
|
53
|
+
}
|
|
37
54
|
if (wanted.text) |text| {
|
|
38
55
|
if (node.text == null or !std.mem.eql(u8, node.text.?, text)) return false;
|
|
39
56
|
}
|
|
@@ -62,15 +79,42 @@ pub fn find(nodes: []const types.UiNode, wanted: Selector) ?types.UiNode {
|
|
|
62
79
|
pub fn parseFromJson(allocator: std.mem.Allocator, value: std.json.Value) !Selector {
|
|
63
80
|
if (value != .object) return error.SelectorMustBeObject;
|
|
64
81
|
const object = value.object;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
.
|
|
72
|
-
|
|
73
|
-
|
|
82
|
+
try rejectUnknownFields(object);
|
|
83
|
+
var parsed: Selector = .{};
|
|
84
|
+
errdefer parsed.deinit(allocator);
|
|
85
|
+
parsed.id = try stringField(allocator, object, "id");
|
|
86
|
+
const resource_id = try stringField(allocator, object, "resourceId");
|
|
87
|
+
if (parsed.id == null) {
|
|
88
|
+
parsed.id = resource_id;
|
|
89
|
+
} else if (resource_id) |resource_id_value| {
|
|
90
|
+
allocator.free(resource_id_value);
|
|
91
|
+
}
|
|
92
|
+
parsed.stable_id = try stringField(allocator, object, "stableId");
|
|
93
|
+
parsed.text = try stringField(allocator, object, "text");
|
|
94
|
+
parsed.text_contains = try stringField(allocator, object, "textContains");
|
|
95
|
+
parsed.content_desc = try stringField(allocator, object, "contentDesc");
|
|
96
|
+
parsed.content_desc_contains = try stringField(allocator, object, "contentDescContains");
|
|
97
|
+
parsed.class_name = try stringField(allocator, object, "className");
|
|
98
|
+
if (!parsed.hasAny()) return error.SelectorMustNotBeEmpty;
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn rejectUnknownFields(object: std.json.ObjectMap) !void {
|
|
103
|
+
var iterator = object.iterator();
|
|
104
|
+
while (iterator.next()) |entry| {
|
|
105
|
+
if (!isKnownField(entry.key_ptr.*)) return error.UnknownSelectorField;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
fn isKnownField(key: []const u8) bool {
|
|
110
|
+
return std.mem.eql(u8, key, "id") or
|
|
111
|
+
std.mem.eql(u8, key, "resourceId") or
|
|
112
|
+
std.mem.eql(u8, key, "stableId") or
|
|
113
|
+
std.mem.eql(u8, key, "text") or
|
|
114
|
+
std.mem.eql(u8, key, "textContains") or
|
|
115
|
+
std.mem.eql(u8, key, "contentDesc") or
|
|
116
|
+
std.mem.eql(u8, key, "contentDescContains") or
|
|
117
|
+
std.mem.eql(u8, key, "className");
|
|
74
118
|
}
|
|
75
119
|
|
|
76
120
|
fn stringField(
|
package/src/trace_json.zig
CHANGED
|
@@ -109,6 +109,10 @@ pub fn writeSelectorJson(writer: anytype, wanted: selector.Selector) !void {
|
|
|
109
109
|
try jsonField(writer, "id", value, first);
|
|
110
110
|
first = false;
|
|
111
111
|
}
|
|
112
|
+
if (wanted.stable_id) |value| {
|
|
113
|
+
try jsonField(writer, "stableId", value, first);
|
|
114
|
+
first = false;
|
|
115
|
+
}
|
|
112
116
|
if (wanted.text) |value| {
|
|
113
117
|
try jsonField(writer, "text", value, first);
|
|
114
118
|
first = false;
|
package/src/validation.zig
CHANGED
|
@@ -87,9 +87,13 @@ fn diagnoseFailure(allocator: std.mem.Allocator, content: []const u8, err: anyer
|
|
|
87
87
|
if (syntaxLocation(allocator, content)) |location| return location;
|
|
88
88
|
return switch (err) {
|
|
89
89
|
error.ScenarioMustBeObject => try pathDiagnostic(allocator, content, "$", null),
|
|
90
|
+
error.UnknownScenarioField,
|
|
91
|
+
=> try pathDiagnostic(allocator, content, "$", null),
|
|
90
92
|
error.ScenarioMissingSteps,
|
|
91
93
|
error.ScenarioStepsMustBeArray,
|
|
92
94
|
=> try pathDiagnostic(allocator, content, "$.steps", "steps"),
|
|
95
|
+
error.UnknownScenarioStepField,
|
|
96
|
+
=> try pathDiagnostic(allocator, content, "$.steps[]", null),
|
|
93
97
|
error.StepMissingAction,
|
|
94
98
|
error.StepActionMustBeString,
|
|
95
99
|
error.unknownAction,
|
|
@@ -120,6 +124,7 @@ fn diagnoseFailure(allocator: std.mem.Allocator, content: []const u8, err: anyer
|
|
|
120
124
|
error.MissingSelector,
|
|
121
125
|
error.StepMissingSelector,
|
|
122
126
|
error.SelectorMustNotBeEmpty,
|
|
127
|
+
error.UnknownSelectorField,
|
|
123
128
|
=> try pathDiagnostic(allocator, content, "$.steps[].selector", "selector"),
|
|
124
129
|
error.MissingSelectors,
|
|
125
130
|
error.StepMissingSelectors,
|
package/src/version.zig
CHANGED