zeno-mobile-runner 0.2.7 → 0.2.9
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 +22 -1
- package/FEATURES.md +1 -1
- package/README.md +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/shims/ios/ZMRShimUITestCase.swift +1 -1
- 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 +145 -0
- package/src/runner_waits.zig +91 -39
- 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,27 @@ All notable changes to Zeno Mobile Runner are tracked here.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.2.9 (2026-06-17)
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- iOS `assertHealthy` now bounds each native crash/error selector probe
|
|
12
|
+
independently and falls back to the broad snapshot path after transient
|
|
13
|
+
native query timeouts. This prevents healthy but heavy screens from failing
|
|
14
|
+
when one absent overlay selector consumes the whole observation budget.
|
|
15
|
+
- The iOS XCTest shim `query` command now uses the fast selector resolver it
|
|
16
|
+
already validates, avoiding broad element enumeration for exact selector
|
|
17
|
+
checks.
|
|
18
|
+
|
|
19
|
+
## 0.2.8 (2026-06-17)
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- iOS native selector waits now cap each XCTest query to the remaining scenario
|
|
24
|
+
step timeout and retry transient query command timeouts. Missing selectors can
|
|
25
|
+
no longer outlive the scenario `timeoutMs` by waiting on the shim's longer
|
|
26
|
+
cold-start command timeout.
|
|
27
|
+
|
|
7
28
|
## 0.2.7 (2026-06-15)
|
|
8
29
|
|
|
9
30
|
### Fixed
|
|
@@ -39,7 +60,7 @@ All notable changes to Zeno Mobile Runner are tracked here.
|
|
|
39
60
|
- Extended the iOS simulator `openLink` interruption sweep to cover Expo
|
|
40
61
|
dev-client deep-link chooser sheets that appear more than six seconds after
|
|
41
62
|
`simctl openurl` returns. The sweep remains bounded, but now covers the
|
|
42
|
-
delayed chooser timing observed in
|
|
63
|
+
delayed chooser timing observed in app auth smoke runs.
|
|
43
64
|
|
|
44
65
|
## 0.2.3 (2026-06-15)
|
|
45
66
|
|
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.9`, 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.9` developer preview.
|
|
201
201
|
Protocol version: `2026-04-28`.
|
|
202
202
|
|
|
203
203
|
## 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.9.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.9","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.9`.
|
|
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.9","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.9","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.9","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.9","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.9","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.9","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.9","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.9","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.9","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
|
|
@@ -133,7 +133,7 @@ final class ZMRShimUITestCase: XCTestCase {
|
|
|
133
133
|
guard isFastQueryable(parts: parts) else {
|
|
134
134
|
return error("selector.unsupported", "unsupported query selector: \(selector)")
|
|
135
135
|
}
|
|
136
|
-
let element =
|
|
136
|
+
let element = resolveFastElement(selector: selector, app: app, preferredTypes: [])
|
|
137
137
|
return [
|
|
138
138
|
"status": "ok",
|
|
139
139
|
"exists": element?.exists ?? false,
|
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
|
@@ -456,6 +456,106 @@ test "assertHealthy retries through a transient observation command failure" {
|
|
|
456
456
|
try std.testing.expect(std.mem.indexOf(u8, events, "\"status\":\"ok\"") != null);
|
|
457
457
|
}
|
|
458
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
|
+
|
|
459
559
|
test "assertHealthy uses native selector probes before broad snapshots" {
|
|
460
560
|
const allocator = std.testing.allocator;
|
|
461
561
|
const dir = "zig-cache-test-runner-assert-healthy-native-selector";
|
|
@@ -497,6 +597,51 @@ test "assertHealthy uses native selector probes before broad snapshots" {
|
|
|
497
597
|
try std.testing.expect(std.mem.indexOf(u8, events, "\"strategy\":\"nativeSelector\"") != null);
|
|
498
598
|
}
|
|
499
599
|
|
|
600
|
+
test "assertHealthy bounds each native selector probe independently" {
|
|
601
|
+
const allocator = std.testing.allocator;
|
|
602
|
+
const dir = "zig-cache-test-runner-assert-healthy-native-probe-budget";
|
|
603
|
+
std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
604
|
+
defer std.Io.Dir.cwd().deleteTree(stdio.io(), dir) catch {};
|
|
605
|
+
|
|
606
|
+
const NativeSlowAbsentHealthDevice = struct {
|
|
607
|
+
allocator: std.mem.Allocator,
|
|
608
|
+
bounded_queries: usize = 0,
|
|
609
|
+
largest_query_timeout_ms: u64 = 0,
|
|
610
|
+
snapshots: usize = 0,
|
|
611
|
+
|
|
612
|
+
pub fn visibleBySelectorWithTimeout(self: *@This(), wanted: selector.Selector, timeout_ms: u64) !?bool {
|
|
613
|
+
_ = wanted;
|
|
614
|
+
self.bounded_queries += 1;
|
|
615
|
+
self.largest_query_timeout_ms = @max(self.largest_query_timeout_ms, timeout_ms);
|
|
616
|
+
if (timeout_ms > 0) stdio.sleepNs(timeout_ms * std.time.ns_per_ms);
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
pub fn snapshot(self: *@This(), writer: anytype) !types.ObservationSnapshot {
|
|
621
|
+
_ = writer;
|
|
622
|
+
self.snapshots += 1;
|
|
623
|
+
return error.UnexpectedSnapshotFallback;
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
var device = NativeSlowAbsentHealthDevice{ .allocator = allocator };
|
|
628
|
+
var tw = try trace.TraceWriter.init(allocator, dir);
|
|
629
|
+
defer tw.deinit();
|
|
630
|
+
|
|
631
|
+
try std.testing.expect(try assertHealthy(&device, 1, &tw, .{ .settle_ms = 0, .poll_ms = 0, .action_timeout_ms = 1 }));
|
|
632
|
+
try std.testing.expect(device.bounded_queries > 1);
|
|
633
|
+
try std.testing.expect(device.largest_query_timeout_ms <= 1);
|
|
634
|
+
try std.testing.expectEqual(@as(usize, 0), device.snapshots);
|
|
635
|
+
|
|
636
|
+
const events_path = try std.fs.path.join(allocator, &.{ dir, "events.jsonl" });
|
|
637
|
+
defer allocator.free(events_path);
|
|
638
|
+
const events = try stdio.readFileAlloc(allocator, events_path, 1024 * 1024);
|
|
639
|
+
defer allocator.free(events);
|
|
640
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"kind\":\"assert.healthy\"") != null);
|
|
641
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"status\":\"ok\"") != null);
|
|
642
|
+
try std.testing.expect(std.mem.indexOf(u8, events, "\"strategy\":\"nativeSelector\"") != null);
|
|
643
|
+
}
|
|
644
|
+
|
|
500
645
|
test "assertHealthy reports unhealthy native selector matches" {
|
|
501
646
|
const allocator = std.testing.allocator;
|
|
502
647
|
const dir = "zig-cache-test-runner-assert-healthy-native-unhealthy";
|
package/src/runner_waits.zig
CHANGED
|
@@ -3,12 +3,12 @@ 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");
|
|
10
9
|
|
|
11
10
|
const RunOptions = runner_config.RunOptions;
|
|
11
|
+
const native_health_probe_timeout_ms: u64 = 250;
|
|
12
12
|
|
|
13
13
|
pub fn waitUntilVisible(
|
|
14
14
|
device: anytype,
|
|
@@ -40,18 +40,28 @@ fn untilVisibleKind(
|
|
|
40
40
|
) !bool {
|
|
41
41
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
42
42
|
while (true) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
if (
|
|
49
|
-
if (
|
|
50
|
-
|
|
43
|
+
if (nativeSelectorQueryTimeoutMs(deadline)) |query_timeout_ms| {
|
|
44
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| {
|
|
45
|
+
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
46
|
+
return err;
|
|
47
|
+
};
|
|
48
|
+
if (native_result) |visible| {
|
|
49
|
+
if (visible) {
|
|
50
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, kind, wanted, null, timeout_ms);
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
if (stdio.nowMs() >= deadline) {
|
|
54
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
try sleepMs(options.poll_ms);
|
|
58
|
+
continue;
|
|
51
59
|
}
|
|
52
|
-
|
|
53
|
-
|
|
60
|
+
} else if (hasNativeSelectorQuery(device)) {
|
|
61
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
62
|
+
return false;
|
|
54
63
|
}
|
|
64
|
+
|
|
55
65
|
var snap = device.snapshot(writer) catch |err| {
|
|
56
66
|
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
57
67
|
return err;
|
|
@@ -110,18 +120,28 @@ fn untilNotVisibleKind(
|
|
|
110
120
|
) !bool {
|
|
111
121
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
112
122
|
while (true) {
|
|
113
|
-
if (
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
if (
|
|
119
|
-
if (
|
|
120
|
-
|
|
123
|
+
if (nativeSelectorQueryTimeoutMs(deadline)) |query_timeout_ms| {
|
|
124
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| {
|
|
125
|
+
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
126
|
+
return err;
|
|
127
|
+
};
|
|
128
|
+
if (native_result) |visible| {
|
|
129
|
+
if (!visible) {
|
|
130
|
+
if (writer) |tw| try runner_events.recordNativeWait(tw, kind, wanted, null, timeout_ms);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
if (stdio.nowMs() >= deadline) {
|
|
134
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
try sleepMs(options.poll_ms);
|
|
138
|
+
continue;
|
|
121
139
|
}
|
|
122
|
-
|
|
123
|
-
|
|
140
|
+
} else if (hasNativeSelectorQuery(device)) {
|
|
141
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, kind, &[_]selector.Selector{wanted}, timeout_ms);
|
|
142
|
+
return false;
|
|
124
143
|
}
|
|
144
|
+
|
|
125
145
|
var snap = device.snapshot(writer) catch |err| {
|
|
126
146
|
if (try retryTransientObservation(err, kind, writer, deadline, options)) continue;
|
|
127
147
|
return err;
|
|
@@ -161,7 +181,19 @@ pub fn waitUntilAnyVisible(
|
|
|
161
181
|
while (true) {
|
|
162
182
|
var all_native = true;
|
|
163
183
|
for (selectors, 0..) |wanted, index| {
|
|
164
|
-
|
|
184
|
+
const query_timeout_ms = nativeSelectorQueryTimeoutMs(deadline) orelse {
|
|
185
|
+
if (hasNativeSelectorQuery(device)) {
|
|
186
|
+
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.any", selectors, timeout_ms);
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
all_native = false;
|
|
190
|
+
break;
|
|
191
|
+
};
|
|
192
|
+
const native_result = nativeVisibleBySelector(device, wanted, query_timeout_ms) catch |err| {
|
|
193
|
+
if (try retryTransientObservation(err, "wait.any", writer, deadline, options)) continue;
|
|
194
|
+
return err;
|
|
195
|
+
};
|
|
196
|
+
if (native_result) |visible| {
|
|
165
197
|
if (visible) {
|
|
166
198
|
if (writer) |tw| try runner_events.recordNativeWait(tw, "wait.any", wanted, index, timeout_ms);
|
|
167
199
|
return index;
|
|
@@ -171,6 +203,7 @@ pub fn waitUntilAnyVisible(
|
|
|
171
203
|
break;
|
|
172
204
|
}
|
|
173
205
|
}
|
|
206
|
+
|
|
174
207
|
if (all_native) {
|
|
175
208
|
if (stdio.nowMs() >= deadline) {
|
|
176
209
|
if (writer) |tw| try runner_events.recordNativeWaitTimeoutWithDiagnostics(device, tw, "wait.any", selectors, timeout_ms);
|
|
@@ -179,6 +212,7 @@ pub fn waitUntilAnyVisible(
|
|
|
179
212
|
try sleepMs(options.poll_ms);
|
|
180
213
|
continue;
|
|
181
214
|
}
|
|
215
|
+
|
|
182
216
|
var snap = device.snapshot(writer) catch |err| {
|
|
183
217
|
if (try retryTransientObservation(err, "wait.any", writer, deadline, options)) continue;
|
|
184
218
|
return err;
|
|
@@ -251,7 +285,7 @@ pub fn assertHealthy(
|
|
|
251
285
|
) !bool {
|
|
252
286
|
const health_selectors = health.defaultSelectors();
|
|
253
287
|
const deadline = stdio.nowMs() + @as(i64, @intCast(timeout_ms));
|
|
254
|
-
if (try nativeAssertHealthy(device, health_selectors, timeout_ms, writer,
|
|
288
|
+
if (try nativeAssertHealthy(device, health_selectors, timeout_ms, writer, options)) |healthy| return healthy;
|
|
255
289
|
while (true) {
|
|
256
290
|
var snap = device.snapshot(writer) catch |err| {
|
|
257
291
|
if (try retryTransientObservation(err, "assert.healthy", writer, deadline, options)) continue;
|
|
@@ -282,27 +316,27 @@ fn nativeAssertHealthy(
|
|
|
282
316
|
health_selectors: []const selector.Selector,
|
|
283
317
|
timeout_ms: u64,
|
|
284
318
|
writer: ?*trace.TraceWriter,
|
|
285
|
-
deadline: i64,
|
|
286
319
|
options: RunOptions,
|
|
287
320
|
) !?bool {
|
|
288
|
-
if (
|
|
321
|
+
if (!hasNativeSelectorQuery(device)) return null;
|
|
289
322
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (try
|
|
294
|
-
return
|
|
295
|
-
};
|
|
296
|
-
const visible = result orelse return null;
|
|
297
|
-
if (visible) {
|
|
298
|
-
if (writer) |tw| try runner_events.recordNativeSelectorArrayStatus(tw, "assert.healthy", "unhealthy", health_selectors, index, timeout_ms);
|
|
299
|
-
return false;
|
|
323
|
+
for (health_selectors, 0..) |wanted, index| {
|
|
324
|
+
const result = nativeVisibleBySelector(device, wanted, nativeHealthProbeTimeoutMs(timeout_ms, options)) catch |err| {
|
|
325
|
+
if (err == error.CommandTimedOut or err == error.CommandFailed) {
|
|
326
|
+
if (writer) |tw| try runner_events.recordObservationRetry(tw, "assert.healthy", err);
|
|
327
|
+
return null;
|
|
300
328
|
}
|
|
329
|
+
return err;
|
|
330
|
+
};
|
|
331
|
+
const visible = result orelse return null;
|
|
332
|
+
if (visible) {
|
|
333
|
+
if (writer) |tw| try runner_events.recordNativeSelectorArrayStatus(tw, "assert.healthy", "unhealthy", health_selectors, index, timeout_ms);
|
|
334
|
+
return false;
|
|
301
335
|
}
|
|
302
|
-
|
|
303
|
-
if (writer) |tw| try runner_events.recordNativeSelectorArrayStatus(tw, "assert.healthy", "ok", health_selectors, null, timeout_ms);
|
|
304
|
-
return true;
|
|
305
336
|
}
|
|
337
|
+
|
|
338
|
+
if (writer) |tw| try runner_events.recordNativeSelectorArrayStatus(tw, "assert.healthy", "ok", health_selectors, null, timeout_ms);
|
|
339
|
+
return true;
|
|
306
340
|
}
|
|
307
341
|
|
|
308
342
|
pub fn scrollUntilVisible(
|
|
@@ -369,11 +403,29 @@ pub fn scrollUntilVisible(
|
|
|
369
403
|
}
|
|
370
404
|
}
|
|
371
405
|
|
|
372
|
-
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);
|
|
373
412
|
if (!@hasDecl(@TypeOf(device.*), "visibleBySelector")) return null;
|
|
374
413
|
return try device.visibleBySelector(wanted);
|
|
375
414
|
}
|
|
376
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
|
+
|
|
422
|
+
fn nativeHealthProbeTimeoutMs(timeout_ms: u64, options: RunOptions) u64 {
|
|
423
|
+
var probe_timeout_ms = if (timeout_ms == 0) @as(u64, 1) else timeout_ms;
|
|
424
|
+
probe_timeout_ms = @min(probe_timeout_ms, native_health_probe_timeout_ms);
|
|
425
|
+
if (options.action_timeout_ms > 0) probe_timeout_ms = @min(probe_timeout_ms, options.action_timeout_ms);
|
|
426
|
+
return @max(probe_timeout_ms, 1);
|
|
427
|
+
}
|
|
428
|
+
|
|
377
429
|
fn retryTransientObservation(
|
|
378
430
|
err: anyerror,
|
|
379
431
|
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