zig-mobile-runner 0.1.0

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.
Files changed (225) hide show
  1. package/CHANGELOG.md +484 -0
  2. package/CONTRIBUTING.md +42 -0
  3. package/FEATURES.md +112 -0
  4. package/LICENSE +21 -0
  5. package/README.md +255 -0
  6. package/SECURITY.md +34 -0
  7. package/build.zig +38 -0
  8. package/build.zig.zon +7 -0
  9. package/clients/README.md +144 -0
  10. package/clients/go/README.md +24 -0
  11. package/clients/go/examples/fake-session/main.go +93 -0
  12. package/clients/go/go.mod +3 -0
  13. package/clients/go/zmr/client.go +432 -0
  14. package/clients/kotlin/README.md +35 -0
  15. package/clients/kotlin/build.gradle.kts +35 -0
  16. package/clients/kotlin/settings.gradle.kts +15 -0
  17. package/clients/kotlin/src/main/kotlin/dev/zmr/FakeSession.kt +86 -0
  18. package/clients/kotlin/src/main/kotlin/dev/zmr/ZmrClient.kt +67 -0
  19. package/clients/python/README.md +29 -0
  20. package/clients/python/examples/fake_session.py +48 -0
  21. package/clients/python/pyproject.toml +13 -0
  22. package/clients/python/zmr_client.py +202 -0
  23. package/clients/rust/Cargo.lock +107 -0
  24. package/clients/rust/Cargo.toml +10 -0
  25. package/clients/rust/README.md +19 -0
  26. package/clients/rust/examples/fake_session.rs +70 -0
  27. package/clients/rust/src/lib.rs +461 -0
  28. package/clients/swift/Package.swift +16 -0
  29. package/clients/swift/README.md +36 -0
  30. package/clients/swift/Sources/ZMRClient/ZMRClient.swift +114 -0
  31. package/clients/swift/Sources/ZMRFakeSession/main.swift +86 -0
  32. package/clients/typescript/README.md +34 -0
  33. package/clients/typescript/examples/fake-session.mjs +36 -0
  34. package/clients/typescript/index.d.ts +144 -0
  35. package/clients/typescript/index.mjs +192 -0
  36. package/clients/typescript/package.json +8 -0
  37. package/docs/adr/0001-agent-native-runner-boundary.md +31 -0
  38. package/docs/adr/0002-app-local-zmr-contract.md +39 -0
  39. package/docs/adr/0003-ios-simulator-xctest-shim.md +41 -0
  40. package/docs/adr/0004-benchmark-claims-and-baseline-collection.md +37 -0
  41. package/docs/adr/README.md +12 -0
  42. package/docs/ai-agents.md +156 -0
  43. package/docs/app-integration.md +316 -0
  44. package/docs/benchmarking.md +275 -0
  45. package/docs/client-installation.md +141 -0
  46. package/docs/clients.md +98 -0
  47. package/docs/config.md +175 -0
  48. package/docs/demo.md +259 -0
  49. package/docs/dsl.md +57 -0
  50. package/docs/install.md +233 -0
  51. package/docs/market-positioning.md +70 -0
  52. package/docs/npm.md +359 -0
  53. package/docs/protocol-fixtures/README.md +8 -0
  54. package/docs/protocol-fixtures/core-session.requests.jsonl +8 -0
  55. package/docs/protocol-fixtures/core-session.responses.jsonl +8 -0
  56. package/docs/protocol-versioning.md +65 -0
  57. package/docs/protocol.md +560 -0
  58. package/docs/publication.md +77 -0
  59. package/docs/release-audit.md +99 -0
  60. package/docs/release-candidate.md +111 -0
  61. package/docs/release-evidence.md +188 -0
  62. package/docs/release-notes-template.md +58 -0
  63. package/docs/roadmap.md +334 -0
  64. package/docs/scenario-authoring.md +88 -0
  65. package/docs/shipping.md +170 -0
  66. package/docs/trace-privacy.md +88 -0
  67. package/docs/troubleshooting.md +256 -0
  68. package/examples/android-app-auth-probe.json +89 -0
  69. package/examples/android-app-error-state.json +13 -0
  70. package/examples/android-app-login-smoke.json +192 -0
  71. package/examples/android-app-onboarding.json +12 -0
  72. package/examples/android-app-referral-deep-link.json +12 -0
  73. package/examples/android-shim-smoke.json +19 -0
  74. package/examples/demo-failure.json +12 -0
  75. package/examples/demo-fake.json +14 -0
  76. package/examples/ios-dev-client-open-link.json +26 -0
  77. package/examples/ios-dev-client-route-snapshot.json +24 -0
  78. package/examples/ios-shim-smoke.json +23 -0
  79. package/examples/ios-smoke.json +9 -0
  80. package/go.work +3 -0
  81. package/npm/agents.mjs +183 -0
  82. package/npm/app-config.mjs +95 -0
  83. package/npm/build-zmr.mjs +21 -0
  84. package/npm/commands.mjs +104 -0
  85. package/npm/generated-files.mjs +50 -0
  86. package/npm/index.mjs +75 -0
  87. package/npm/init-app.mjs +80 -0
  88. package/npm/package-scripts.mjs +72 -0
  89. package/npm/postinstall.mjs +21 -0
  90. package/npm/scaffold.mjs +179 -0
  91. package/npm/scenarios.mjs +93 -0
  92. package/npm/setup.mjs +69 -0
  93. package/npm/wizard.mjs +117 -0
  94. package/npm/zmr.mjs +23 -0
  95. package/package.json +114 -0
  96. package/prebuilds/darwin-arm64/zmr +0 -0
  97. package/prebuilds/darwin-x64/zmr +0 -0
  98. package/prebuilds/linux-arm64/zmr +0 -0
  99. package/prebuilds/linux-x64/zmr +0 -0
  100. package/schemas/README.md +26 -0
  101. package/schemas/action-result.schema.json +27 -0
  102. package/schemas/capabilities-output.schema.json +98 -0
  103. package/schemas/devices-output.schema.json +25 -0
  104. package/schemas/doctor-output.schema.json +51 -0
  105. package/schemas/explain-output.schema.json +51 -0
  106. package/schemas/import-output.schema.json +23 -0
  107. package/schemas/init-output.schema.json +71 -0
  108. package/schemas/json-rpc.schema.json +55 -0
  109. package/schemas/release-manifest.schema.json +43 -0
  110. package/schemas/release-readiness-output.schema.json +127 -0
  111. package/schemas/run-output.schema.json +43 -0
  112. package/schemas/scenario.schema.json +128 -0
  113. package/schemas/schemas-output.schema.json +26 -0
  114. package/schemas/semantic-snapshot.schema.json +116 -0
  115. package/schemas/snapshot.schema.json +60 -0
  116. package/schemas/trace-event.schema.json +14 -0
  117. package/schemas/trace-manifest.schema.json +59 -0
  118. package/schemas/validate-output.schema.json +42 -0
  119. package/schemas/version-output.schema.json +23 -0
  120. package/schemas/zmr-config.schema.json +75 -0
  121. package/scripts/android-emulator.sh +126 -0
  122. package/scripts/assert-ios-physical-ready.sh +213 -0
  123. package/scripts/benchmark-command.sh +307 -0
  124. package/scripts/benchmark.sh +359 -0
  125. package/scripts/benchmark_gate.py +117 -0
  126. package/scripts/benchmark_result_row.py +88 -0
  127. package/scripts/compare-benchmarks.py +288 -0
  128. package/scripts/create-android-demo-app.sh +342 -0
  129. package/scripts/create-ios-demo-app.sh +261 -0
  130. package/scripts/demo-android-real.sh +232 -0
  131. package/scripts/demo-ios-real.sh +270 -0
  132. package/scripts/demo.sh +464 -0
  133. package/scripts/device-matrix.sh +338 -0
  134. package/scripts/ensure-ios-shim-target.rb +237 -0
  135. package/scripts/install-android-shim.sh +281 -0
  136. package/scripts/install-ios-shim.sh +589 -0
  137. package/scripts/pilot-gate.sh +560 -0
  138. package/scripts/release-readiness.py +838 -0
  139. package/scripts/release-readiness.sh +91 -0
  140. package/scripts/run-android-pilot.sh +561 -0
  141. package/scripts/run-ios-pilot.sh +509 -0
  142. package/shims/android/README.md +21 -0
  143. package/shims/android/ZMRShimInstrumentedTest.java +152 -0
  144. package/shims/android/protocol.md +18 -0
  145. package/shims/ios/README.md +50 -0
  146. package/shims/ios/ZMRShim.swift +110 -0
  147. package/shims/ios/ZMRShimUITestCase.swift +475 -0
  148. package/shims/ios/protocol.md +74 -0
  149. package/skills/zmr-mobile-testing/SKILL.md +127 -0
  150. package/src/android.zig +344 -0
  151. package/src/android_device_info.zig +99 -0
  152. package/src/android_emulator.zig +154 -0
  153. package/src/android_screen_recording.zig +112 -0
  154. package/src/android_shell.zig +112 -0
  155. package/src/bundle.zig +124 -0
  156. package/src/bundle_redaction.zig +272 -0
  157. package/src/bundle_tar.zig +123 -0
  158. package/src/cli_devices.zig +97 -0
  159. package/src/cli_doctor.zig +114 -0
  160. package/src/cli_import.zig +70 -0
  161. package/src/cli_info.zig +39 -0
  162. package/src/cli_init.zig +72 -0
  163. package/src/cli_output.zig +467 -0
  164. package/src/cli_run.zig +259 -0
  165. package/src/cli_serve.zig +287 -0
  166. package/src/cli_trace.zig +111 -0
  167. package/src/cli_validate.zig +41 -0
  168. package/src/command.zig +211 -0
  169. package/src/config.zig +305 -0
  170. package/src/config_diagnostics.zig +212 -0
  171. package/src/config_paths.zig +49 -0
  172. package/src/device_registry.zig +37 -0
  173. package/src/doctor.zig +412 -0
  174. package/src/doctor_hints.zig +52 -0
  175. package/src/errors.zig +55 -0
  176. package/src/fake_device.zig +163 -0
  177. package/src/health.zig +28 -0
  178. package/src/importer.zig +343 -0
  179. package/src/importer_json.zig +100 -0
  180. package/src/importer_model.zig +103 -0
  181. package/src/ios.zig +399 -0
  182. package/src/ios_devices.zig +219 -0
  183. package/src/ios_lifecycle.zig +72 -0
  184. package/src/ios_shim.zig +242 -0
  185. package/src/ios_snapshot.zig +20 -0
  186. package/src/json_fields.zig +80 -0
  187. package/src/json_rpc.zig +150 -0
  188. package/src/json_rpc_methods.zig +318 -0
  189. package/src/json_rpc_observation.zig +31 -0
  190. package/src/json_rpc_params.zig +52 -0
  191. package/src/json_rpc_protocol.zig +110 -0
  192. package/src/json_rpc_trace.zig +73 -0
  193. package/src/main.zig +135 -0
  194. package/src/mcp.zig +234 -0
  195. package/src/mcp_protocol.zig +64 -0
  196. package/src/mcp_trace.zig +83 -0
  197. package/src/report.zig +346 -0
  198. package/src/report_html.zig +63 -0
  199. package/src/report_values.zig +27 -0
  200. package/src/run_options.zig +152 -0
  201. package/src/runner.zig +280 -0
  202. package/src/runner_actions.zig +109 -0
  203. package/src/runner_config.zig +6 -0
  204. package/src/runner_diagnostics.zig +268 -0
  205. package/src/runner_events.zig +170 -0
  206. package/src/runner_native.zig +88 -0
  207. package/src/runner_waits.zig +300 -0
  208. package/src/scaffold.zig +472 -0
  209. package/src/scenario.zig +346 -0
  210. package/src/scenario_fields.zig +50 -0
  211. package/src/schema_registry.zig +53 -0
  212. package/src/selector.zig +84 -0
  213. package/src/semantic.zig +171 -0
  214. package/src/trace.zig +315 -0
  215. package/src/trace_json.zig +340 -0
  216. package/src/trace_summary.zig +218 -0
  217. package/src/trace_summary_diagnostic.zig +202 -0
  218. package/src/types.zig +120 -0
  219. package/src/uiautomator.zig +164 -0
  220. package/src/validation.zig +187 -0
  221. package/src/version.zig +22 -0
  222. package/viewer/app.js +373 -0
  223. package/viewer/index.html +126 -0
  224. package/viewer/parser.js +233 -0
  225. package/viewer/styles.css +585 -0
