zeno-mobile-runner 0.2.11 → 0.2.12
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 +14 -0
- package/FEATURES.md +1 -1
- package/README.md +4 -2
- package/clients/kotlin/README.md +1 -1
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/python/pyproject.toml +1 -1
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/typescript/package.json +1 -1
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +10 -10
- package/docs/troubleshooting.md +7 -5
- package/package.json +1 -1
- 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/scripts/install-ios-shim.sh +1 -1
- package/src/errors.zig +5 -0
- package/src/ios.zig +50 -1
- package/src/main.zig +4 -1
- package/src/runner_native.zig +17 -0
- package/src/version.zig +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to Zeno Mobile Runner are tracked here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.12 (2026-06-22)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- iOS XCTest-shim command failures now classify generated shim timeout and
|
|
12
|
+
server-exit stderr into typed ZMR errors, so traces and CLI output distinguish
|
|
13
|
+
response timeouts, server-start timeouts, build timeouts, and server exits from
|
|
14
|
+
generic device command failures.
|
|
15
|
+
- iOS native selector actions and semantic snapshot extraction now emit
|
|
16
|
+
`started` trace events before entering XCTest, making long-running simulator
|
|
17
|
+
commands visible while they are in flight.
|
|
18
|
+
- The generated app-local iOS shim removes completed request files together with
|
|
19
|
+
response files, preventing stale request buildup during long E2E sessions.
|
|
20
|
+
|
|
7
21
|
## 0.2.11 (2026-06-22)
|
|
8
22
|
|
|
9
23
|
### Fixed
|
package/FEATURES.md
CHANGED
|
@@ -142,7 +142,7 @@ state, and writes deterministic traces. It does not embed an LLM.
|
|
|
142
142
|
|
|
143
143
|
## Current Limitations
|
|
144
144
|
|
|
145
|
-
- Current release status is `0.2.
|
|
145
|
+
- Current release status is `0.2.12`, a public developer preview rather than
|
|
146
146
|
a production-stable `1.0.0`.
|
|
147
147
|
- Physical iOS log capture is still simulator-first. Physical iOS screenshots
|
|
148
148
|
are available when the XCTest/XCUIAutomation shim is configured.
|
package/README.md
CHANGED
|
@@ -196,8 +196,10 @@ comparisons against your current E2E tool, and multi-device matrices, see
|
|
|
196
196
|
| iOS physical device | Supported, validate locally | `devicectl` lifecycle plus XCTest shim; pilot on your own app/device before relying on it in CI |
|
|
197
197
|
| Cloud device farms | Not included | ZMR focuses on local and self-managed device targets in this preview |
|
|
198
198
|
|
|
199
|
-
Slow CI hardware can extend the iOS shim
|
|
200
|
-
`
|
|
199
|
+
Slow CI hardware can extend the generated iOS shim build timeout with
|
|
200
|
+
`ZMR_IOS_SHIM_BUILD_TIMEOUT_SECONDS`; `ZMR_IOS_SHIM_RESPONSE_TIMEOUT_SECONDS`
|
|
201
|
+
bounds each in-flight request, and `ZMR_IOS_SHIM_TIMEOUT_MS` remains the outer
|
|
202
|
+
process ceiling. Current release: `0.2.12` developer preview.
|
|
201
203
|
Protocol version: `2026-04-28`.
|
|
202
204
|
|
|
203
205
|
## Optional protocol clients
|
package/clients/kotlin/README.md
CHANGED
|
@@ -27,7 +27,7 @@ gradle -p clients/kotlin runFakeSession \
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
```kotlin
|
|
30
|
-
implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.
|
|
30
|
+
implementation(files("path/to/zeno-mobile-runner/clients/kotlin/build/libs/zmr-client-0.2.12.jar"))
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
```kotlin
|
package/clients/rust/Cargo.lock
CHANGED
package/clients/rust/Cargo.toml
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.
|
|
1
|
+
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.12","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
|
|
2
2
|
{"jsonrpc":"2.0","id":2,"result":[{"serial":"fake-device-1","state":"device","ready":true}]}
|
|
3
3
|
{"jsonrpc":"2.0","id":3,"result":{"sessionId":"default"}}
|
|
4
4
|
{"jsonrpc":"2.0","id":4,"result":true}
|
package/docs/protocol.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
ZMR exposes newline-delimited JSON-RPC 2.0 over stdio or localhost TCP in v1. Each request is one JSON object followed by `\n`. Each response is one JSON object followed by `\n`.
|
|
4
4
|
|
|
5
|
-
Current runner version: `0.2.
|
|
5
|
+
Current runner version: `0.2.12`.
|
|
6
6
|
|
|
7
7
|
Current protocol version: `2026-04-28`.
|
|
8
8
|
|
|
@@ -47,7 +47,7 @@ and protocol versions. The response is covered by
|
|
|
47
47
|
`schemas/inspect-output.schema.json`:
|
|
48
48
|
|
|
49
49
|
```json
|
|
50
|
-
{"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.
|
|
50
|
+
{"ok":true,"status":"ready","schemaVersion":1,"runnerVersion":"0.2.12","protocolVersion":"2026-04-28","dir":".","configPath":".zmr/config.json","configExists":true,"agentInstructionsPath":".zmr/AGENTS.md","agentInstructionsExists":true,"platforms":[{"name":"android","enabled":true,"defaultDevice":"emulator-5554","smokeScenario":".zmr/android-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-android"},{"name":"ios","enabled":true,"defaultDevice":"booted","smokeScenario":".zmr/ios-smoke.json","smokeScenarioExists":true,"traceDir":"traces/zmr-ios"}],"recommendedCommands":["zmr doctor --strict --json --config .zmr/config.json","zmr schemas --json","zmr validate --json .zmr/android-smoke.json","zmr validate --json .zmr/ios-smoke.json","zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent","zmr mcp --config .zmr/config.json --trace-dir traces/zmr-agent"],"claimsPolicy":["verify runs with trace evidence before making readiness claims","do not claim Flutter widget-tree inspection"],"limitations":["inspect is read-only and does not launch devices","autonomous crawling is not shipped; generate or edit scenarios for human review"]}
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
`zmr discover --from-trace <trace-dir> --out <scenario.json> --validate --json`
|
|
@@ -60,7 +60,7 @@ invent credentials, or commit files. The response is covered by
|
|
|
60
60
|
`schemas/discover-output.schema.json`:
|
|
61
61
|
|
|
62
62
|
```json
|
|
63
|
-
{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.
|
|
63
|
+
{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.12","protocolVersion":"2026-04-28","out":".zmr/discovered/replay-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/replay-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/replay-smoke.json","zmr run .zmr/discovered/replay-smoke.json --json --trace-dir traces/zmr-agent"]}
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
`zmr explore --from-trace <trace-dir> --out <scenario.json> --goal <goal>
|
|
@@ -71,7 +71,7 @@ launch devices, crawl the app, invent missing actions, discover credentials, or
|
|
|
71
71
|
commit files. The response is covered by `schemas/explore-output.schema.json`:
|
|
72
72
|
|
|
73
73
|
```json
|
|
74
|
-
{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.
|
|
74
|
+
{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.12","protocolVersion":"2026-04-28","goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"],"out":".zmr/discovered/login-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":6,"replay":{"enabled":true,"eventCount":4,"stepCount":3,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/login-smoke.json","name":"draft from login smoke","appId":"com.example.mobiletest","stepCount":6},"nextCommands":["zmr validate --json .zmr/discovered/login-smoke.json","zmr run .zmr/discovered/login-smoke.json --json --trace-dir traces/zmr-agent"]}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
77
|
`zmr draft --from-trace <trace-dir> --out <scenario.json> --json` is the lower
|
|
@@ -84,7 +84,7 @@ into fields, or commit files. The response is covered by
|
|
|
84
84
|
`schemas/draft-output.schema.json`:
|
|
85
85
|
|
|
86
86
|
```json
|
|
87
|
-
{"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.
|
|
87
|
+
{"ok":true,"mode":"draft","schemaVersion":1,"runnerVersion":"0.2.12","protocolVersion":"2026-04-28","out":".zmr/discovered/surface-smoke.json","traceDir":"traces/zmr-agent","sourceSnapshot":"traces/zmr-agent/artifacts/snapshot-2.json","name":"draft from login smoke","appId":"com.example.mobiletest","selectorCount":2,"stepCount":4,"replay":{"enabled":false,"eventCount":0,"stepCount":0,"skippedEventCount":0},"warnings":["draft requires human review before commit"],"nextCommands":["zmr validate --json .zmr/discovered/surface-smoke.json","zmr run .zmr/discovered/surface-smoke.json --json --trace-dir traces/zmr-agent"]}
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
`zmr draft --include-actions` additionally parses `events.jsonl` and prepends
|
|
@@ -214,7 +214,7 @@ installers, setup scripts, and generated clients. The response is covered by
|
|
|
214
214
|
`schemas/version-output.schema.json`:
|
|
215
215
|
|
|
216
216
|
```json
|
|
217
|
-
{"name":"zmr","version":"0.2.
|
|
217
|
+
{"name":"zmr","version":"0.2.12","protocolVersion":"2026-04-28","minimumCompatibleProtocolVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"}
|
|
218
218
|
```
|
|
219
219
|
|
|
220
220
|
## Capabilities Output Contract
|
|
@@ -226,7 +226,7 @@ and method inventory for JSON-RPC clients. The result object is covered by
|
|
|
226
226
|
iOS simulator, or physical iOS workflows are available.
|
|
227
227
|
|
|
228
228
|
```json
|
|
229
|
-
{"name":"zmr","version":"0.2.
|
|
229
|
+
{"name":"zmr","version":"0.2.12","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}
|
|
230
230
|
```
|
|
231
231
|
|
|
232
232
|
## Doctor Output Contract
|
|
@@ -432,7 +432,7 @@ Request:
|
|
|
432
432
|
Response:
|
|
433
433
|
|
|
434
434
|
```json
|
|
435
|
-
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.
|
|
435
|
+
{"jsonrpc":"2.0","id":1,"result":{"name":"zmr","version":"0.2.12","protocolVersion":"2026-04-28","protocol":{"version":"2026-04-28","minimumCompatibleVersion":"2026-04-28","stability":"dev-preview","breakingChangePolicy":"version-and-changelog"},"platforms":["android","ios"],"platformSupport":{"android":{"status":"supported","deviceTypes":["emulator","physical"],"automation":["adb","uiautomator","android-shim"]},"ios":{"status":"supported","deviceTypes":["simulator","physical"],"automation":["simctl","devicectl","xctest-shim"],"physicalDevices":true}},"iosPreview":false,"transports":["stdio","tcp"],"methods":["runner.capabilities","device.list","session.create","session.close","app.install","app.launch","app.stop","app.openLink","app.clearState","observe.snapshot","observe.semanticSnapshot","ui.tap","ui.type","ui.eraseText","ui.hideKeyboard","ui.swipe","ui.pressBack","ui.scrollUntilVisible","wait.until","wait.any","wait.gone","assert.visible","assert.notVisible","assert.healthy","scenario.validate","trace.events","trace.explore","trace.discover","trace.explain","trace.export"]}}
|
|
436
436
|
```
|
|
437
437
|
|
|
438
438
|
### `trace.events`
|
|
@@ -514,7 +514,7 @@ Request:
|
|
|
514
514
|
Response:
|
|
515
515
|
|
|
516
516
|
```json
|
|
517
|
-
{"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.
|
|
517
|
+
{"jsonrpc":"2.0","id":25,"result":{"ok":true,"mode":"discover","schemaVersion":1,"runnerVersion":"0.2.12","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-smoke.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-smoke.json","name":"agent smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-smoke.json","zmr run .zmr/discovered/agent-smoke.json --json --trace-dir traces/agent-session"]}}
|
|
518
518
|
```
|
|
519
519
|
|
|
520
520
|
Without `--trace-dir`, it returns `ok: false` with `traceDir: null`. Generated
|
|
@@ -537,7 +537,7 @@ Request:
|
|
|
537
537
|
Response:
|
|
538
538
|
|
|
539
539
|
```json
|
|
540
|
-
{"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.
|
|
540
|
+
{"jsonrpc":"2.0","id":27,"result":{"ok":true,"mode":"explore","schemaVersion":1,"runnerVersion":"0.2.12","protocolVersion":"2026-04-28","out":".zmr/discovered/agent-goal.json","traceDir":"traces/agent-session","sourceSnapshot":"traces/agent-session/artifacts/snapshot-1.json","name":"agent goal smoke","appId":"com.example.mobiletest","selectorCount":1,"stepCount":4,"replay":{"enabled":true,"eventCount":2,"stepCount":1,"skippedEventCount":1},"warnings":["draft requires human review before commit"],"validated":true,"validation":{"ok":true,"path":".zmr/discovered/agent-goal.json","name":"agent goal smoke","appId":"com.example.mobiletest","stepCount":4},"nextCommands":["zmr validate --json .zmr/discovered/agent-goal.json","zmr run .zmr/discovered/agent-goal.json --json --trace-dir traces/agent-session"],"goal":"find a stable login smoke","autonomous":false,"reviewRequired":true,"guardrails":["writes from existing trace evidence only","does not crawl the app","does not discover credentials or secrets","requires human review before commit"]}}
|
|
541
541
|
```
|
|
542
542
|
|
|
543
543
|
Without `--trace-dir`, it returns `ok: false` with `traceDir: null`.
|
package/docs/troubleshooting.md
CHANGED
|
@@ -188,11 +188,13 @@ app targets. Pass `--project` explicitly for still-ambiguous multi-project
|
|
|
188
188
|
workspaces. Run with `--ios-shim ./.zmr/ios-shim` or set
|
|
189
189
|
`tools.iosShimPath` in `.zmr/config.json`.
|
|
190
190
|
|
|
191
|
-
A clean prebuild can push the shim's first `build-for-testing` through
|
|
192
|
-
native dependency compile.
|
|
193
|
-
hardware, raise the ceiling with
|
|
194
|
-
|
|
195
|
-
|
|
191
|
+
A clean prebuild can push the generated shim's first `build-for-testing` through
|
|
192
|
+
a full native dependency compile. The app-local shim waits up to 90 minutes by
|
|
193
|
+
default; on slower CI hardware, raise the build ceiling with
|
|
194
|
+
`ZMR_IOS_SHIM_BUILD_TIMEOUT_SECONDS=10800` for three hours. Individual shim
|
|
195
|
+
requests are bounded separately by `ZMR_IOS_SHIM_RESPONSE_TIMEOUT_SECONDS`
|
|
196
|
+
(default 180 seconds), while `ZMR_IOS_SHIM_TIMEOUT_MS` remains the outer ZMR
|
|
197
|
+
process ceiling for the whole shim command.
|
|
196
198
|
|
|
197
199
|
If a real iOS run fails with CoreSimulator or Xcode cache errors such as
|
|
198
200
|
`Operation not permitted`, `CoreSimulatorService connection became invalid`, or
|
package/package.json
CHANGED
|
Binary file
|
package/prebuilds/darwin-x64/zmr
CHANGED
|
Binary file
|
|
Binary file
|
package/prebuilds/linux-x64/zmr
CHANGED
|
Binary file
|
package/src/errors.zig
CHANGED
|
@@ -58,7 +58,12 @@ pub fn classify(err: anyerror) PublicError {
|
|
|
58
58
|
error.AssertionFailed => .{ .code = "runner.assertion_failed", .message = "assertion failed" },
|
|
59
59
|
error.SelectorNotFound => .{ .code = "runner.selector_not_found", .message = "selector not found" },
|
|
60
60
|
error.CommandFailed => .{ .code = "device.command_failed", .message = "device command failed" },
|
|
61
|
+
error.CommandTimedOut => .{ .code = "device.command_timed_out", .message = "device command timed out" },
|
|
61
62
|
error.IosXCTestShimRequired => .{ .code = "ios.xctest_shim_required", .message = "iOS selector interaction requires the XCTest shim" },
|
|
63
|
+
error.IosXCTestShimResponseTimedOut => .{ .code = "ios.xctest_shim_response_timeout", .message = "iOS XCTest shim response timed out" },
|
|
64
|
+
error.IosXCTestShimStartTimedOut => .{ .code = "ios.xctest_shim_start_timeout", .message = "iOS XCTest shim server startup timed out" },
|
|
65
|
+
error.IosXCTestShimBuildTimedOut => .{ .code = "ios.xctest_shim_build_timeout", .message = "iOS XCTest shim build timed out" },
|
|
66
|
+
error.IosXCTestShimServerExited => .{ .code = "ios.xctest_shim_server_exited", .message = "iOS XCTest shim server exited" },
|
|
62
67
|
else => .{ .code = "internal.error", .message = @errorName(err) },
|
|
63
68
|
};
|
|
64
69
|
}
|
package/src/ios.zig
CHANGED
|
@@ -232,6 +232,7 @@ pub const IosDevice = struct {
|
|
|
232
232
|
errdefer self.allocator.free(active_package);
|
|
233
233
|
var shim_viewport: ?types.Viewport = null;
|
|
234
234
|
const nodes = if (self.shim_path != null) blk: {
|
|
235
|
+
if (writer) |tw| try self.recordSnapshotSemanticStarted(tw);
|
|
235
236
|
const shim_snapshot = self.snapshotFromShim() catch |err| {
|
|
236
237
|
if (screenshot_artifact == null) return err;
|
|
237
238
|
if (writer) |tw| try self.recordSnapshotSemanticFailure(tw, screenshot_artifact.?, err);
|
|
@@ -291,6 +292,11 @@ pub const IosDevice = struct {
|
|
|
291
292
|
return try ios_shim.parseSnapshotResponse(self.allocator, response);
|
|
292
293
|
}
|
|
293
294
|
|
|
295
|
+
fn recordSnapshotSemanticStarted(self: *IosDevice, writer: *trace.TraceWriter) !void {
|
|
296
|
+
try writer.recordEvent("observe.snapshot.semanticExtraction", "{\"status\":\"started\",\"source\":\"ios-xctest-shim\"}");
|
|
297
|
+
_ = self;
|
|
298
|
+
}
|
|
299
|
+
|
|
294
300
|
fn recordSnapshotSemanticFailure(self: *IosDevice, writer: *trace.TraceWriter, screenshot_artifact: []const u8, err: anyerror) !void {
|
|
295
301
|
var payload: std.Io.Writer.Allocating = .init(writer.allocator);
|
|
296
302
|
defer payload.deinit();
|
|
@@ -378,7 +384,7 @@ pub const IosDevice = struct {
|
|
|
378
384
|
stdio.sleepNs(shim_bootstrap_retry_delay_ms * std.time.ns_per_ms);
|
|
379
385
|
continue;
|
|
380
386
|
}
|
|
381
|
-
return
|
|
387
|
+
return classifyShimCommandFailure(result);
|
|
382
388
|
};
|
|
383
389
|
return try self.allocator.dupe(u8, result.stdout);
|
|
384
390
|
}
|
|
@@ -422,6 +428,27 @@ fn isTransientShimBootstrapFailure(result: command.ExecResult) bool {
|
|
|
422
428
|
std.mem.indexOf(u8, result.stderr, "operation never finished bootstrapping") != null;
|
|
423
429
|
}
|
|
424
430
|
|
|
431
|
+
fn classifyShimCommandFailure(result: command.ExecResult) anyerror {
|
|
432
|
+
if (result.timed_out) return error.CommandTimedOut;
|
|
433
|
+
if (std.mem.indexOf(u8, result.stderr, "timed out waiting for iOS shim response") != null) {
|
|
434
|
+
return error.IosXCTestShimResponseTimedOut;
|
|
435
|
+
}
|
|
436
|
+
if (std.mem.indexOf(u8, result.stderr, "timed out waiting for iOS shim server readiness") != null) {
|
|
437
|
+
return error.IosXCTestShimStartTimedOut;
|
|
438
|
+
}
|
|
439
|
+
if (std.mem.indexOf(u8, result.stderr, "timed out waiting for iOS shim build-for-testing") != null) {
|
|
440
|
+
return error.IosXCTestShimBuildTimedOut;
|
|
441
|
+
}
|
|
442
|
+
if (std.mem.indexOf(u8, result.stderr, "iOS shim server exited before it became ready") != null or
|
|
443
|
+
std.mem.indexOf(u8, result.stderr, "iOS shim server exited while waiting for response") != null or
|
|
444
|
+
std.mem.indexOf(u8, result.stderr, "Early unexpected exit") != null or
|
|
445
|
+
std.mem.indexOf(u8, result.stderr, "operation never finished bootstrapping") != null)
|
|
446
|
+
{
|
|
447
|
+
return error.IosXCTestShimServerExited;
|
|
448
|
+
}
|
|
449
|
+
return error.CommandFailed;
|
|
450
|
+
}
|
|
451
|
+
|
|
425
452
|
fn shimTimeoutMs() u64 {
|
|
426
453
|
return parseShimTimeoutMs(stdio.getenv(shim_timeout_env));
|
|
427
454
|
}
|
|
@@ -523,3 +550,25 @@ test "ios xctest shim timeout env override" {
|
|
|
523
550
|
try std.testing.expectEqual(@as(u64, default_shim_timeout_ms), parseShimTimeoutMs("not-a-number"));
|
|
524
551
|
try std.testing.expectEqual(@as(u64, default_shim_timeout_ms), parseShimTimeoutMs("0"));
|
|
525
552
|
}
|
|
553
|
+
|
|
554
|
+
test "ios xctest shim command failures classify generated shim timeout causes" {
|
|
555
|
+
try expectClassifiedShimFailure(error.IosXCTestShimResponseTimedOut, "timed out waiting for iOS shim response 12345\n");
|
|
556
|
+
try expectClassifiedShimFailure(error.IosXCTestShimStartTimedOut, "timed out waiting for iOS shim server readiness\n");
|
|
557
|
+
try expectClassifiedShimFailure(error.IosXCTestShimBuildTimedOut, "timed out waiting for iOS shim build-for-testing after 5400s\n");
|
|
558
|
+
try expectClassifiedShimFailure(error.IosXCTestShimServerExited, "iOS shim server exited while waiting for response 12345\n");
|
|
559
|
+
try expectClassifiedShimFailure(error.CommandFailed, "xcodebuild failed for another reason\n");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
fn expectClassifiedShimFailure(expected: anyerror, stderr: []const u8) !void {
|
|
563
|
+
const allocator = std.testing.allocator;
|
|
564
|
+
const stdout_owned = try allocator.dupe(u8, "");
|
|
565
|
+
defer allocator.free(stdout_owned);
|
|
566
|
+
const stderr_owned = try allocator.dupe(u8, stderr);
|
|
567
|
+
defer allocator.free(stderr_owned);
|
|
568
|
+
const result = command.ExecResult{
|
|
569
|
+
.stdout = stdout_owned,
|
|
570
|
+
.stderr = stderr_owned,
|
|
571
|
+
.term = .{ .exited = 1 },
|
|
572
|
+
};
|
|
573
|
+
try std.testing.expectEqual(expected, classifyShimCommandFailure(result));
|
|
574
|
+
}
|
package/src/main.zig
CHANGED
|
@@ -94,7 +94,10 @@ fn writeTopLevelError(err: anyerror) void {
|
|
|
94
94
|
defer stderr_io.deinit();
|
|
95
95
|
const stderr = stderr_io.writer();
|
|
96
96
|
stderr.print("error[{s}]: {s}\n", .{ public.code, public.message }) catch {};
|
|
97
|
-
if (err == error.CommandFailed
|
|
97
|
+
if (err == error.CommandFailed or err == error.CommandTimedOut or
|
|
98
|
+
err == error.IosXCTestShimResponseTimedOut or err == error.IosXCTestShimStartTimedOut or
|
|
99
|
+
err == error.IosXCTestShimBuildTimedOut or err == error.IosXCTestShimServerExited)
|
|
100
|
+
{
|
|
98
101
|
stderr.writeAll("hint: run `zmr doctor --json` for setup diagnostics.\n") catch {};
|
|
99
102
|
}
|
|
100
103
|
if (err == error.unknownFlag) {
|
package/src/runner_native.zig
CHANGED
|
@@ -9,6 +9,7 @@ pub fn tryTapSelector(
|
|
|
9
9
|
settle_ms: u64,
|
|
10
10
|
) !bool {
|
|
11
11
|
if (!@hasDecl(@TypeOf(device.*), "tapBySelector")) return false;
|
|
12
|
+
if (writer) |tw| try recordSelectorActionStarted(tw, "ui.tap", wanted);
|
|
12
13
|
const tapped = device.tapBySelector(wanted) catch |err| {
|
|
13
14
|
if (writer) |tw| try recordSelectorActionFailure(tw, "ui.tap", wanted, err);
|
|
14
15
|
return err;
|
|
@@ -27,6 +28,7 @@ pub fn tryTypeTextSelector(
|
|
|
27
28
|
settle_ms: u64,
|
|
28
29
|
) !bool {
|
|
29
30
|
if (!@hasDecl(@TypeOf(device.*), "typeTextBySelector")) return false;
|
|
31
|
+
if (writer) |tw| try recordSelectorActionStarted(tw, "ui.type", wanted);
|
|
30
32
|
const typed = device.typeTextBySelector(wanted, text) catch |err| {
|
|
31
33
|
if (writer) |tw| try recordSelectorActionFailure(tw, "ui.type", wanted, err);
|
|
32
34
|
return err;
|
|
@@ -45,6 +47,7 @@ pub fn tryEraseTextSelector(
|
|
|
45
47
|
settle_ms: u64,
|
|
46
48
|
) !bool {
|
|
47
49
|
if (!@hasDecl(@TypeOf(device.*), "eraseTextBySelector")) return false;
|
|
50
|
+
if (writer) |tw| try recordSelectorActionStarted(tw, "ui.eraseText", wanted);
|
|
48
51
|
const erased = device.eraseTextBySelector(wanted, max_chars) catch |err| {
|
|
49
52
|
if (writer) |tw| try recordSelectorActionFailure(tw, "ui.eraseText", wanted, err);
|
|
50
53
|
return err;
|
|
@@ -55,6 +58,20 @@ pub fn tryEraseTextSelector(
|
|
|
55
58
|
return true;
|
|
56
59
|
}
|
|
57
60
|
|
|
61
|
+
fn recordSelectorActionStarted(
|
|
62
|
+
tw: *trace.TraceWriter,
|
|
63
|
+
kind: []const u8,
|
|
64
|
+
wanted: selector.Selector,
|
|
65
|
+
) !void {
|
|
66
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
67
|
+
defer payload.deinit();
|
|
68
|
+
const writer = &payload.writer;
|
|
69
|
+
try writer.writeAll("{\"status\":\"started\",\"strategy\":\"nativeSelector\",\"selector\":");
|
|
70
|
+
try trace.writeSelectorJson(writer, wanted);
|
|
71
|
+
try writer.writeAll("}");
|
|
72
|
+
try tw.recordEvent(kind, writer.buffered());
|
|
73
|
+
}
|
|
74
|
+
|
|
58
75
|
fn recordSelectorAction(
|
|
59
76
|
tw: *trace.TraceWriter,
|
|
60
77
|
kind: []const u8,
|
package/src/version.zig
CHANGED