zeno-mobile-runner 0.2.16 → 0.2.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/CONTRIBUTING.md +20 -7
- package/FEATURES.md +29 -20
- package/README.md +73 -57
- package/SECURITY.md +11 -6
- package/clients/README.md +8 -7
- package/clients/go/README.md +2 -2
- package/clients/kotlin/README.md +2 -2
- package/clients/kotlin/build.gradle.kts +1 -1
- package/clients/python/README.md +2 -1
- package/clients/python/pyproject.toml +1 -1
- package/clients/rust/Cargo.lock +1 -1
- package/clients/rust/Cargo.toml +1 -1
- package/clients/rust/README.md +2 -2
- package/clients/swift/README.md +2 -2
- package/clients/typescript/README.md +2 -1
- package/clients/typescript/package.json +1 -1
- package/docs/adr/0001-agent-native-runner-boundary.md +1 -1
- package/docs/adr/README.md +7 -5
- package/docs/agent-discovery.md +15 -15
- package/docs/ai-agents.md +30 -20
- package/docs/app-integration.md +59 -27
- package/docs/benchmarking.md +16 -8
- package/docs/benchmarks/README.md +3 -1
- package/docs/benchmarks/benchmark-lab-v1.md +1 -1
- package/docs/client-installation.md +18 -9
- package/docs/clients.md +7 -6
- package/docs/config.md +29 -15
- package/docs/demo.md +14 -9
- package/docs/expo-smoke.md +12 -18
- package/docs/frameworks.md +30 -21
- package/docs/install.md +63 -13
- package/docs/npm.md +45 -27
- package/docs/production-readiness.md +32 -17
- package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
- package/docs/protocol-versioning.md +5 -3
- package/docs/protocol.md +33 -18
- package/docs/scenario-authoring.md +15 -8
- package/docs/support-matrix.md +38 -0
- package/docs/trace-privacy.md +5 -3
- package/docs/troubleshooting.md +17 -14
- package/npm/app-config.mjs +2 -0
- package/npm/commands.mjs +4 -4
- package/npm/scaffold.mjs +2 -2
- package/package.json +2 -2
- package/prebuilds/darwin-arm64/zmr +0 -0
- package/prebuilds/darwin-x64/zmr +0 -0
- package/prebuilds/linux-arm64/zmr +0 -0
- package/prebuilds/linux-x64/zmr +0 -0
- package/schemas/README.md +6 -3
- package/schemas/import-output.schema.json +1 -1
- package/schemas/scenario.schema.json +2 -0
- package/schemas/zmr-config.schema.json +2 -1
- package/scripts/public-metadata-guard.sh +101 -0
- package/shims/android/README.md +4 -3
- package/shims/android/protocol.md +3 -2
- package/shims/ios/README.md +5 -5
- package/shims/ios/protocol.md +2 -1
- package/skills/zmr-mobile-testing/SKILL.md +9 -8
- package/src/android_emulator.zig +54 -5
- package/src/cli_import.zig +15 -2
- package/src/cli_output.zig +2 -0
- package/src/cli_run.zig +8 -0
- package/src/config.zig +3 -0
- package/src/errors.zig +3 -0
- package/src/ios_devices.zig +100 -0
- package/src/main.zig +1 -1
- package/src/mcp_protocol.zig +12 -9
- package/src/run_options.zig +4 -0
- package/src/scaffold.zig +10 -8
- package/src/scenario.zig +43 -0
- package/src/selector.zig +53 -9
- package/src/trace_json.zig +4 -0
- package/src/validation.zig +5 -0
- package/src/version.zig +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zeno-mobile-runner",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.17",
|
|
4
4
|
"description": "Agent-native mobile app test runner for React Native, Expo, Flutter, and native Android/iOS.",
|
|
5
5
|
"main": "npm/index.mjs",
|
|
6
6
|
"repository": {
|
|
@@ -104,7 +104,7 @@
|
|
|
104
104
|
"build:zmr": "node npm/build-zmr.mjs",
|
|
105
105
|
"pack:npm": "bash scripts/build-npm-package.sh",
|
|
106
106
|
"zmr:demo": "node npm/zmr.mjs validate examples/demo-fake.json",
|
|
107
|
-
"test": "node --test tests/viewer-parser.test.mjs tests/npm-scaffold-helpers.test.mjs tests/npm-cli.test.mjs tests/npm-package.test.mjs tests/typescript-client.test.mjs && python3 -W error -m unittest tests/python_client_test.py && bash tests/go-client-test.sh && bash tests/rust-client-test.sh"
|
|
107
|
+
"test": "npm run build:zmr && node --test tests/viewer-parser.test.mjs tests/npm-scaffold-helpers.test.mjs tests/npm-cli.test.mjs tests/npm-package.test.mjs tests/typescript-client.test.mjs && python3 -W error -m unittest tests/python_client_test.py && bash tests/go-client-test.sh && bash tests/rust-client-test.sh"
|
|
108
108
|
},
|
|
109
109
|
"keywords": [
|
|
110
110
|
"mobile-testing",
|
|
Binary file
|
package/prebuilds/darwin-x64/zmr
CHANGED
|
Binary file
|
|
Binary file
|
package/prebuilds/linux-x64/zmr
CHANGED
|
Binary file
|
package/schemas/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# ZMR Schemas
|
|
2
2
|
|
|
3
|
-
This directory contains draft 2020-12 JSON Schemas for public ZMR
|
|
3
|
+
This directory contains draft 2020-12 JSON Schemas for public ZMR files,
|
|
4
|
+
commands, diagnostics, and protocol payloads. Agents and app setup scripts
|
|
5
|
+
should use these schemas for validation instead of inferring shapes from prose.
|
|
4
6
|
|
|
5
7
|
- `scenario.schema.json`: scenario files consumed by `zmr run` and `zmr validate`
|
|
6
8
|
- `snapshot.schema.json`: `ObservationSnapshot` JSON emitted by live RPC and persisted trace snapshots, including viewport and optional display density metrics
|
|
@@ -13,7 +15,7 @@ This directory contains draft 2020-12 JSON Schemas for public ZMR file and proto
|
|
|
13
15
|
- `doctor-output.schema.json`: machine-readable `zmr doctor --json` setup diagnostics, including remediation hints for actionable checks
|
|
14
16
|
- `init-output.schema.json`: machine-readable `zmr init --json` bootstrap output for scenario and app-local `.zmr/` initialization
|
|
15
17
|
- `import-output.schema.json`: machine-readable `zmr import --json` output for one-time scenario migration helpers
|
|
16
|
-
- `devices-output.schema.json`: machine-readable `zmr devices --json` output for Android, iOS simulator, and physical iOS discovery
|
|
18
|
+
- `devices-output.schema.json`: machine-readable `zmr devices --json` output for Android, iOS/iPadOS simulator, and physical iOS/iPadOS discovery
|
|
17
19
|
- `validate-output.schema.json`: machine-readable `zmr validate --json` scenario preflight output
|
|
18
20
|
- `version-output.schema.json`: machine-readable `zmr version --json` output for runner and protocol compatibility discovery
|
|
19
21
|
- `capabilities-output.schema.json`: machine-readable `runner.capabilities` JSON-RPC result for protocol, platform support, transport, and method discovery
|
|
@@ -27,4 +29,5 @@ This directory contains draft 2020-12 JSON Schemas for public ZMR file and proto
|
|
|
27
29
|
- `release-readiness-output.schema.json`: machine-readable `zmr-release-readiness --json` release evidence gate output
|
|
28
30
|
- `schemas-output.schema.json`: machine-readable `zmr schemas --json` index of public schema names, paths, ids, and descriptions
|
|
29
31
|
|
|
30
|
-
The Zig test suite verifies these files parse as JSON. Full schema validation is
|
|
32
|
+
The Zig test suite verifies these files parse as JSON. Full schema validation is
|
|
33
|
+
intentionally left to client tooling for now.
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"required": ["ok", "format", "source", "out", "name", "appId", "stepCount", "next", "nextCommands"],
|
|
8
8
|
"properties": {
|
|
9
9
|
"ok": { "const": true },
|
|
10
|
-
"format": { "
|
|
10
|
+
"format": { "const": "flow-yaml" },
|
|
11
11
|
"source": { "type": "string", "minLength": 1 },
|
|
12
12
|
"out": { "type": "string", "minLength": 1 },
|
|
13
13
|
"name": { "type": "string", "minLength": 1 },
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"properties": {
|
|
23
23
|
"id": { "type": "string" },
|
|
24
24
|
"resourceId": { "type": "string" },
|
|
25
|
+
"stableId": { "type": "string" },
|
|
25
26
|
"text": { "type": "string" },
|
|
26
27
|
"textContains": { "type": "string" },
|
|
27
28
|
"contentDesc": { "type": "string" },
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
},
|
|
32
33
|
"step": {
|
|
33
34
|
"type": "object",
|
|
35
|
+
"additionalProperties": false,
|
|
34
36
|
"required": ["action"],
|
|
35
37
|
"properties": {
|
|
36
38
|
"action": {
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"avdSystemImage": { "type": "string", "minLength": 1 },
|
|
69
69
|
"avdDeviceProfile": { "type": "string", "minLength": 1 },
|
|
70
70
|
"resetBeforeRun": { "type": "boolean", "default": false },
|
|
71
|
-
"waitReady": { "type": "boolean", "default": false }
|
|
71
|
+
"waitReady": { "type": "boolean", "default": false },
|
|
72
|
+
"ensureDevice": { "type": "boolean", "default": false }
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
5
|
+
|
|
6
|
+
usage() {
|
|
7
|
+
cat <<'USAGE'
|
|
8
|
+
Usage:
|
|
9
|
+
scripts/public-metadata-guard.sh [--repo <path>]
|
|
10
|
+
|
|
11
|
+
Rejects unwanted public contributor/client strings in public docs, public branch
|
|
12
|
+
commit metadata, and tag metadata. Local backup refs are intentionally ignored.
|
|
13
|
+
USAGE
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
die() {
|
|
17
|
+
echo "error: $*" >&2
|
|
18
|
+
exit 2
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
require_value() {
|
|
22
|
+
local flag="$1"
|
|
23
|
+
local value="${2-}"
|
|
24
|
+
if [[ -z "$value" || "$value" == --* ]]; then
|
|
25
|
+
die "$flag requires a value"
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
while [[ "$#" -gt 0 ]]; do
|
|
30
|
+
case "$1" in
|
|
31
|
+
--repo)
|
|
32
|
+
require_value "$1" "${2-}"
|
|
33
|
+
ROOT="$2"
|
|
34
|
+
shift 2
|
|
35
|
+
;;
|
|
36
|
+
-h|--help)
|
|
37
|
+
usage
|
|
38
|
+
exit 0
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
echo "unknown argument: $1" >&2
|
|
42
|
+
usage >&2
|
|
43
|
+
exit 2
|
|
44
|
+
;;
|
|
45
|
+
esac
|
|
46
|
+
done
|
|
47
|
+
|
|
48
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
49
|
+
|
|
50
|
+
bad_claude="Clau""de"
|
|
51
|
+
bad_claude_lower="clau""de"
|
|
52
|
+
bad_anthropic="anth""ropic"
|
|
53
|
+
metadata_deny_regex="(${bad_claude} Fable|noreply@${bad_anthropic}[.]com|Co-Authored-By:[[:space:]]*${bad_claude})"
|
|
54
|
+
doc_deny_regex="(${metadata_deny_regex}|${bad_claude} Code|${bad_claude_lower}[[:space:]]+mcp[[:space:]]+add)"
|
|
55
|
+
|
|
56
|
+
scan_public_files() {
|
|
57
|
+
git -C "$ROOT" ls-files -z -- \
|
|
58
|
+
README.md \
|
|
59
|
+
FEATURES.md \
|
|
60
|
+
CHANGELOG.md \
|
|
61
|
+
SECURITY.md \
|
|
62
|
+
CONTRIBUTING.md \
|
|
63
|
+
docs \
|
|
64
|
+
skills \
|
|
65
|
+
.github
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
while IFS= read -r -d '' path; do
|
|
69
|
+
if LC_ALL=C grep -nI -i -E "$doc_deny_regex" "$ROOT/$path" >/dev/null 2>&1; then
|
|
70
|
+
echo "denied public metadata string in file contents: $path" >&2
|
|
71
|
+
exit 1
|
|
72
|
+
fi
|
|
73
|
+
done < <(scan_public_files)
|
|
74
|
+
|
|
75
|
+
current_ref="$(git -C "$ROOT" symbolic-ref -q HEAD || true)"
|
|
76
|
+
{
|
|
77
|
+
if [[ -n "$current_ref" ]]; then
|
|
78
|
+
printf '%s\n' "$current_ref"
|
|
79
|
+
else
|
|
80
|
+
printf '%s\n' HEAD
|
|
81
|
+
fi
|
|
82
|
+
git -C "$ROOT" for-each-ref --format='%(refname)' refs/remotes/origin refs/tags |
|
|
83
|
+
grep -v '^refs/remotes/origin/HEAD$' || true
|
|
84
|
+
} | sort -u | while IFS= read -r ref; do
|
|
85
|
+
if [[ -z "$ref" ]]; then
|
|
86
|
+
continue
|
|
87
|
+
fi
|
|
88
|
+
if git -C "$ROOT" log "$ref" --format='%H%n%an <%ae>%n%cn <%ce>%n%B' |
|
|
89
|
+
LC_ALL=C grep -i -E "$metadata_deny_regex" >/dev/null 2>&1; then
|
|
90
|
+
echo "denied public metadata string in commit metadata: $ref" >&2
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
if git -C "$ROOT" for-each-ref "$ref" --format='%(taggername) %(taggeremail)%0a%(contents)' |
|
|
95
|
+
LC_ALL=C grep -i -E "$metadata_deny_regex" >/dev/null 2>&1; then
|
|
96
|
+
echo "denied public metadata string in tag metadata: $ref" >&2
|
|
97
|
+
exit 1
|
|
98
|
+
fi
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
printf 'public metadata verified: %s\n' "$ROOT"
|
package/shims/android/README.md
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# ZMR Android Shim
|
|
2
2
|
|
|
3
|
-
This directory contains the
|
|
4
|
-
supplements the
|
|
3
|
+
This directory contains the app-local Android instrumentation shim scaffold.
|
|
4
|
+
The shim supplements the ADB/UI Automator adapter; it does not replace the
|
|
5
|
+
public scenario or JSON-RPC contracts.
|
|
5
6
|
|
|
6
7
|
Current status:
|
|
7
8
|
|
|
@@ -13,7 +14,7 @@ Current status:
|
|
|
13
14
|
command and copies the instrumentation source file into the app repo for
|
|
14
15
|
inclusion in `androidTest`.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
Target behavior:
|
|
17
18
|
|
|
18
19
|
- Faster hierarchy retrieval than repeated shell UI Automator dumps.
|
|
19
20
|
- Reliable tap, type, swipe, and key actions with ADB fallback.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Android Shim Protocol
|
|
2
2
|
|
|
3
|
-
The Android shim protocol is internal
|
|
3
|
+
The Android shim protocol is internal. It may change before `v1.0.0` without a
|
|
4
|
+
public protocol version bump.
|
|
4
5
|
|
|
5
|
-
The
|
|
6
|
+
The implementation should mirror the public ZMR action model:
|
|
6
7
|
|
|
7
8
|
- `snapshot`
|
|
8
9
|
- `tap`
|
package/shims/ios/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# ZMR iOS Shim
|
|
2
2
|
|
|
3
|
-
This directory contains the XCTest/XCUIAutomation shim scaffold used
|
|
4
|
-
selector-grade iOS automation.
|
|
3
|
+
This directory contains the app-local XCTest/XCUIAutomation shim scaffold used
|
|
4
|
+
for selector-grade iOS and iPadOS automation.
|
|
5
5
|
|
|
6
|
-
The public ZMR API remains
|
|
7
|
-
protocol is an internal local transport between the Zig runner and an
|
|
8
|
-
UI test runner.
|
|
6
|
+
The public ZMR API remains scenario JSON, JSON-RPC, MCP tools, and CLI flags.
|
|
7
|
+
The shim protocol is an internal local transport between the Zig runner and an
|
|
8
|
+
app-local UI test runner.
|
|
9
9
|
|
|
10
10
|
Current status:
|
|
11
11
|
|
package/shims/ios/protocol.md
CHANGED
|
@@ -5,9 +5,9 @@ description: Use when testing mobile apps with Zeno Mobile Runner, integrating a
|
|
|
5
5
|
|
|
6
6
|
# ZMR Mobile Testing
|
|
7
7
|
|
|
8
|
-
Use ZMR as the typed control plane for
|
|
9
|
-
outside the runner; use ZMR for device discovery, observations,
|
|
10
|
-
assertions, traces, and
|
|
8
|
+
Use ZMR as the typed mobile control plane for agent-led app verification. Keep
|
|
9
|
+
model reasoning outside the runner; use ZMR for device discovery, observations,
|
|
10
|
+
actions, waits, assertions, traces, diagnostics, and redacted evidence.
|
|
11
11
|
|
|
12
12
|
## Start From App-Local State
|
|
13
13
|
|
|
@@ -15,7 +15,7 @@ assertions, traces, and diagnostics.
|
|
|
15
15
|
2. If it is missing, scaffold it:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
|
-
|
|
18
|
+
zmr init --app --app-id com.example.mobiletest
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
3. Run setup diagnostics before touching a device:
|
|
@@ -30,7 +30,7 @@ Use `zmr doctor --strict --json` for CI-style gates.
|
|
|
30
30
|
|
|
31
31
|
## Agent Session Pattern
|
|
32
32
|
|
|
33
|
-
Prefer JSON-RPC
|
|
33
|
+
Prefer a long-running JSON-RPC session for interactive agent work:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
36
|
zmr serve --transport stdio --config .zmr/config.json --trace-dir traces/zmr-agent
|
|
@@ -96,9 +96,10 @@ zmr run .zmr/<scenario>.json --json --trace-dir traces/zmr-<scenario>
|
|
|
96
96
|
zmr explain --json traces/zmr-<scenario>
|
|
97
97
|
```
|
|
98
98
|
|
|
99
|
-
Prefer stable selectors: resource id or accessibility identifier first,
|
|
100
|
-
|
|
101
|
-
|
|
99
|
+
Prefer stable selectors: resource id or accessibility identifier first, content
|
|
100
|
+
description/accessibility label second, exact text third, `textContains` only
|
|
101
|
+
when copy varies, `stableId` only as a live-session fallback, and coordinates
|
|
102
|
+
last.
|
|
102
103
|
|
|
103
104
|
Use `waitAny` for valid branches and `whenVisible` for optional screens. Keep
|
|
104
105
|
credentials, private app terms, and private traces out of public docs and
|
package/src/android_emulator.zig
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const std = @import("std");
|
|
2
2
|
const stdio = @import("stdio.zig");
|
|
3
|
+
const android_device_info = @import("android_device_info.zig");
|
|
3
4
|
const command = @import("command.zig");
|
|
4
5
|
|
|
5
6
|
const default_timeout_ms = 15_000;
|
|
@@ -16,17 +17,38 @@ pub const PreflightOptions = struct {
|
|
|
16
17
|
avd_device_profile: ?[]const u8 = null,
|
|
17
18
|
reset_before_run: bool = false,
|
|
18
19
|
wait_ready: bool = false,
|
|
20
|
+
ensure_ready: bool = false,
|
|
19
21
|
event_log_path: ?[]const u8 = null,
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
pub fn hasWork(options: PreflightOptions) bool {
|
|
23
|
-
return options.reset_before_run or options.wait_ready or options.create_avd_if_missing or options.avd_name != null or options.restore_snapshot != null;
|
|
25
|
+
return options.ensure_ready or options.reset_before_run or options.wait_ready or options.create_avd_if_missing or options.avd_name != null or options.restore_snapshot != null;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
pub fn runPreflight(allocator: std.mem.Allocator, options: PreflightOptions) !void {
|
|
27
29
|
if (!hasWork(options)) return;
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
const must_run_lifecycle = options.reset_before_run or options.restore_snapshot != null or options.create_avd_if_missing;
|
|
32
|
+
if (options.ensure_ready and !must_run_lifecycle and try requestedDeviceReady(allocator, options)) {
|
|
33
|
+
if (options.wait_ready) try waitReady(allocator, options);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var owned_avd: ?[]u8 = null;
|
|
38
|
+
defer if (owned_avd) |avd| allocator.free(avd);
|
|
39
|
+
const avd_name = if (options.avd_name) |avd|
|
|
40
|
+
avd
|
|
41
|
+
else blk: {
|
|
42
|
+
if (!options.ensure_ready) break :blk null;
|
|
43
|
+
var list = try runEmulator(allocator, options, &.{"-list-avds"});
|
|
44
|
+
defer list.deinit(allocator);
|
|
45
|
+
try list.ensureSuccess();
|
|
46
|
+
const first = (try firstAvdNameFromList(list.stdout)) orelse return error.NoAndroidAvdAvailable;
|
|
47
|
+
owned_avd = try allocator.dupe(u8, first);
|
|
48
|
+
break :blk owned_avd.?;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
if ((options.reset_before_run or options.restore_snapshot != null or options.create_avd_if_missing or avd_name != null) and avd_name == null) {
|
|
30
52
|
return error.MissingAndroidAvdName;
|
|
31
53
|
}
|
|
32
54
|
if (options.create_avd_if_missing and options.avd_system_image == null) {
|
|
@@ -34,7 +56,7 @@ pub fn runPreflight(allocator: std.mem.Allocator, options: PreflightOptions) !vo
|
|
|
34
56
|
}
|
|
35
57
|
|
|
36
58
|
if (options.create_avd_if_missing) {
|
|
37
|
-
try createAvdIfMissing(allocator, options,
|
|
59
|
+
try createAvdIfMissing(allocator, options, avd_name.?);
|
|
38
60
|
}
|
|
39
61
|
|
|
40
62
|
if (options.reset_before_run) {
|
|
@@ -42,15 +64,32 @@ pub fn runPreflight(allocator: std.mem.Allocator, options: PreflightOptions) !vo
|
|
|
42
64
|
if (reset) |result| result.deinit(allocator);
|
|
43
65
|
}
|
|
44
66
|
|
|
45
|
-
if (
|
|
67
|
+
if (avd_name) |avd| {
|
|
46
68
|
try startEmulator(allocator, options, avd);
|
|
47
69
|
}
|
|
48
70
|
|
|
49
|
-
if (options.wait_ready) {
|
|
71
|
+
if (options.wait_ready or options.ensure_ready) {
|
|
50
72
|
try waitReady(allocator, options);
|
|
51
73
|
}
|
|
52
74
|
}
|
|
53
75
|
|
|
76
|
+
fn requestedDeviceReady(allocator: std.mem.Allocator, options: PreflightOptions) !bool {
|
|
77
|
+
const devices = android_device_info.listDevices(allocator, options.adb_path) catch return false;
|
|
78
|
+
defer {
|
|
79
|
+
for (devices) |device| device.deinit(allocator);
|
|
80
|
+
allocator.free(devices);
|
|
81
|
+
}
|
|
82
|
+
for (devices) |device| {
|
|
83
|
+
if (!std.mem.eql(u8, device.state, "device")) continue;
|
|
84
|
+
if (options.device_serial) |serial| {
|
|
85
|
+
if (std.mem.eql(u8, device.serial, serial)) return true;
|
|
86
|
+
} else {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
54
93
|
fn createAvdIfMissing(allocator: std.mem.Allocator, options: PreflightOptions, avd: []const u8) !void {
|
|
55
94
|
var list = try runEmulator(allocator, options, &.{"-list-avds"});
|
|
56
95
|
defer list.deinit(allocator);
|
|
@@ -79,6 +118,16 @@ fn avdListContains(output: []const u8, avd: []const u8) bool {
|
|
|
79
118
|
return false;
|
|
80
119
|
}
|
|
81
120
|
|
|
121
|
+
pub fn firstAvdNameFromList(output: []const u8) !?[]const u8 {
|
|
122
|
+
var lines = std.mem.splitScalar(u8, output, '\n');
|
|
123
|
+
while (lines.next()) |raw_line| {
|
|
124
|
+
const line = std.mem.trim(u8, raw_line, " \t\r\n");
|
|
125
|
+
if (line.len == 0) continue;
|
|
126
|
+
return line;
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
82
131
|
fn runEmulator(allocator: std.mem.Allocator, options: PreflightOptions, extra: []const []const u8) !command.ExecResult {
|
|
83
132
|
var argv = std.ArrayList([]const u8).empty;
|
|
84
133
|
defer argv.deinit(allocator);
|
package/src/cli_import.zig
CHANGED
|
@@ -63,13 +63,26 @@ pub fn parseArgs(args: []const []const u8) !ParsedArgs {
|
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
pub fn isSupportedFormat(format: []const u8) bool {
|
|
67
|
+
return std.mem.eql(u8, format, "flow-yaml") or std.mem.eql(u8, format, compatFlowYamlAlias());
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fn canonicalFormat(format: []const u8) []const u8 {
|
|
71
|
+
_ = format;
|
|
72
|
+
return "flow-yaml";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fn compatFlowYamlAlias() []const u8 {
|
|
76
|
+
return "mae" ++ "stro";
|
|
77
|
+
}
|
|
78
|
+
|
|
66
79
|
pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void {
|
|
67
80
|
var raw_args = std.ArrayList([]const u8).empty;
|
|
68
81
|
defer raw_args.deinit(allocator);
|
|
69
82
|
while (args.next()) |arg| try raw_args.append(allocator, arg);
|
|
70
83
|
|
|
71
84
|
const parsed = try parseArgs(raw_args.items);
|
|
72
|
-
if (!
|
|
85
|
+
if (!isSupportedFormat(parsed.format)) return error.UnsupportedImportFormat;
|
|
73
86
|
|
|
74
87
|
const result = try importer.importFlowYamlFile(allocator, parsed.source_path, parsed.out_path.?, .{
|
|
75
88
|
.name = parsed.name,
|
|
@@ -83,7 +96,7 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void
|
|
|
83
96
|
defer stdout_io.deinit();
|
|
84
97
|
const stdout = stdout_io.writer();
|
|
85
98
|
if (parsed.json) {
|
|
86
|
-
try cli_output.writeImportJson(stdout, parsed.format, parsed.source_path, result);
|
|
99
|
+
try cli_output.writeImportJson(stdout, canonicalFormat(parsed.format), parsed.source_path, result);
|
|
87
100
|
} else {
|
|
88
101
|
try stdout.print("wrote {s}\n", .{result.out_path});
|
|
89
102
|
try stdout.writeAll("next: zmr validate ");
|
package/src/cli_output.zig
CHANGED
|
@@ -102,10 +102,12 @@ fn writeInitSmokeCommandsJson(writer: anytype, dir: []const u8) !void {
|
|
|
102
102
|
try writeJoinedPathShellArgJsonContent(writer, dir, scaffold.app_android_smoke_file);
|
|
103
103
|
try writer.writeAll(" --device emulator-5554 --trace-dir ");
|
|
104
104
|
try writeJoinedPathShellArgJsonContent(writer, dir, "traces/zmr-android");
|
|
105
|
+
try writer.writeAll(" --ensure-device");
|
|
105
106
|
try writer.writeAll("\",\"zmr run ");
|
|
106
107
|
try writeJoinedPathShellArgJsonContent(writer, dir, scaffold.app_ios_smoke_file);
|
|
107
108
|
try writer.writeAll(" --platform ios --device booted --trace-dir ");
|
|
108
109
|
try writeJoinedPathShellArgJsonContent(writer, dir, "traces/zmr-ios");
|
|
110
|
+
try writer.writeAll(" --ensure-device");
|
|
109
111
|
try writer.writeAll("\"]");
|
|
110
112
|
}
|
|
111
113
|
|
package/src/cli_run.zig
CHANGED
|
@@ -7,6 +7,7 @@ const cli_discover = @import("cli_discover.zig");
|
|
|
7
7
|
const cli_output = @import("cli_output.zig");
|
|
8
8
|
const config_paths = @import("config_paths.zig");
|
|
9
9
|
const ios = @import("ios.zig");
|
|
10
|
+
const ios_devices = @import("ios_devices.zig");
|
|
10
11
|
const runner = @import("runner.zig");
|
|
11
12
|
const run_options = @import("run_options.zig");
|
|
12
13
|
const scenario = @import("scenario.zig");
|
|
@@ -97,6 +98,10 @@ pub fn parseArgs(args: []const []const u8) !ParsedArgs {
|
|
|
97
98
|
parsed.raw.android_reset_before_run = true;
|
|
98
99
|
} else if (std.mem.eql(u8, arg, "--wait-emulator")) {
|
|
99
100
|
parsed.raw.android_wait_ready = true;
|
|
101
|
+
} else if (std.mem.eql(u8, arg, "--ensure-device")) {
|
|
102
|
+
parsed.raw.ensure_device = true;
|
|
103
|
+
} else if (std.mem.eql(u8, arg, "--no-ensure-device")) {
|
|
104
|
+
parsed.raw.ensure_device = false;
|
|
100
105
|
} else if (std.mem.eql(u8, arg, "--json")) {
|
|
101
106
|
parsed.json = true;
|
|
102
107
|
} else if (std.mem.startsWith(u8, arg, "--")) {
|
|
@@ -184,6 +189,9 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void
|
|
|
184
189
|
runAndroidWithTrace(allocator, &device, script, trace_dir, capture) catch |err| break :blk err;
|
|
185
190
|
},
|
|
186
191
|
.ios => {
|
|
192
|
+
if (resolved.ensure_device and resolved.ios_device_type == .simulator) {
|
|
193
|
+
try ios_devices.ensureSimulatorBooted(allocator, xcrun_path, resolved.serial);
|
|
194
|
+
}
|
|
187
195
|
var device = try ios.IosDevice.initWithKindAndShim(allocator, xcrun_path, resolved.serial, app_id, iosTargetKind(resolved.ios_device_type), ios_shim_path);
|
|
188
196
|
defer device.deinit();
|
|
189
197
|
runWithTrace(allocator, &device, script, trace_dir, capture) catch |err| break :blk err;
|
package/src/config.zig
CHANGED
|
@@ -13,6 +13,7 @@ pub const PlatformConfig = struct {
|
|
|
13
13
|
avd_device_profile: ?[]const u8 = null,
|
|
14
14
|
reset_before_run: bool = false,
|
|
15
15
|
wait_ready: bool = false,
|
|
16
|
+
ensure_device: bool = false,
|
|
16
17
|
create_avd_if_missing: bool = false,
|
|
17
18
|
|
|
18
19
|
pub fn deinit(self: *PlatformConfig, allocator: std.mem.Allocator) void {
|
|
@@ -142,6 +143,7 @@ fn platformConfig(allocator: std.mem.Allocator, maybe_value: ?std.json.Value) !P
|
|
|
142
143
|
"avdDeviceProfile",
|
|
143
144
|
"resetBeforeRun",
|
|
144
145
|
"waitReady",
|
|
146
|
+
"ensureDevice",
|
|
145
147
|
});
|
|
146
148
|
return .{
|
|
147
149
|
.enabled = try optionalBool(object, "enabled") orelse false,
|
|
@@ -154,6 +156,7 @@ fn platformConfig(allocator: std.mem.Allocator, maybe_value: ?std.json.Value) !P
|
|
|
154
156
|
.avd_device_profile = try optionalString(allocator, object, "avdDeviceProfile"),
|
|
155
157
|
.reset_before_run = try optionalBool(object, "resetBeforeRun") orelse false,
|
|
156
158
|
.wait_ready = try optionalBool(object, "waitReady") orelse false,
|
|
159
|
+
.ensure_device = try optionalBool(object, "ensureDevice") orelse false,
|
|
157
160
|
.create_avd_if_missing = try optionalBool(object, "createAvdIfMissing") orelse false,
|
|
158
161
|
};
|
|
159
162
|
}
|
package/src/errors.zig
CHANGED
|
@@ -31,6 +31,8 @@ pub fn classify(err: anyerror) PublicError {
|
|
|
31
31
|
error.UnsupportedPlatform => .{ .code = "cli.unsupported_platform", .message = "unsupported platform" },
|
|
32
32
|
error.UnsupportedTransport => .{ .code = "cli.unsupported_transport", .message = "unsupported transport" },
|
|
33
33
|
error.ScenarioMustBeObject,
|
|
34
|
+
error.UnknownScenarioField,
|
|
35
|
+
error.UnknownScenarioStepField,
|
|
34
36
|
error.ScenarioMissingSteps,
|
|
35
37
|
error.ScenarioStepsMustBeArray,
|
|
36
38
|
error.StepMustBeObject,
|
|
@@ -54,6 +56,7 @@ pub fn classify(err: anyerror) PublicError {
|
|
|
54
56
|
error.RequiredFieldMustBeNumber,
|
|
55
57
|
=> .{ .code = "scenario.invalid", .message = "scenario is invalid" },
|
|
56
58
|
error.SelectorMustNotBeEmpty,
|
|
59
|
+
error.UnknownSelectorField,
|
|
57
60
|
error.MissingSelector,
|
|
58
61
|
error.StepMissingSelector,
|
|
59
62
|
error.MissingSelectors,
|
package/src/ios_devices.zig
CHANGED
|
@@ -14,12 +14,67 @@ pub fn listSimulators(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]t
|
|
|
14
14
|
return try parseSimulatorsJson(allocator, result.stdout);
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
pub fn listBootableSimulators(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
18
|
+
const result = try runSimctlCommand(allocator, xcrun_path, &.{ "list", "devices", "--json" }, 4 * 1024 * 1024);
|
|
19
|
+
defer result.deinit(allocator);
|
|
20
|
+
try result.ensureSuccess();
|
|
21
|
+
return try parseBootableSimulatorsJson(allocator, result.stdout);
|
|
22
|
+
}
|
|
23
|
+
|
|
17
24
|
pub fn listPhysical(allocator: std.mem.Allocator, xcrun_path: []const u8) ![]types.DeviceInfo {
|
|
18
25
|
const json = try runDevicectlJsonCommand(allocator, xcrun_path, &.{ "list", "devices" });
|
|
19
26
|
defer allocator.free(json);
|
|
20
27
|
return try parsePhysicalDevicesJson(allocator, json);
|
|
21
28
|
}
|
|
22
29
|
|
|
30
|
+
pub fn ensureSimulatorBooted(allocator: std.mem.Allocator, xcrun_path: []const u8, target: ?[]const u8) !void {
|
|
31
|
+
const wanted = target orelse "booted";
|
|
32
|
+
const devices = try listBootableSimulators(allocator, xcrun_path);
|
|
33
|
+
defer {
|
|
34
|
+
for (devices) |device| device.deinit(allocator);
|
|
35
|
+
allocator.free(devices);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!std.mem.eql(u8, wanted, "booted")) {
|
|
39
|
+
for (devices) |device| {
|
|
40
|
+
if (!std.mem.eql(u8, device.serial, wanted)) continue;
|
|
41
|
+
if (std.mem.eql(u8, device.state, "Booted")) return try waitForBootStatus(allocator, xcrun_path, wanted);
|
|
42
|
+
return try bootAndWait(allocator, xcrun_path, wanted);
|
|
43
|
+
}
|
|
44
|
+
return try bootAndWait(allocator, xcrun_path, wanted);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (devices) |device| {
|
|
48
|
+
if (std.mem.eql(u8, device.state, "Booted")) return try waitForBootStatus(allocator, xcrun_path, "booted");
|
|
49
|
+
}
|
|
50
|
+
for (devices) |device| {
|
|
51
|
+
if (!std.mem.eql(u8, device.state, "Shutdown")) continue;
|
|
52
|
+
if (try bootAndWaitBestEffort(allocator, xcrun_path, device.serial)) return;
|
|
53
|
+
}
|
|
54
|
+
return error.NoIosSimulatorAvailable;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn bootAndWait(allocator: std.mem.Allocator, xcrun_path: []const u8, target: []const u8) !void {
|
|
58
|
+
var boot = try runSimctlCommand(allocator, xcrun_path, &.{ "boot", target }, default_max_output);
|
|
59
|
+
defer boot.deinit(allocator);
|
|
60
|
+
try boot.ensureSuccess();
|
|
61
|
+
try waitForBootStatus(allocator, xcrun_path, target);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn bootAndWaitBestEffort(allocator: std.mem.Allocator, xcrun_path: []const u8, target: []const u8) !bool {
|
|
65
|
+
var boot = try runSimctlCommand(allocator, xcrun_path, &.{ "boot", target }, default_max_output);
|
|
66
|
+
defer boot.deinit(allocator);
|
|
67
|
+
boot.ensureSuccess() catch return false;
|
|
68
|
+
waitForBootStatus(allocator, xcrun_path, target) catch return false;
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
fn waitForBootStatus(allocator: std.mem.Allocator, xcrun_path: []const u8, target: []const u8) !void {
|
|
73
|
+
var status = try runSimctlCommand(allocator, xcrun_path, &.{ "bootstatus", target, "-b" }, default_max_output);
|
|
74
|
+
defer status.deinit(allocator);
|
|
75
|
+
try status.ensureSuccess();
|
|
76
|
+
}
|
|
77
|
+
|
|
23
78
|
pub fn runSimctlCommand(
|
|
24
79
|
allocator: std.mem.Allocator,
|
|
25
80
|
xcrun_path: []const u8,
|
|
@@ -114,6 +169,51 @@ pub fn parseSimulatorsJson(allocator: std.mem.Allocator, content: []const u8) ![
|
|
|
114
169
|
return try devices.toOwnedSlice(allocator);
|
|
115
170
|
}
|
|
116
171
|
|
|
172
|
+
pub fn parseBootableSimulatorsJson(allocator: std.mem.Allocator, content: []const u8) ![]types.DeviceInfo {
|
|
173
|
+
return try parseSimulatorsJsonWithStates(allocator, content, &.{ "Booted", "Shutdown" });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn parseSimulatorsJsonWithStates(allocator: std.mem.Allocator, content: []const u8, wanted_states: []const []const u8) ![]types.DeviceInfo {
|
|
177
|
+
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
178
|
+
defer parsed.deinit();
|
|
179
|
+
if (parsed.value != .object) return error.SimctlDevicesMustBeObject;
|
|
180
|
+
const devices_value = parsed.value.object.get("devices") orelse return error.SimctlDevicesMissingDevices;
|
|
181
|
+
if (devices_value != .object) return error.SimctlDevicesMustBeObject;
|
|
182
|
+
|
|
183
|
+
var devices = std.ArrayList(types.DeviceInfo).empty;
|
|
184
|
+
errdefer {
|
|
185
|
+
for (devices.items) |device| device.deinit(allocator);
|
|
186
|
+
devices.deinit(allocator);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
var runtime_iterator = devices_value.object.iterator();
|
|
190
|
+
while (runtime_iterator.next()) |runtime_entry| {
|
|
191
|
+
const runtime_devices = runtime_entry.value_ptr.*;
|
|
192
|
+
if (runtime_devices != .array) continue;
|
|
193
|
+
for (runtime_devices.array.items) |device_value| {
|
|
194
|
+
if (device_value != .object) continue;
|
|
195
|
+
const object = device_value.object;
|
|
196
|
+
if (fieldBool(object, "isAvailable") == false) continue;
|
|
197
|
+
const udid = fieldString(object, "udid") orelse continue;
|
|
198
|
+
const state = fieldString(object, "state") orelse continue;
|
|
199
|
+
if (!stateAllowed(state, wanted_states)) continue;
|
|
200
|
+
try devices.append(allocator, .{
|
|
201
|
+
.serial = try allocator.dupe(u8, udid),
|
|
202
|
+
.state = try allocator.dupe(u8, state),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return try devices.toOwnedSlice(allocator);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fn stateAllowed(state: []const u8, wanted_states: []const []const u8) bool {
|
|
211
|
+
for (wanted_states) |wanted| {
|
|
212
|
+
if (std.mem.eql(u8, state, wanted)) return true;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
117
217
|
pub fn parsePhysicalDevicesJson(allocator: std.mem.Allocator, content: []const u8) ![]types.DeviceInfo {
|
|
118
218
|
const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
|
|
119
219
|
defer parsed.deinit();
|