zeno-mobile-runner 0.2.6 → 0.2.8
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 +19 -1
- package/FEATURES.md +1 -1
- package/README.md +1 -1
- package/build.zig.zon +1 -1
- 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/npm.md +8 -0
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol.md +10 -10
- 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/src/cli_devices.zig +9 -4
- package/src/cli_discover.zig +1 -0
- package/src/cli_doctor.zig +1 -0
- package/src/cli_draft.zig +6 -6
- package/src/cli_explore.zig +1 -0
- package/src/cli_import.zig +9 -5
- package/src/cli_info.zig +13 -5
- package/src/cli_init.zig +19 -11
- package/src/cli_inspect.zig +1 -0
- package/src/cli_run.zig +1 -0
- package/src/cli_trace.zig +8 -2
- package/src/cli_validate.zig +1 -0
- package/src/command.zig +40 -15
- package/src/ios.zig +9 -2
- package/src/main.zig +1 -0
- package/src/runner.zig +183 -0
- package/src/runner_events.zig +17 -0
- package/src/runner_waits.zig +97 -23
- package/src/stdio.zig +17 -2
- package/src/test_io.zig +60 -0
- package/src/version.zig +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to Zeno Mobile Runner are tracked here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.8 (2026-06-17)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- iOS native selector waits now cap each XCTest query to the remaining scenario
|
|
12
|
+
step timeout and retry transient query command timeouts. Missing selectors can
|
|
13
|
+
no longer outlive the scenario `timeoutMs` by waiting on the shim's longer
|
|
14
|
+
cold-start command timeout.
|
|
15
|
+
|
|
16
|
+
## 0.2.7 (2026-06-15)
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- `assertHealthy` now uses native iOS selector probes for known crash and error
|
|
21
|
+
overlays before falling back to a broad accessibility snapshot. This avoids
|
|
22
|
+
false `CommandFailed` failures when XCTest broad snapshot enumeration races
|
|
23
|
+
with animated or reloading screens, while preserving direct overlay detection.
|
|
24
|
+
|
|
7
25
|
## 0.2.6 (2026-06-15)
|
|
8
26
|
|
|
9
27
|
### Fixed
|
|
@@ -30,7 +48,7 @@ All notable changes to Zeno Mobile Runner are tracked here.
|
|
|
30
48
|
- Extended the iOS simulator `openLink` interruption sweep to cover Expo
|
|
31
49
|
dev-client deep-link chooser sheets that appear more than six seconds after
|
|
32
50
|
`simctl openurl` returns. The sweep remains bounded, but now covers the
|
|
33
|
-
delayed chooser timing observed in
|
|
51
|
+
delayed chooser timing observed in app auth smoke runs.
|
|
34
52
|
|
|
35
53
|
## 0.2.3 (2026-06-15)
|
|
36
54
|
|
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.8`, 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
|
@@ -197,7 +197,7 @@ comparisons against your current E2E tool, and multi-device matrices, see
|
|
|
197
197
|
| Cloud device farms | Not included | ZMR focuses on local and self-managed device targets in this preview |
|
|
198
198
|
|
|
199
199
|
Slow CI hardware can extend the iOS shim cold-build timeout with
|
|
200
|
-
`ZMR_IOS_SHIM_TIMEOUT_MS`. Current release: `0.2.
|
|
200
|
+
`ZMR_IOS_SHIM_TIMEOUT_MS`. Current release: `0.2.8` developer preview.
|
|
201
201
|
Protocol version: `2026-04-28`.
|
|
202
202
|
|
|
203
203
|
## Optional protocol clients
|
package/build.zig.zon
CHANGED
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.8.jar"))
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
```kotlin
|
package/clients/rust/Cargo.lock
CHANGED
package/clients/rust/Cargo.toml
CHANGED
package/docs/npm.md
CHANGED
|
@@ -431,6 +431,14 @@ Publish the generated tarball from `dist/`:
|
|
|
431
431
|
npm publish ./dist/zeno-mobile-runner-<version>.tgz --access public
|
|
432
432
|
```
|
|
433
433
|
|
|
434
|
+
If npm returns `EOTP`, the account or organization requires a TOTP-style
|
|
435
|
+
one-time password for this publish command. The browser/passkey login flow can
|
|
436
|
+
authenticate the local CLI session, but `npm publish` itself only accepts the
|
|
437
|
+
publish-time second factor through `--otp`. Prefer the tagged trusted-publishing
|
|
438
|
+
workflow for normal releases; otherwise enter the TOTP locally or use a granular
|
|
439
|
+
automation token configured to bypass 2FA. Do not send OTPs or tokens through
|
|
440
|
+
issue comments, chat, or commit history.
|
|
441
|
+
|
|
434
442
|
If npm returns `E403` with a two-factor authentication message, the account or
|
|
435
443
|
organization requires either a current interactive 2FA challenge or a granular
|
|
436
444
|
automation token configured to bypass 2FA. For local passkey accounts, rerun
|
|
@@ -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.8","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.8`.
|
|
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.8","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.8","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.8","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.8","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.8","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.8","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.8","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.8","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.8","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/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/cli_devices.zig
CHANGED
|
@@ -48,11 +48,16 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void
|
|
|
48
48
|
stdout_io.init(.stdout());
|
|
49
49
|
defer stdout_io.deinit();
|
|
50
50
|
const stdout = stdout_io.writer();
|
|
51
|
-
if (json)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
try stdout.print("{s}
|
|
51
|
+
if (json) {
|
|
52
|
+
try device_registry.writeJson(stdout, registryPlatform(platform), devices);
|
|
53
|
+
} else if (devices.len == 0) {
|
|
54
|
+
try stdout.print("No {s} devices found.\n", .{@tagName(platform)});
|
|
55
|
+
} else {
|
|
56
|
+
for (devices) |device| {
|
|
57
|
+
try stdout.print("{s}\t{s}\n", .{ device.serial, device.state });
|
|
58
|
+
}
|
|
55
59
|
}
|
|
60
|
+
try stdout_io.flush();
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
fn parsePlatform(value: []const u8) !run_options.Platform {
|
package/src/cli_discover.zig
CHANGED
|
@@ -106,6 +106,7 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void
|
|
|
106
106
|
try cli_output.writeShellArg(stdout, discovered.summary.draft.out_path);
|
|
107
107
|
try stdout.writeAll("\n");
|
|
108
108
|
}
|
|
109
|
+
try stdout_io.flush();
|
|
109
110
|
if (!discovered.summary.ok) std.process.exit(1);
|
|
110
111
|
}
|
|
111
112
|
|
package/src/cli_doctor.zig
CHANGED
|
@@ -114,5 +114,6 @@ fn runParsed(allocator: std.mem.Allocator, parsed: ParsedArgs) !void {
|
|
|
114
114
|
} else {
|
|
115
115
|
try cli_output.writeDoctorText(stdout, config_check, checks);
|
|
116
116
|
}
|
|
117
|
+
try stdout_io.flush();
|
|
117
118
|
if (parsed.strict and !cli_output.doctorChecksHealthy(config_check, checks)) std.process.exit(1);
|
|
118
119
|
}
|
package/src/cli_draft.zig
CHANGED
|
@@ -125,13 +125,13 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void
|
|
|
125
125
|
const stdout = stdout_io.writer();
|
|
126
126
|
if (parsed.json) {
|
|
127
127
|
try writeJson(stdout, draft.summary);
|
|
128
|
-
|
|
128
|
+
} else {
|
|
129
|
+
try stdout.print("wrote {s}\n", .{draft.summary.out_path});
|
|
130
|
+
try stdout.writeAll("next: zmr validate --json ");
|
|
131
|
+
try cli_output.writeShellArg(stdout, draft.summary.out_path);
|
|
132
|
+
try stdout.writeAll("\n");
|
|
129
133
|
}
|
|
130
|
-
|
|
131
|
-
try stdout.print("wrote {s}\n", .{draft.summary.out_path});
|
|
132
|
-
try stdout.writeAll("next: zmr validate --json ");
|
|
133
|
-
try cli_output.writeShellArg(stdout, draft.summary.out_path);
|
|
134
|
-
try stdout.writeAll("\n");
|
|
134
|
+
try stdout_io.flush();
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
pub fn draftFromTrace(allocator: std.mem.Allocator, parsed: ParsedArgs) !OwnedDraft {
|
package/src/cli_explore.zig
CHANGED
|
@@ -99,6 +99,7 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void
|
|
|
99
99
|
try cli_output.writeShellArg(stdout, explored.discovered.summary.draft.out_path);
|
|
100
100
|
try stdout.writeAll("\n");
|
|
101
101
|
}
|
|
102
|
+
try stdout_io.flush();
|
|
102
103
|
if (!explored.summary.ok) std.process.exit(1);
|
|
103
104
|
}
|
|
104
105
|
|
package/src/cli_import.zig
CHANGED
|
@@ -82,9 +82,13 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void
|
|
|
82
82
|
stdout_io.init(.stdout());
|
|
83
83
|
defer stdout_io.deinit();
|
|
84
84
|
const stdout = stdout_io.writer();
|
|
85
|
-
if (parsed.json)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
85
|
+
if (parsed.json) {
|
|
86
|
+
try cli_output.writeImportJson(stdout, parsed.format, parsed.source_path, result);
|
|
87
|
+
} else {
|
|
88
|
+
try stdout.print("wrote {s}\n", .{result.out_path});
|
|
89
|
+
try stdout.writeAll("next: zmr validate ");
|
|
90
|
+
try cli_output.writeShellArg(stdout, result.out_path);
|
|
91
|
+
try stdout.writeAll("\n");
|
|
92
|
+
}
|
|
93
|
+
try stdout_io.flush();
|
|
90
94
|
}
|
package/src/cli_info.zig
CHANGED
|
@@ -22,8 +22,12 @@ pub fn runVersion(allocator: std.mem.Allocator, args: *std.process.Args.Iterator
|
|
|
22
22
|
stdout_io.init(.stdout());
|
|
23
23
|
defer stdout_io.deinit();
|
|
24
24
|
const stdout = stdout_io.writer();
|
|
25
|
-
if (json)
|
|
26
|
-
|
|
25
|
+
if (json) {
|
|
26
|
+
try version.writeJson(stdout);
|
|
27
|
+
} else {
|
|
28
|
+
try version.writePlain(stdout);
|
|
29
|
+
}
|
|
30
|
+
try stdout_io.flush();
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
pub fn runSchemas(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void {
|
|
@@ -32,10 +36,14 @@ pub fn runSchemas(allocator: std.mem.Allocator, args: *std.process.Args.Iterator
|
|
|
32
36
|
stdout_io.init(.stdout());
|
|
33
37
|
defer stdout_io.deinit();
|
|
34
38
|
const stdout = stdout_io.writer();
|
|
35
|
-
if (json)
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
if (json) {
|
|
40
|
+
try schema_registry.writeJson(stdout);
|
|
41
|
+
} else {
|
|
42
|
+
for (schema_registry.all()) |schema_info| {
|
|
43
|
+
try stdout.print("{s}\t{s}\n", .{ schema_info.name, schema_info.path });
|
|
44
|
+
}
|
|
38
45
|
}
|
|
46
|
+
try stdout_io.flush();
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
fn parseArgIterator(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !bool {
|
package/src/cli_init.zig
CHANGED
|
@@ -57,20 +57,28 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void
|
|
|
57
57
|
const stdout = stdout_io.writer();
|
|
58
58
|
if (parsed.app_scaffold) {
|
|
59
59
|
try scaffold.writeAppScaffold(allocator, parsed.dir, parsed.app_id, parsed.force);
|
|
60
|
-
if (parsed.json)
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
if (parsed.json) {
|
|
61
|
+
try cli_output.writeInitAppJson(stdout, parsed.dir, parsed.app_id);
|
|
62
|
+
} else {
|
|
63
|
+
for (scaffold.app_created_files) |path| {
|
|
64
|
+
try stdout.print("created {s}/{s}\n", .{ parsed.dir, path });
|
|
65
|
+
}
|
|
66
|
+
try stdout.writeAll("next: zmr doctor --strict --json --config ");
|
|
67
|
+
try cli_output.writeJoinedPathShellArg(stdout, parsed.dir, scaffold.app_config_file);
|
|
68
|
+
try stdout.writeAll("\n");
|
|
63
69
|
}
|
|
64
|
-
try
|
|
65
|
-
try cli_output.writeJoinedPathShellArg(stdout, parsed.dir, scaffold.app_config_file);
|
|
66
|
-
try stdout.writeAll("\n");
|
|
70
|
+
try stdout_io.flush();
|
|
67
71
|
return;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
try scaffold.writeStarterScenario(allocator, parsed.path, parsed.app_id, parsed.force);
|
|
71
|
-
if (parsed.json)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
if (parsed.json) {
|
|
76
|
+
try cli_output.writeInitScenarioJson(stdout, parsed.path, parsed.app_id);
|
|
77
|
+
} else {
|
|
78
|
+
try stdout.print("created {s}\n", .{parsed.path});
|
|
79
|
+
try stdout.writeAll("next: zmr validate ");
|
|
80
|
+
try cli_output.writeShellArg(stdout, parsed.path);
|
|
81
|
+
try stdout.writeAll("\n");
|
|
82
|
+
}
|
|
83
|
+
try stdout_io.flush();
|
|
76
84
|
}
|
package/src/cli_inspect.zig
CHANGED
package/src/cli_run.zig
CHANGED
package/src/cli_trace.zig
CHANGED
|
@@ -120,6 +120,7 @@ pub fn runReport(allocator: std.mem.Allocator, args: *std.process.Args.Iterator)
|
|
|
120
120
|
try report.writeJUnitReport(allocator, parsed.input_path, junit_path);
|
|
121
121
|
try stdout.print("wrote {s}\n", .{junit_path});
|
|
122
122
|
}
|
|
123
|
+
try stdout_io.flush();
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
pub fn runExplain(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void {
|
|
@@ -132,8 +133,12 @@ pub fn runExplain(allocator: std.mem.Allocator, args: *std.process.Args.Iterator
|
|
|
132
133
|
stdout_io.init(.stdout());
|
|
133
134
|
defer stdout_io.deinit();
|
|
134
135
|
const stdout = stdout_io.writer();
|
|
135
|
-
if (parsed.json)
|
|
136
|
-
|
|
136
|
+
if (parsed.json) {
|
|
137
|
+
try report.writeTraceExplanationJson(allocator, parsed.trace_dir.?, stdout);
|
|
138
|
+
} else {
|
|
139
|
+
try report.writeTraceExplanation(allocator, parsed.trace_dir.?, stdout);
|
|
140
|
+
}
|
|
141
|
+
try stdout_io.flush();
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
pub fn runExport(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void {
|
|
@@ -151,4 +156,5 @@ pub fn runExport(allocator: std.mem.Allocator, args: *std.process.Args.Iterator)
|
|
|
151
156
|
defer stdout_io.deinit();
|
|
152
157
|
const stdout = stdout_io.writer();
|
|
153
158
|
try stdout.print("wrote {s}\n", .{parsed.out_path.?});
|
|
159
|
+
try stdout_io.flush();
|
|
154
160
|
}
|
package/src/cli_validate.zig
CHANGED
package/src/command.zig
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const builtin = @import("builtin");
|
|
1
2
|
const std = @import("std");
|
|
2
3
|
const stdio = @import("stdio.zig");
|
|
3
4
|
|
|
@@ -61,6 +62,7 @@ pub fn runWithInputTimeout(
|
|
|
61
62
|
.stdin = .pipe,
|
|
62
63
|
.stdout = .pipe,
|
|
63
64
|
.stderr = .pipe,
|
|
65
|
+
.pgid = processGroupForTimeout(timeout_ms),
|
|
64
66
|
});
|
|
65
67
|
defer if (child.id != null) child.kill(stdio.io());
|
|
66
68
|
|
|
@@ -84,21 +86,16 @@ pub fn runWithTimeout(
|
|
|
84
86
|
) !ExecResult {
|
|
85
87
|
if (timeout_ms == 0) return run(allocator, argv, max_output_bytes);
|
|
86
88
|
|
|
87
|
-
|
|
89
|
+
var child = try std.process.spawn(stdio.io(), .{
|
|
88
90
|
.argv = argv,
|
|
89
|
-
.
|
|
90
|
-
.
|
|
91
|
-
.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return
|
|
97
|
-
.stdout = result.stdout,
|
|
98
|
-
.stderr = result.stderr,
|
|
99
|
-
.term = result.term,
|
|
100
|
-
.timed_out = false,
|
|
101
|
-
};
|
|
91
|
+
.stdin = .ignore,
|
|
92
|
+
.stdout = .pipe,
|
|
93
|
+
.stderr = .pipe,
|
|
94
|
+
.pgid = processGroupForTimeout(timeout_ms),
|
|
95
|
+
});
|
|
96
|
+
defer if (child.id != null) child.kill(stdio.io());
|
|
97
|
+
|
|
98
|
+
return try collectSpawnedOutput(allocator, &child, max_output_bytes, timeoutForMs(timeout_ms));
|
|
102
99
|
}
|
|
103
100
|
|
|
104
101
|
fn collectSpawnedOutput(
|
|
@@ -121,7 +118,7 @@ fn collectSpawnedOutput(
|
|
|
121
118
|
} else |err| switch (err) {
|
|
122
119
|
error.EndOfStream => {},
|
|
123
120
|
error.Timeout => {
|
|
124
|
-
child
|
|
121
|
+
terminateTimedOutChild(child);
|
|
125
122
|
return timedOutResult(allocator);
|
|
126
123
|
},
|
|
127
124
|
else => |actual| return actual,
|
|
@@ -143,6 +140,34 @@ fn collectSpawnedOutput(
|
|
|
143
140
|
};
|
|
144
141
|
}
|
|
145
142
|
|
|
143
|
+
fn processGroupForTimeout(timeout_ms: u64) ?std.posix.pid_t {
|
|
144
|
+
if (timeout_ms == 0) return null;
|
|
145
|
+
return switch (builtin.os.tag) {
|
|
146
|
+
.linux,
|
|
147
|
+
.macos,
|
|
148
|
+
.ios,
|
|
149
|
+
.tvos,
|
|
150
|
+
.watchos,
|
|
151
|
+
.visionos,
|
|
152
|
+
.freebsd,
|
|
153
|
+
.netbsd,
|
|
154
|
+
.openbsd,
|
|
155
|
+
.haiku,
|
|
156
|
+
.illumos,
|
|
157
|
+
=> 0,
|
|
158
|
+
else => null,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn terminateTimedOutChild(child: *std.process.Child) void {
|
|
163
|
+
if (child.id) |pid| {
|
|
164
|
+
if (processGroupForTimeout(1) != null and pid > 0) {
|
|
165
|
+
std.posix.kill(-pid, .TERM) catch {};
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
child.kill(stdio.io());
|
|
169
|
+
}
|
|
170
|
+
|
|
146
171
|
fn timedOutResult(allocator: std.mem.Allocator) !ExecResult {
|
|
147
172
|
return .{
|
|
148
173
|
.stdout = try allocator.dupe(u8, ""),
|
package/src/ios.zig
CHANGED
|
@@ -149,11 +149,18 @@ pub const IosDevice = struct {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
pub fn visibleBySelector(self: *IosDevice, wanted: selector.Selector) !?bool {
|
|
152
|
+
return try self.visibleBySelectorWithTimeout(wanted, shimTimeoutMs());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
pub fn visibleBySelectorWithTimeout(self: *IosDevice, wanted: selector.Selector, timeout_ms: u64) !?bool {
|
|
152
156
|
if (self.shim_path == null) return null;
|
|
153
157
|
const shim_selector = try ios_shim.selectorString(self.allocator, wanted) orelse return null;
|
|
154
158
|
defer self.allocator.free(shim_selector);
|
|
155
159
|
|
|
156
|
-
const response = try self.
|
|
160
|
+
const response = try self.runShimWithTimeout(.{
|
|
161
|
+
.kind = .query,
|
|
162
|
+
.selector = shim_selector,
|
|
163
|
+
}, @max(timeout_ms, 1));
|
|
157
164
|
defer self.allocator.free(response);
|
|
158
165
|
return try ios_shim.parseQueryResponse(response);
|
|
159
166
|
}
|
|
@@ -460,7 +467,7 @@ test "ios simulator openLink keeps sweeping delayed XCTest interruptions until a
|
|
|
460
467
|
\\if [[ "$count" -lt 6 ]]; then
|
|
461
468
|
\\ printf '{{"status":"ok","accepted":false,"count":0}}\n'
|
|
462
469
|
\\else
|
|
463
|
-
\\ printf '{{"status":"ok","accepted":true,"label":"
|
|
470
|
+
\\ printf '{{"status":"ok","accepted":true,"label":"Demo App","count":1}}\n'
|
|
464
471
|
\\fi
|
|
465
472
|
\\
|
|
466
473
|
, .{ tmp.sub_path, tmp.sub_path });
|
package/src/main.zig
CHANGED
package/src/runner.zig
CHANGED
|
@@ -455,3 +455,186 @@ test "assertHealthy retries through a transient observation command failure" {
|
|
|
455
455
|
try std.testing.expect(std.mem.indexOf(u8, events, "\"kind\":\"assert.healthy\"") != null);
|
|
456
456
|
try std.testing.expect(std.mem.indexOf(u8, events, "\"status\":\"ok\"") != null);
|
|
457
457
|
}
|
|
458
|
+
|
|
459
|
+
test "native selector waits pass bounded query timeouts instead of legacy blocking queries" {
|
|
460
|
+
const allocator = std.testing.allocator;
|
|
461
|
+
const dir = "zig-cache-test-runner-native-wait-bounded-query-timeout";
|
|
462
|
+
std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
463
|
+
defer std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
464
|
+
|
|
465
|
+
const NativeBoundedWaitDevice = struct {
|
|
466
|
+
allocator: std.mem.Allocator,
|
|
467
|
+
legacy_queries: usize = 0,
|
|
468
|
+
bounded_queries: usize = 0,
|
|
469
|
+
largest_query_timeout_ms: u64 = 0,
|
|
470
|
+
snapshots: usize = 0,
|
|
471
|
+
|
|
472
|
+
pub fn visibleBySelector(self: *@This(), wanted: selector.Selector) !?bool {
|
|
473
|
+
_ = wanted;
|
|
474
|
+
self.legacy_queries += 1;
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
pub fn visibleBySelectorWithTimeout(self: *@This(), wanted: selector.Selector, timeout_ms: u64) !?bool {
|
|
479
|
+
_ = wanted;
|
|
480
|
+
self.bounded_queries += 1;
|
|
481
|
+
self.largest_query_timeout_ms = @max(self.largest_query_timeout_ms, timeout_ms);
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
pub fn snapshot(self: *@This(), writer: anytype) !types.ObservationSnapshot {
|
|
486
|
+
_ = writer;
|
|
487
|
+
self.snapshots += 1;
|
|
488
|
+
const nodes = try self.allocator.alloc(types.UiNode, 0);
|
|
489
|
+
return .{
|
|
490
|
+
.id = try self.allocator.dupe(u8, "native-bounded-timeout-final"),
|
|
491
|
+
.timestamp_ms = 1,
|
|
492
|
+
.nodes = nodes,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
};
|
|
496
|
+
|
|
497
|
+
var device = NativeBoundedWaitDevice{ .allocator = allocator };
|
|
498
|
+
var tw = try trace.TraceWriter.init(allocator, dir);
|
|
499
|
+
defer tw.deinit();
|
|
500
|
+
|
|
501
|
+
try std.testing.expect(!try waitUntilVisible(&device, .{ .id = "never-visible" }, 25, &tw, .{ .poll_ms = 0 }));
|
|
502
|
+
try std.testing.expect(device.bounded_queries > 0);
|
|
503
|
+
try std.testing.expectEqual(@as(usize, 0), device.legacy_queries);
|
|
504
|
+
try std.testing.expect(device.largest_query_timeout_ms > 0);
|
|
505
|
+
try std.testing.expect(device.largest_query_timeout_ms <= 25);
|
|
506
|
+
try std.testing.expectEqual(@as(usize, 1), device.snapshots);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
test "native selector wait retries bounded query command timeouts before falling back" {
|
|
510
|
+
const allocator = std.testing.allocator;
|
|
511
|
+
const dir = "zig-cache-test-runner-native-wait-retries-bounded-query-timeout";
|
|
512
|
+
std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
513
|
+
defer std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
514
|
+
|
|
515
|
+
const NativeFlakyBoundedWaitDevice = struct {
|
|
516
|
+
allocator: std.mem.Allocator,
|
|
517
|
+
legacy_queries: usize = 0,
|
|
518
|
+
bounded_queries: usize = 0,
|
|
519
|
+
snapshots: usize = 0,
|
|
520
|
+
|
|
521
|
+
pub fn visibleBySelector(self: *@This(), wanted: selector.Selector) !?bool {
|
|
522
|
+
_ = wanted;
|
|
523
|
+
self.legacy_queries += 1;
|
|
524
|
+
return error.LegacyNativeQueryUsed;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
pub fn visibleBySelectorWithTimeout(self: *@This(), wanted: selector.Selector, timeout_ms: u64) !?bool {
|
|
528
|
+
_ = wanted;
|
|
529
|
+
try std.testing.expect(timeout_ms > 0);
|
|
530
|
+
self.bounded_queries += 1;
|
|
531
|
+
if (self.bounded_queries == 1) return error.CommandTimedOut;
|
|
532
|
+
return true;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
pub fn snapshot(self: *@This(), writer: anytype) !types.ObservationSnapshot {
|
|
536
|
+
_ = writer;
|
|
537
|
+
self.snapshots += 1;
|
|
538
|
+
return error.UnexpectedSnapshotFallback;
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
var device = NativeFlakyBoundedWaitDevice{ .allocator = allocator };
|
|
543
|
+
var tw = try trace.TraceWriter.init(allocator, dir);
|
|
544
|
+
defer tw.deinit();
|
|
545
|
+
|
|
546
|
+
try std.testing.expect(try waitUntilVisible(&device, .{ .text = "Ready" }, 100, &tw, .{ .poll_ms = 0 }));
|
|
547
|
+
try std.testing.expectEqual(@as(usize, 0), device.legacy_queries);
|
|
548
|
+
try std.testing.expectEqual(@as(usize, 2), device.bounded_queries);
|
|
549
|
+
try std.testing.expectEqual(@as(usize, 0), device.snapshots);
|
|
550
|
+
|
|
551
|
+
const events_path = try std.fs.path.join(allocator, &.{ dir, "events.jsonl" });
|
|
552
|
+
defer allocator.free(events_path);
|
|
553
|
+
const events = try stdio.readFileAlloc(allocator, events_path, 1024 * 1024);
|
|
554
|
+
defer allocator.free(events);
|
|
555
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"kind\":\"observe.retry\"") != null);
|
|
556
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"error\":\"CommandTimedOut\"") != null);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
test "assertHealthy uses native selector probes before broad snapshots" {
|
|
560
|
+
const allocator = std.testing.allocator;
|
|
561
|
+
const dir = "zig-cache-test-runner-assert-healthy-native-selector";
|
|
562
|
+
std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
563
|
+
defer std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
564
|
+
|
|
565
|
+
const NativeHealthDevice = struct {
|
|
566
|
+
allocator: std.mem.Allocator,
|
|
567
|
+
queries: usize = 0,
|
|
568
|
+
snapshots: usize = 0,
|
|
569
|
+
|
|
570
|
+
pub fn visibleBySelector(self: *@This(), wanted: selector.Selector) !?bool {
|
|
571
|
+
_ = wanted;
|
|
572
|
+
self.queries += 1;
|
|
573
|
+
return false;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
pub fn snapshot(self: *@This(), writer: anytype) !types.ObservationSnapshot {
|
|
577
|
+
_ = writer;
|
|
578
|
+
self.snapshots += 1;
|
|
579
|
+
return error.UnexpectedSnapshotFallback;
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
var device = NativeHealthDevice{ .allocator = allocator };
|
|
584
|
+
var tw = try trace.TraceWriter.init(allocator, dir);
|
|
585
|
+
defer tw.deinit();
|
|
586
|
+
|
|
587
|
+
try std.testing.expect(try assertHealthy(&device, 100, &tw, .{ .settle_ms = 0, .poll_ms = 0 }));
|
|
588
|
+
try std.testing.expect(device.queries > 0);
|
|
589
|
+
try std.testing.expectEqual(@as(usize, 0), device.snapshots);
|
|
590
|
+
|
|
591
|
+
const events_path = try std.fs.path.join(allocator, &.{ dir, "events.jsonl" });
|
|
592
|
+
defer allocator.free(events_path);
|
|
593
|
+
const events = try stdio.readFileAlloc(allocator, events_path, 1024 * 1024);
|
|
594
|
+
defer allocator.free(events);
|
|
595
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"kind\":\"assert.healthy\"") != null);
|
|
596
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"status\":\"ok\"") != null);
|
|
597
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"strategy\":\"nativeSelector\"") != null);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
test "assertHealthy reports unhealthy native selector matches" {
|
|
601
|
+
const allocator = std.testing.allocator;
|
|
602
|
+
const dir = "zig-cache-test-runner-assert-healthy-native-unhealthy";
|
|
603
|
+
std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
604
|
+
defer std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
605
|
+
|
|
606
|
+
const NativeUnhealthyDevice = struct {
|
|
607
|
+
allocator: std.mem.Allocator,
|
|
608
|
+
queries: usize = 0,
|
|
609
|
+
snapshots: usize = 0,
|
|
610
|
+
|
|
611
|
+
pub fn visibleBySelector(self: *@This(), wanted: selector.Selector) !?bool {
|
|
612
|
+
self.queries += 1;
|
|
613
|
+
if (wanted.text_contains) |text| return std.mem.eql(u8, text, "ReferenceError");
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
pub fn snapshot(self: *@This(), writer: anytype) !types.ObservationSnapshot {
|
|
618
|
+
_ = writer;
|
|
619
|
+
self.snapshots += 1;
|
|
620
|
+
return error.UnexpectedSnapshotFallback;
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
var device = NativeUnhealthyDevice{ .allocator = allocator };
|
|
625
|
+
var tw = try trace.TraceWriter.init(allocator, dir);
|
|
626
|
+
defer tw.deinit();
|
|
627
|
+
|
|
628
|
+
try std.testing.expect(!try assertHealthy(&device, 100, &tw, .{ .settle_ms = 0, .poll_ms = 0 }));
|
|
629
|
+
try std.testing.expect(device.queries > 0);
|
|
630
|
+
try std.testing.expectEqual(@as(usize, 0), device.snapshots);
|
|
631
|
+
|
|
632
|
+
const events_path = try std.fs.path.join(allocator, &.{ dir, "events.jsonl" });
|
|
633
|
+
defer allocator.free(events_path);
|
|
634
|
+
const events = try stdio.readFileAlloc(allocator, events_path, 1024 * 1024);
|
|
635
|
+
defer allocator.free(events);
|
|
636
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"kind\":\"assert.healthy\"") != null);
|
|
637
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"status\":\"unhealthy\"") != null);
|
|
638
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"strategy\":\"nativeSelector\"") != null);
|
|
639
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"matchedIndex\"") != null);
|
|
640
|
+
}
|
package/src/runner_events.zig
CHANGED
|
@@ -48,6 +48,23 @@ pub fn recordNativeWaitTimeoutWithDiagnostics(device: anytype, tw: *trace.TraceW
|
|
|
48
48
|
try recordDiagnosticWithStrategyAndTimeout(tw, kind, "timeout", "nativeSelector", selectors, snap, timeout_ms);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
pub fn recordNativeSelectorArrayStatus(tw: *trace.TraceWriter, kind: []const u8, status: []const u8, selectors: []const selector.Selector, matched_index: ?usize, timeout_ms: u64) !void {
|
|
52
|
+
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
53
|
+
defer payload.deinit();
|
|
54
|
+
const out = &payload.writer;
|
|
55
|
+
try out.writeAll("{\"status\":");
|
|
56
|
+
try trace.writeJsonString(out, status);
|
|
57
|
+
try out.writeAll(",\"strategy\":\"nativeSelector\"");
|
|
58
|
+
if (matched_index) |index| try out.print(",\"matchedIndex\":{d}", .{index});
|
|
59
|
+
try out.writeAll(",\"selectors\":[");
|
|
60
|
+
for (selectors, 0..) |wanted, index| {
|
|
61
|
+
if (index > 0) try out.writeAll(",");
|
|
62
|
+
try trace.writeSelectorJson(out, wanted);
|
|
63
|
+
}
|
|
64
|
+
try out.print("],\"timeoutMs\":{d}}}", .{timeout_ms});
|
|
65
|
+
try tw.recordEvent(kind, out.buffered());
|
|
66
|
+
}
|
|
67
|
+
|
|
51
68
|
pub fn recordSelectorArrayStatus(tw: *trace.TraceWriter, kind: []const u8, status: []const u8, selectors: []const selector.Selector, timeout_ms: u64) !void {
|
|
52
69
|
var payload: std.Io.Writer.Allocating = .init(tw.allocator);
|
|
53
70
|
defer payload.deinit();
|
package/src/runner_waits.zig
CHANGED
|
@@ -3,7 +3,6 @@ const stdio = @import("stdio.zig");
|
|
|
3
3
|
const health = @import("health.zig");
|
|
4
4
|
const runner_config = @import("runner_config.zig");
|
|
5
5
|
const runner_events = @import("runner_events.zig");
|
|
6
|
-
const runner_native = @import("runner_native.zig");
|
|
7
6
|
const scenario = @import("scenario.zig");
|
|
8
7
|
const selector = @import("selector.zig");
|
|
9
8
|
const trace = @import("trace.zig");
|
|
@@ -40,18 +39,28 @@ fn untilVisibleKind(
|
|
|
40
39
|
) !bool {
|
|
41
40
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
42
41
|
while (true) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
if (
|
|
49
|
-
if (
|
|
50
|
-
|
|
42
|
+
if (nativeSelectorQueryTimeoutMs(deadline)) |query_timeout_ms| {
|
|
43
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| {
|
|
44
|
+
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
45
|
+
return err;
|
|
46
|
+
};
|
|
47
|
+
if (native_result) |visible| {
|
|
48
|
+
if (visible) {
|
|
49
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, kind, wanted, null, timeout_ms);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (stdio.nowMs() >= deadline) {
|
|
53
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
try sleepMs(options.poll_ms);
|
|
57
|
+
continue;
|
|
51
58
|
}
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
} else if (hasNativeSelectorQuery(device)) {
|
|
60
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
61
|
+
return false;
|
|
54
62
|
}
|
|
63
|
+
|
|
55
64
|
var snap = device.snapshot(writer) catch |err| {
|
|
56
65
|
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
57
66
|
return err;
|
|
@@ -110,18 +119,28 @@ fn untilNotVisibleKind(
|
|
|
110
119
|
) !bool {
|
|
111
120
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
112
121
|
while (true) {
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
if (
|
|
119
|
-
if (
|
|
120
|
-
|
|
122
|
+
if (nativeSelectorQueryTimeoutMs(deadline)) |query_timeout_ms| {
|
|
123
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| {
|
|
124
|
+
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
125
|
+
return err;
|
|
126
|
+
};
|
|
127
|
+
if (native_result) |visible| {
|
|
128
|
+
if (!visible) {
|
|
129
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, kind, wanted, null, timeout_ms);
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
if (stdio.nowMs() >= deadline) {
|
|
133
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
try sleepMs(options.poll_ms);
|
|
137
|
+
continue;
|
|
121
138
|
}
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
} else if (hasNativeSelectorQuery(device)) {
|
|
140
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
141
|
+
return false;
|
|
124
142
|
}
|
|
143
|
+
|
|
125
144
|
var snap = device.snapshot(writer) catch |err| {
|
|
126
145
|
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
127
146
|
return err;
|
|
@@ -161,7 +180,19 @@ pub fn waitUntilAnyVisible(
|
|
|
161
180
|
while (true) {
|
|
162
181
|
var all_native = true;
|
|
163
182
|
for (selectors, 0..) |wanted, index| {
|
|
164
|
-
|
|
183
|
+
const query_timeout_ms = nativeSelectorQueryTimeoutMs(deadline) orelse {
|
|
184
|
+
if (hasNativeSelectorQuery(device)) {
|
|
185
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.any", selectors, timeout_ms);
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
all_native = false;
|
|
189
|
+
break;
|
|
190
|
+
};
|
|
191
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| {
|
|
192
|
+
if (try retryTransientObservation(err, "wait.any", writer, deadline, options)) continue;
|
|
193
|
+
return err;
|
|
194
|
+
};
|
|
195
|
+
if (native_result) |visible| {
|
|
165
196
|
if (visible) {
|
|
166
197
|
if (writer) |tw| try runner_events.recordNativeWait(tw, "wait.any", wanted, index, timeout_ms);
|
|
167
198
|
return index;
|
|
@@ -171,6 +202,7 @@ pub fn waitUntilAnyVisible(
|
|
|
171
202
|
break;
|
|
172
203
|
}
|
|
173
204
|
}
|
|
205
|
+
|
|
174
206
|
if (all_native) {
|
|
175
207
|
if (stdio.nowMs() >= deadline) {
|
|
176
208
|
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.any", selectors, timeout_ms);
|
|
@@ -179,6 +211,7 @@ pub fn waitUntilAnyVisible(
|
|
|
179
211
|
try sleepMs(options.poll_ms);
|
|
180
212
|
continue;
|
|
181
213
|
}
|
|
214
|
+
|
|
182
215
|
var snap = device.snapshot(writer) catch |err| {
|
|
183
216
|
if (try retryTransientObservation(err, "wait.any", writer, deadline, options)) continue;
|
|
184
217
|
return err;
|
|
@@ -251,6 +284,7 @@ pub fn assertHealthy(
|
|
|
251
284
|
) !bool {
|
|
252
285
|
const health_selectors = health.defaultSelectors();
|
|
253
286
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
287
|
+
if (try nativeAssertHealthy(device, health_selectors, timeout_ms, writer, deadline, options)) |healthy| return healthy;
|
|
254
288
|
while (true) {
|
|
255
289
|
var snap = device.snapshot(writer) catch |err| {
|
|
256
290
|
if (try retryTransientObservation(err, "assert.healthy", writer, deadline, options)) continue;
|
|
@@ -276,6 +310,35 @@ pub fn assertHealthy(
|
|
|
276
310
|
}
|
|
277
311
|
}
|
|
278
312
|
|
|
313
|
+
fn nativeAssertHealthy(
|
|
314
|
+
device: anytype,
|
|
315
|
+
health_selectors: []const selector.Selector,
|
|
316
|
+
timeout_ms: u64,
|
|
317
|
+
writer: ?*trace.TraceWriter,
|
|
318
|
+
deadline: i64,
|
|
319
|
+
options: RunOptions,
|
|
320
|
+
) !?bool {
|
|
321
|
+
if (!hasNativeSelectorQuery(device)) return null;
|
|
322
|
+
|
|
323
|
+
probe: while (true) {
|
|
324
|
+
for (health_selectors, 0..) |wanted, index| {
|
|
325
|
+
const query_timeout_ms = nativeSelectorQueryTimeoutMs(deadline) orelse return false;
|
|
326
|
+
const result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| {
|
|
327
|
+
if (try retryTransientObservation(err, "assert.healthy", writer, deadline, options)) continue :probe;
|
|
328
|
+
return err;
|
|
329
|
+
};
|
|
330
|
+
const visible = result orelse return null;
|
|
331
|
+
if (visible) {
|
|
332
|
+
if (writer) |tw| try runner_events.recordNativeSelectorArrayStatus(tw, "assert.healthy", "unhealthy", health_selectors, index, timeout_ms);
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (writer) |tw| try runner_events.recordNativeSelectorArrayStatus(tw, "assert.healthy", "ok", health_selectors, null, timeout_ms);
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
279
342
|
pub fn scrollUntilVisible(
|
|
280
343
|
device: anytype,
|
|
281
344
|
wanted: selector.Selector,
|
|
@@ -340,11 +403,22 @@ pub fn scrollUntilVisible(
|
|
|
340
403
|
}
|
|
341
404
|
}
|
|
342
405
|
|
|
343
|
-
fn
|
|
406
|
+
fn hasNativeSelectorQuery(device: anytype) bool {
|
|
407
|
+
return @hasDecl(@TypeOf(device.*), "visibleBySelectorWithTimeout") or @hasDecl(@TypeOf(device.*), "visibleBySelector");
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
fn nativeVisibleBySelector(device: anytype, wanted: selector.Selector, timeout_ms: u64) !?bool {
|
|
411
|
+
if (@hasDecl(@TypeOf(device.*), "visibleBySelectorWithTimeout")) return try device.visibleBySelectorWithTimeout(wanted, timeout_ms);
|
|
344
412
|
if (!@hasDecl(@TypeOf(device.*), "visibleBySelector")) return null;
|
|
345
413
|
return try device.visibleBySelector(wanted);
|
|
346
414
|
}
|
|
347
415
|
|
|
416
|
+
fn nativeSelectorQueryTimeoutMs(deadline: i64) ?u64 {
|
|
417
|
+
const now = stdio.nowMs();
|
|
418
|
+
if (now >= deadline) return null;
|
|
419
|
+
return @as(u64, @intCast(deadline - now));
|
|
420
|
+
}
|
|
421
|
+
|
|
348
422
|
fn retryTransientObservation(
|
|
349
423
|
err: anyerror,
|
|
350
424
|
kind: []const u8,
|
package/src/stdio.zig
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const builtin = @import("builtin");
|
|
1
2
|
const std = @import("std");
|
|
2
3
|
|
|
3
4
|
const default_buffer_size = 8192;
|
|
@@ -6,6 +7,7 @@ var process_threaded: ?std.Io.Threaded = null;
|
|
|
6
7
|
|
|
7
8
|
fn processIo() std.Io {
|
|
8
9
|
if (process_threaded) |*threaded| return threaded.io();
|
|
10
|
+
if (builtin.is_test) return std.testing.io;
|
|
9
11
|
return std.Io.Threaded.global_single_threaded.io();
|
|
10
12
|
}
|
|
11
13
|
|
|
@@ -46,7 +48,12 @@ pub fn nowMs() i64 {
|
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
pub fn getenv(name: []const u8) ?[]const u8 {
|
|
49
|
-
const environ = process_environ orelse
|
|
51
|
+
const environ = process_environ orelse {
|
|
52
|
+
if (builtin.is_test) {
|
|
53
|
+
return std.process.Environ.getPosix(std.testing.environ, name);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
};
|
|
50
57
|
const block = environ.block;
|
|
51
58
|
const Block = @TypeOf(block);
|
|
52
59
|
if (Block != std.process.Environ.PosixBlock) return null;
|
|
@@ -86,7 +93,15 @@ pub const Output = struct {
|
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
pub fn flush(self: *Output) !void {
|
|
89
|
-
if (self.initialized)
|
|
96
|
+
if (!self.initialized) return;
|
|
97
|
+
self.file_writer.interface.flush() catch |err| switch (err) {
|
|
98
|
+
error.WriteFailed => {
|
|
99
|
+
if (self.file_writer.err) |actual| return actual;
|
|
100
|
+
if (self.file_writer.write_file_err) |actual| return actual;
|
|
101
|
+
return err;
|
|
102
|
+
},
|
|
103
|
+
else => |actual| return actual,
|
|
104
|
+
};
|
|
90
105
|
}
|
|
91
106
|
|
|
92
107
|
pub fn deinit(self: *Output) void {
|
package/src/test_io.zig
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const std = @import("std");
|
|
2
|
+
const stdio = @import("stdio.zig");
|
|
3
|
+
|
|
4
|
+
pub fn cwd() Cwd {
|
|
5
|
+
return .{};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
pub const Cwd = struct {
|
|
9
|
+
pub fn makePath(_: Cwd, path: []const u8) !void {
|
|
10
|
+
return std.Io.Dir.cwd().createDirPath(stdio.io(), path);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
pub fn writeFile(_: Cwd, options: std.Io.Dir.WriteFileOptions) !void {
|
|
14
|
+
return std.Io.Dir.cwd().writeFile(stdio.io(), options);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
pub fn readFileAlloc(_: Cwd, allocator: std.mem.Allocator, path: []const u8, limit: usize) ![]u8 {
|
|
18
|
+
return std.Io.Dir.cwd().readFileAlloc(stdio.io(), path, allocator, .limited(limit));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
pub fn createFile(_: Cwd, path: []const u8, flags: std.Io.Dir.CreateFileOptions) !File {
|
|
22
|
+
return createFileIn(std.Io.Dir.cwd(), path, flags);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub fn deleteTree(_: Cwd, path: []const u8) !void {
|
|
26
|
+
return std.Io.Dir.cwd().deleteTree(stdio.io(), path);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub fn deleteFile(_: Cwd, path: []const u8) !void {
|
|
30
|
+
return std.Io.Dir.cwd().deleteFile(stdio.io(), path);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub fn access(_: Cwd, path: []const u8, options: std.Io.Dir.AccessOptions) !void {
|
|
34
|
+
return std.Io.Dir.cwd().access(stdio.io(), path, options);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
pub const File = struct {
|
|
39
|
+
inner: std.Io.File,
|
|
40
|
+
|
|
41
|
+
pub fn writeAll(self: *File, bytes: []const u8) !void {
|
|
42
|
+
return std.Io.File.writeStreamingAll(self.inner, stdio.io(), bytes);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
pub fn chmod(self: *File, mode: std.posix.mode_t) !void {
|
|
46
|
+
return self.inner.setPermissions(stdio.io(), .fromMode(mode));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
pub fn close(self: *File) void {
|
|
50
|
+
self.inner.close(stdio.io());
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
pub fn createFileIn(dir: std.Io.Dir, path: []const u8, flags: std.Io.Dir.CreateFileOptions) !File {
|
|
55
|
+
return .{ .inner = try dir.createFile(stdio.io(), path, flags) };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub fn readFileAllocIn(dir: std.Io.Dir, allocator: std.mem.Allocator, path: []const u8, limit: usize) ![]u8 {
|
|
59
|
+
return dir.readFileAlloc(stdio.io(), path, allocator, .limited(limit));
|
|
60
|
+
}
|
package/src/version.zig
CHANGED