@@ -0,0 +1,29 @@
1
+ # ZMR Python Reference Client
2
+
3
+ Standard-library Python client for ZMR's newline-delimited JSON-RPC protocol.
4
+
5
+ ```python
6
+ from zmr_client import ZmrClient
7
+
8
+ with ZmrClient(
9
+ "zmr",
10
+ [
11
+ "serve",
12
+ "--transport", "stdio",
13
+ "--device", "emulator-5554",
14
+ "--app-id", "com.example.mobiletest",
15
+ "--trace-dir", "traces/agent-session",
16
+ ],
17
+ ) as zmr:
18
+ zmr.create_session()
19
+ zmr.open_link("exampleapp://e2e-auth?probe=1")
20
+ zmr.wait_until({"text": "E2E auth probe"}, timeout_ms=30000)
21
+ snapshot = zmr.snapshot()
22
+ events = zmr.trace_events(0, limit=100)
23
+ print(snapshot["nodes"])
24
+ print(len(events["events"]))
25
+ zmr.export_trace("traces/agent-session-redacted.zmrtrace", redact=True, omit_screenshots=True)
26
+ ```
27
+
28
+ The client intentionally avoids runtime dependencies so agents can vendor or
29
+ copy it into automation harnesses easily.
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+
5
+ import sys
6
+
7
+ sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
8
+
9
+ from zmr_client import ZmrClient # noqa: E402
10
+
11
+
12
+ ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
13
+
14
+
15
+ with ZmrClient(
16
+ os.path.join(ROOT, "zig-out", "bin", "zmr"),
17
+ [
18
+ "serve",
19
+ "--transport",
20
+ "stdio",
21
+ "--device",
22
+ "fake-android-1",
23
+ "--app-id",
24
+ "com.example.mobiletest",
25
+ "--adb",
26
+ os.path.join(ROOT, "tests", "fake-adb.sh"),
27
+ "--trace-dir",
28
+ "traces/demo-python-client",
29
+ ],
30
+ ) as zmr:
31
+ capabilities = zmr.capabilities()
32
+ zmr.create_session()
33
+ zmr.open_link("exampleapp://e2e-auth?probe=1")
34
+ zmr.assert_healthy(timeout_ms=100)
35
+ snapshot = zmr.snapshot()
36
+ events = zmr.trace_events(0, limit=20)
37
+ zmr.export_trace("traces/demo-python-client-redacted.zmrtrace", redact=True, omit_screenshots=True)
38
+ print(
39
+ json.dumps(
40
+ {
41
+ "protocolVersion": capabilities["protocolVersion"],
42
+ "activePackage": snapshot["activePackage"],
43
+ "nodes": len(snapshot["nodes"]),
44
+ "events": len(events["events"]),
45
+ },
46
+ separators=(",", ":"),
47
+ )
48
+ )
@@ -0,0 +1,13 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "zmr-client"
7
+ version = "0.1.0.dev1"
8
+ description = "Python JSON-RPC client for Zig Mobile Runner."
9
+ requires-python = ">=3.9"
10
+ license = { text = "MIT" }
11
+
12
+ [tool.setuptools]
13
+ py-modules = ["zmr_client"]
@@ -0,0 +1,202 @@
1
+ import json
2
+ import subprocess
3
+ import threading
4
+
5
+
6
+ class ZmrRpcError(RuntimeError):
7
+ def __init__(self, error):
8
+ super().__init__(error.get("message", "ZMR JSON-RPC error"))
9
+ self.code = error.get("code")
10
+ self.public_code = error.get("publicCode")
11
+ self.data = error.get("data")
12
+
13
+
14
+ class ZmrClient:
15
+ def __init__(self, command, args=None, cwd=None, env=None, stderr=None):
16
+ self._process = subprocess.Popen(
17
+ [command, *(args or [])],
18
+ cwd=cwd,
19
+ env=env,
20
+ stdin=subprocess.PIPE,
21
+ stdout=subprocess.PIPE,
22
+ stderr=stderr,
23
+ text=True,
24
+ encoding="utf-8",
25
+ bufsize=1,
26
+ )
27
+ self._next_id = 1
28
+ self._lock = threading.Lock()
29
+ self._closed = False
30
+
31
+ def request(self, method, params=None):
32
+ with self._lock:
33
+ if self._closed:
34
+ raise RuntimeError("zmr client is closed")
35
+ request_id = self._next_id
36
+ self._next_id += 1
37
+ line = json.dumps(
38
+ {
39
+ "jsonrpc": "2.0",
40
+ "id": request_id,
41
+ "method": method,
42
+ "params": params or {},
43
+ },
44
+ separators=(",", ":"),
45
+ )
46
+ self._process.stdin.write(line + "\n")
47
+ self._process.stdin.flush()
48
+
49
+ response_line = self._process.stdout.readline()
50
+ if response_line == "":
51
+ code = self._process.poll()
52
+ raise RuntimeError(f"zmr process exited with {code}")
53
+ response = json.loads(response_line)
54
+ if response.get("id") != request_id:
55
+ raise RuntimeError(f"unexpected JSON-RPC response id {response.get('id')!r}")
56
+ if "error" in response:
57
+ raise ZmrRpcError(response["error"])
58
+ return response.get("result")
59
+
60
+ def capabilities(self):
61
+ return self.request("runner.capabilities")
62
+
63
+ def create_session(self):
64
+ return self.request("session.create")
65
+
66
+ def close_session(self):
67
+ return self.request("session.close")
68
+
69
+ def devices(self):
70
+ return self.request("device.list")
71
+
72
+ def launch(self):
73
+ return self.request("app.launch")
74
+
75
+ def stop(self):
76
+ return self.request("app.stop")
77
+
78
+ def clear_state(self):
79
+ return self.request("app.clearState")
80
+
81
+ def open_link(self, url):
82
+ return self.request("app.openLink", {"url": url})
83
+
84
+ def snapshot(self):
85
+ return self.request("observe.snapshot")
86
+
87
+ def semantic_snapshot(self):
88
+ return self.request("observe.semanticSnapshot")
89
+
90
+ def tap(self, selector):
91
+ return self.request("ui.tap", {"selector": selector})
92
+
93
+ def type_text(self, text, selector=None):
94
+ params = {"text": text}
95
+ if selector is not None:
96
+ params["selector"] = selector
97
+ return self.request("ui.type", params)
98
+
99
+ def erase_text(self, selector=None, max_chars=None):
100
+ params = {}
101
+ if selector is not None:
102
+ params["selector"] = selector
103
+ if max_chars is not None:
104
+ params["maxChars"] = max_chars
105
+ return self.request("ui.eraseText", params)
106
+
107
+ def hide_keyboard(self):
108
+ return self.request("ui.hideKeyboard")
109
+
110
+ def swipe(self, x1, y1, x2, y2, duration_ms=None):
111
+ params = {"x1": x1, "y1": y1, "x2": x2, "y2": y2}
112
+ if duration_ms is not None:
113
+ params["durationMs"] = duration_ms
114
+ return self.request("ui.swipe", params)
115
+
116
+ def press_back(self):
117
+ return self.request("ui.pressBack")
118
+
119
+ def scroll_until_visible(self, selector, direction=None, timeout_ms=None):
120
+ params = {"selector": selector}
121
+ if direction is not None:
122
+ params["direction"] = direction
123
+ if timeout_ms is not None:
124
+ params["timeoutMs"] = timeout_ms
125
+ return self.request("ui.scrollUntilVisible", params)
126
+
127
+ def wait_until(self, selector, timeout_ms=None):
128
+ params = {"visible": selector}
129
+ if timeout_ms is not None:
130
+ params["timeoutMs"] = timeout_ms
131
+ return self.request("wait.until", params)
132
+
133
+ def wait_any(self, selectors, timeout_ms=None):
134
+ params = {"selectors": selectors}
135
+ if timeout_ms is not None:
136
+ params["timeoutMs"] = timeout_ms
137
+ return self.request("wait.any", params)
138
+
139
+ def wait_gone(self, selector, timeout_ms=None):
140
+ params = {"selector": selector}
141
+ if timeout_ms is not None:
142
+ params["timeoutMs"] = timeout_ms
143
+ return self.request("wait.gone", params)
144
+
145
+ def assert_visible(self, selector, timeout_ms=None):
146
+ params = {"selector": selector}
147
+ if timeout_ms is not None:
148
+ params["timeoutMs"] = timeout_ms
149
+ return self.request("assert.visible", params)
150
+
151
+ def assert_not_visible(self, selector, timeout_ms=None):
152
+ params = {"selector": selector}
153
+ if timeout_ms is not None:
154
+ params["timeoutMs"] = timeout_ms
155
+ return self.request("assert.notVisible", params)
156
+
157
+ def assert_healthy(self, timeout_ms=None):
158
+ params = {}
159
+ if timeout_ms is not None:
160
+ params["timeoutMs"] = timeout_ms
161
+ return self.request("assert.healthy", params)
162
+
163
+ def export_trace(self, out, redact=False, omit_screenshots=False):
164
+ return self.request(
165
+ "trace.export",
166
+ {
167
+ "out": out,
168
+ "redact": redact,
169
+ "omitScreenshots": omit_screenshots,
170
+ },
171
+ )
172
+
173
+ def trace_events(self, after_seq=0, limit=None):
174
+ params = {"afterSeq": after_seq}
175
+ if limit is not None:
176
+ params["limit"] = limit
177
+ return self.request("trace.events", params)
178
+
179
+ def close(self):
180
+ if self._closed:
181
+ return
182
+ self._closed = True
183
+ if self._process.stdin:
184
+ self._process.stdin.close()
185
+ if self._process.poll() is None:
186
+ self._process.terminate()
187
+ try:
188
+ self._process.wait(timeout=2)
189
+ except subprocess.TimeoutExpired:
190
+ self._process.kill()
191
+ self._process.wait(timeout=2)
192
+ if self._process.stdout:
193
+ self._process.stdout.close()
194
+ if self._process.stderr:
195
+ self._process.stderr.close()
196
+
197
+ def __enter__(self):
198
+ return self
199
+
200
+ def __exit__(self, exc_type, exc, traceback):
201
+ self.close()
202
+ return False
@@ -0,0 +1,107 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "itoa"
7
+ version = "1.0.18"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
10
+
11
+ [[package]]
12
+ name = "memchr"
13
+ version = "2.8.0"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
16
+
17
+ [[package]]
18
+ name = "proc-macro2"
19
+ version = "1.0.106"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
22
+ dependencies = [
23
+ "unicode-ident",
24
+ ]
25
+
26
+ [[package]]
27
+ name = "quote"
28
+ version = "1.0.45"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
31
+ dependencies = [
32
+ "proc-macro2",
33
+ ]
34
+
35
+ [[package]]
36
+ name = "serde"
37
+ version = "1.0.228"
38
+ source = "registry+https://github.com/rust-lang/crates.io-index"
39
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
40
+ dependencies = [
41
+ "serde_core",
42
+ "serde_derive",
43
+ ]
44
+
45
+ [[package]]
46
+ name = "serde_core"
47
+ version = "1.0.228"
48
+ source = "registry+https://github.com/rust-lang/crates.io-index"
49
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
50
+ dependencies = [
51
+ "serde_derive",
52
+ ]
53
+
54
+ [[package]]
55
+ name = "serde_derive"
56
+ version = "1.0.228"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
59
+ dependencies = [
60
+ "proc-macro2",
61
+ "quote",
62
+ "syn",
63
+ ]
64
+
65
+ [[package]]
66
+ name = "serde_json"
67
+ version = "1.0.149"
68
+ source = "registry+https://github.com/rust-lang/crates.io-index"
69
+ checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
70
+ dependencies = [
71
+ "itoa",
72
+ "memchr",
73
+ "serde",
74
+ "serde_core",
75
+ "zmij",
76
+ ]
77
+
78
+ [[package]]
79
+ name = "syn"
80
+ version = "2.0.117"
81
+ source = "registry+https://github.com/rust-lang/crates.io-index"
82
+ checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
83
+ dependencies = [
84
+ "proc-macro2",
85
+ "quote",
86
+ "unicode-ident",
87
+ ]
88
+
89
+ [[package]]
90
+ name = "unicode-ident"
91
+ version = "1.0.24"
92
+ source = "registry+https://github.com/rust-lang/crates.io-index"
93
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
94
+
95
+ [[package]]
96
+ name = "zmij"
97
+ version = "1.0.21"
98
+ source = "registry+https://github.com/rust-lang/crates.io-index"
99
+ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
100
+
101
+ [[package]]
102
+ name = "zmr-client"
103
+ version = "0.1.0"
104
+ dependencies = [
105
+ "serde",
106
+ "serde_json",
107
+ ]
@@ -0,0 +1,10 @@
1
+ [package]
2
+ name = "zmr-client"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ license = "MIT"
6
+ description = "Rust JSON-RPC client for Zig Mobile Runner."
7
+
8
+ [dependencies]
9
+ serde = { version = "1", features = ["derive"] }
10
+ serde_json = "1"
@@ -0,0 +1,19 @@
1
+ # ZMR Rust Client
2
+
3
+ Small synchronous JSON-RPC client for driving `zmr serve --transport stdio`
4
+ from Rust agents and test harnesses.
5
+
6
+ ```rust
7
+ let mut client = zmr_client::Client::start("zmr", ["serve", "--transport", "stdio"])?;
8
+ let snapshot = client.snapshot()?;
9
+ let healthy = client.assert_healthy(Some(1000))?;
10
+ ```
11
+
12
+ Run the fake-session example from the repository root:
13
+
14
+ ```sh
15
+ cargo run --manifest-path clients/rust/Cargo.toml --example fake_session -- \
16
+ --zmr ./zig-out/bin/zmr \
17
+ --adb ./tests/fake-adb.sh \
18
+ --trace-dir traces/demo-rust-client
19
+ ```
@@ -0,0 +1,70 @@
1
+ use serde_json::json;
2
+ use std::env;
3
+ use zmr_client::Client;
4
+
5
+ fn main() -> Result<(), Box<dyn std::error::Error>> {
6
+ let mut zmr = String::from("zig-out/bin/zmr");
7
+ let mut adb = String::from("tests/fake-adb.sh");
8
+ let mut device = String::from("fake-android-1");
9
+ let mut app_id = String::from("com.example.mobiletest");
10
+ let mut trace_dir = String::from("traces/demo-rust-client");
11
+ let mut trace_out = String::from("traces/demo-rust-client-redacted.zmrtrace");
12
+
13
+ let mut args = env::args().skip(1);
14
+ while let Some(arg) = args.next() {
15
+ match arg.as_str() {
16
+ "--zmr" => zmr = args.next().ok_or("--zmr requires a value")?,
17
+ "--adb" => adb = args.next().ok_or("--adb requires a value")?,
18
+ "--device" => device = args.next().ok_or("--device requires a value")?,
19
+ "--app-id" => app_id = args.next().ok_or("--app-id requires a value")?,
20
+ "--trace-dir" => trace_dir = args.next().ok_or("--trace-dir requires a value")?,
21
+ "--trace-out" => trace_out = args.next().ok_or("--trace-out requires a value")?,
22
+ _ => return Err(format!("unknown argument: {arg}").into()),
23
+ }
24
+ }
25
+
26
+ let mut client = Client::start(
27
+ &zmr,
28
+ [
29
+ "serve",
30
+ "--transport",
31
+ "stdio",
32
+ "--device",
33
+ &device,
34
+ "--app-id",
35
+ &app_id,
36
+ "--adb",
37
+ &adb,
38
+ "--trace-dir",
39
+ &trace_dir,
40
+ ],
41
+ )?;
42
+ let capabilities = client.capabilities()?;
43
+ client.create_session()?;
44
+ client.open_link("exampleapp://rust-client")?;
45
+ client.wait_until(json!({ "text": "Dashboard" }), Some(1000))?;
46
+ client.tap(json!({ "text": "Sign in" }))?;
47
+ client.type_text(
48
+ "agent@example.com",
49
+ Some(json!({ "resourceId": "email-login-email-input" })),
50
+ )?;
51
+ client.assert_not_visible(json!({ "text": "Application has crashed" }), Some(100))?;
52
+ client.assert_healthy(Some(100))?;
53
+ let snapshot = client.snapshot()?;
54
+ let exported = client.export_trace(&trace_out, true, true)?;
55
+ let events = client.trace_events(0, Some(10))?;
56
+
57
+ println!(
58
+ "{}",
59
+ json!({
60
+ "protocolVersion": capabilities.protocol_version,
61
+ "activePackage": snapshot.active_package,
62
+ "nodes": snapshot.nodes.len(),
63
+ "events": events.next_seq,
64
+ "traceDir": trace_dir,
65
+ "traceOut": exported.out,
66
+ })
67
+ );
68
+
69
+ Ok(())
70
+ }