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,41 @@
1
+ const std = @import("std");
2
+
3
+ const cli_output = @import("cli_output.zig");
4
+ const validation = @import("validation.zig");
5
+
6
+ pub const ParsedArgs = struct {
7
+ path: []const u8,
8
+ json: bool = false,
9
+ };
10
+
11
+ pub fn parseArgs(args: []const []const u8) !ParsedArgs {
12
+ if (args.len == 0) return error.MissingScenarioPath;
13
+
14
+ var parsed = ParsedArgs{ .path = args[0] };
15
+ for (args[1..]) |arg| {
16
+ if (std.mem.eql(u8, arg, "--json")) {
17
+ parsed.json = true;
18
+ } else {
19
+ return error.UnknownFlag;
20
+ }
21
+ }
22
+ return parsed;
23
+ }
24
+
25
+ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
26
+ var raw_args = std.ArrayList([]const u8).empty;
27
+ defer raw_args.deinit(allocator);
28
+ while (args.next()) |arg| try raw_args.append(allocator, arg);
29
+
30
+ const parsed = try parseArgs(raw_args.items);
31
+ const result = try validation.validateFile(allocator, parsed.path);
32
+ defer result.deinit(allocator);
33
+
34
+ const stdout = std.fs.File.stdout().deprecatedWriter();
35
+ if (parsed.json) {
36
+ try cli_output.writeValidationJson(stdout, parsed.path, result);
37
+ } else {
38
+ try cli_output.writeValidationText(stdout, parsed.path, result);
39
+ }
40
+ if (!result.ok) std.process.exit(1);
41
+ }
@@ -0,0 +1,211 @@
1
+ const std = @import("std");
2
+ const builtin = @import("builtin");
3
+
4
+ pub const ExecResult = struct {
5
+ stdout: []u8,
6
+ stderr: []u8,
7
+ term: std.process.Child.Term,
8
+ timed_out: bool = false,
9
+
10
+ pub fn deinit(self: ExecResult, allocator: std.mem.Allocator) void {
11
+ allocator.free(self.stdout);
12
+ allocator.free(self.stderr);
13
+ }
14
+
15
+ pub fn ensureSuccess(self: ExecResult) !void {
16
+ if (self.timed_out) return error.CommandTimedOut;
17
+ switch (self.term) {
18
+ .Exited => |code| if (code == 0) return,
19
+ else => {},
20
+ }
21
+ return error.CommandFailed;
22
+ }
23
+ };
24
+
25
+ pub fn run(
26
+ allocator: std.mem.Allocator,
27
+ argv: []const []const u8,
28
+ max_output_bytes: usize,
29
+ ) !ExecResult {
30
+ const result = try std.process.Child.run(.{
31
+ .allocator = allocator,
32
+ .argv = argv,
33
+ .max_output_bytes = max_output_bytes,
34
+ });
35
+ return .{
36
+ .stdout = result.stdout,
37
+ .stderr = result.stderr,
38
+ .term = result.term,
39
+ .timed_out = false,
40
+ };
41
+ }
42
+
43
+ pub fn runWithInput(
44
+ allocator: std.mem.Allocator,
45
+ argv: []const []const u8,
46
+ stdin: []const u8,
47
+ max_output_bytes: usize,
48
+ ) !ExecResult {
49
+ return try runWithInputTimeout(allocator, argv, stdin, max_output_bytes, 0);
50
+ }
51
+
52
+ pub fn runWithInputTimeout(
53
+ allocator: std.mem.Allocator,
54
+ argv: []const []const u8,
55
+ stdin: []const u8,
56
+ max_output_bytes: usize,
57
+ timeout_ms: u64,
58
+ ) !ExecResult {
59
+ var child = std.process.Child.init(argv, allocator);
60
+ child.stdin_behavior = .Pipe;
61
+ child.stdout_behavior = .Pipe;
62
+ child.stderr_behavior = .Pipe;
63
+
64
+ var stdout = std.ArrayList(u8).empty;
65
+ defer stdout.deinit(allocator);
66
+ var stderr = std.ArrayList(u8).empty;
67
+ defer stderr.deinit(allocator);
68
+
69
+ try child.spawn();
70
+ errdefer _ = child.kill() catch {};
71
+
72
+ var done = std.Thread.ResetEvent{};
73
+ var timed_out = std.atomic.Value(bool).init(false);
74
+ const use_timeout = timeout_ms > 0;
75
+ const killer = if (use_timeout)
76
+ try std.Thread.spawn(.{}, timeoutKiller, .{
77
+ child.id,
78
+ &done,
79
+ &timed_out,
80
+ std.math.mul(u64, timeout_ms, std.time.ns_per_ms) catch std.math.maxInt(u64),
81
+ })
82
+ else
83
+ null;
84
+
85
+ if (child.stdin) |stdin_file| {
86
+ try stdin_file.writeAll(stdin);
87
+ stdin_file.close();
88
+ child.stdin = null;
89
+ }
90
+
91
+ child.collectOutput(allocator, &stdout, &stderr, max_output_bytes) catch |err| {
92
+ _ = child.kill() catch {};
93
+ done.set();
94
+ if (killer) |thread| thread.join();
95
+ return err;
96
+ };
97
+
98
+ const term = child.wait() catch |err| {
99
+ done.set();
100
+ if (killer) |thread| thread.join();
101
+ return err;
102
+ };
103
+ done.set();
104
+ if (killer) |thread| thread.join();
105
+
106
+ return .{
107
+ .stdout = try stdout.toOwnedSlice(allocator),
108
+ .stderr = try stderr.toOwnedSlice(allocator),
109
+ .term = term,
110
+ .timed_out = timed_out.load(.acquire),
111
+ };
112
+ }
113
+
114
+ pub fn runWithTimeout(
115
+ allocator: std.mem.Allocator,
116
+ argv: []const []const u8,
117
+ max_output_bytes: usize,
118
+ timeout_ms: u64,
119
+ ) !ExecResult {
120
+ if (timeout_ms == 0) return run(allocator, argv, max_output_bytes);
121
+
122
+ var child = std.process.Child.init(argv, allocator);
123
+ child.stdin_behavior = .Ignore;
124
+ child.stdout_behavior = .Pipe;
125
+ child.stderr_behavior = .Pipe;
126
+
127
+ var stdout = std.ArrayList(u8).empty;
128
+ defer stdout.deinit(allocator);
129
+ var stderr = std.ArrayList(u8).empty;
130
+ defer stderr.deinit(allocator);
131
+
132
+ try child.spawn();
133
+ errdefer _ = child.kill() catch {};
134
+
135
+ var done = std.Thread.ResetEvent{};
136
+ var timed_out = std.atomic.Value(bool).init(false);
137
+ const timeout_ns = std.math.mul(u64, timeout_ms, std.time.ns_per_ms) catch std.math.maxInt(u64);
138
+ const killer = try std.Thread.spawn(.{}, timeoutKiller, .{ child.id, &done, &timed_out, timeout_ns });
139
+
140
+ child.collectOutput(allocator, &stdout, &stderr, max_output_bytes) catch |err| {
141
+ _ = child.kill() catch {};
142
+ done.set();
143
+ killer.join();
144
+ return err;
145
+ };
146
+
147
+ const term = child.wait() catch |err| {
148
+ done.set();
149
+ killer.join();
150
+ return err;
151
+ };
152
+ done.set();
153
+ killer.join();
154
+
155
+ return .{
156
+ .stdout = try stdout.toOwnedSlice(allocator),
157
+ .stderr = try stderr.toOwnedSlice(allocator),
158
+ .term = term,
159
+ .timed_out = timed_out.load(.acquire),
160
+ };
161
+ }
162
+
163
+ fn timeoutKiller(
164
+ child_id: std.process.Child.Id,
165
+ done: *std.Thread.ResetEvent,
166
+ timed_out: *std.atomic.Value(bool),
167
+ timeout_ns: u64,
168
+ ) void {
169
+ done.timedWait(timeout_ns) catch {
170
+ timed_out.store(true, .release);
171
+ killChildId(child_id);
172
+ };
173
+ }
174
+
175
+ fn killChildId(child_id: std.process.Child.Id) void {
176
+ switch (builtin.os.tag) {
177
+ .windows => {},
178
+ else => std.posix.kill(child_id, std.posix.SIG.TERM) catch {},
179
+ }
180
+ }
181
+
182
+ pub fn escapeAdbInputText(allocator: std.mem.Allocator, text: []const u8) ![]const u8 {
183
+ var out = std.ArrayList(u8).empty;
184
+ errdefer out.deinit(allocator);
185
+ for (text) |ch| {
186
+ switch (ch) {
187
+ ' ' => try out.appendSlice(allocator, "%s"),
188
+ '&', '<', '>', ';', '|', '*', '~', '"', '\'', '\\', '(', ')' => {
189
+ try out.append(allocator, '\\');
190
+ try out.append(allocator, ch);
191
+ },
192
+ else => try out.append(allocator, ch),
193
+ }
194
+ }
195
+ return try out.toOwnedSlice(allocator);
196
+ }
197
+
198
+ pub fn escapeAdbShellArg(allocator: std.mem.Allocator, value: []const u8) ![]const u8 {
199
+ var out = std.ArrayList(u8).empty;
200
+ errdefer out.deinit(allocator);
201
+ try out.append(allocator, '\'');
202
+ for (value) |ch| {
203
+ if (ch == '\'') {
204
+ try out.appendSlice(allocator, "'\\''");
205
+ } else {
206
+ try out.append(allocator, ch);
207
+ }
208
+ }
209
+ try out.append(allocator, '\'');
210
+ return try out.toOwnedSlice(allocator);
211
+ }
package/src/config.zig ADDED
@@ -0,0 +1,305 @@
1
+ const std = @import("std");
2
+ const config_diagnostics = @import("config_diagnostics.zig");
3
+
4
+ pub const PlatformConfig = struct {
5
+ enabled: bool = false,
6
+ default_device: ?[]const u8 = null,
7
+ smoke_scenario: ?[]const u8 = null,
8
+ trace_dir: ?[]const u8 = null,
9
+ avd_name: ?[]const u8 = null,
10
+ restore_snapshot: ?[]const u8 = null,
11
+ avd_system_image: ?[]const u8 = null,
12
+ avd_device_profile: ?[]const u8 = null,
13
+ reset_before_run: bool = false,
14
+ wait_ready: bool = false,
15
+ create_avd_if_missing: bool = false,
16
+
17
+ pub fn deinit(self: *PlatformConfig, allocator: std.mem.Allocator) void {
18
+ if (self.default_device) |value| allocator.free(value);
19
+ if (self.smoke_scenario) |value| allocator.free(value);
20
+ if (self.trace_dir) |value| allocator.free(value);
21
+ if (self.avd_name) |value| allocator.free(value);
22
+ if (self.restore_snapshot) |value| allocator.free(value);
23
+ if (self.avd_system_image) |value| allocator.free(value);
24
+ if (self.avd_device_profile) |value| allocator.free(value);
25
+ }
26
+ };
27
+
28
+ pub const ToolsConfig = struct {
29
+ adb_path: ?[]const u8 = null,
30
+ emulator_path: ?[]const u8 = null,
31
+ avdmanager_path: ?[]const u8 = null,
32
+ android_shim_path: ?[]const u8 = null,
33
+ xcrun_path: ?[]const u8 = null,
34
+ ios_shim_path: ?[]const u8 = null,
35
+ zig_path: ?[]const u8 = null,
36
+
37
+ pub fn deinit(self: *ToolsConfig, allocator: std.mem.Allocator) void {
38
+ if (self.adb_path) |value| allocator.free(value);
39
+ if (self.emulator_path) |value| allocator.free(value);
40
+ if (self.avdmanager_path) |value| allocator.free(value);
41
+ if (self.android_shim_path) |value| allocator.free(value);
42
+ if (self.xcrun_path) |value| allocator.free(value);
43
+ if (self.ios_shim_path) |value| allocator.free(value);
44
+ if (self.zig_path) |value| allocator.free(value);
45
+ }
46
+ };
47
+
48
+ pub const ArtifactConfig = struct {
49
+ screenshots: bool = true,
50
+ hierarchy: bool = true,
51
+ logs: bool = true,
52
+ screen_recording: bool = false,
53
+ };
54
+
55
+ pub const RedactionConfig = struct {
56
+ denylist_text: []const []const u8 = &.{},
57
+ allowlist_text: []const []const u8 = &.{},
58
+ denylist_resource_ids: []const []const u8 = &.{},
59
+ allowlist_resource_ids: []const []const u8 = &.{},
60
+
61
+ pub fn deinit(self: *RedactionConfig, allocator: std.mem.Allocator) void {
62
+ freeStringList(allocator, self.denylist_text);
63
+ freeStringList(allocator, self.allowlist_text);
64
+ freeStringList(allocator, self.denylist_resource_ids);
65
+ freeStringList(allocator, self.allowlist_resource_ids);
66
+ }
67
+ };
68
+
69
+ pub const ScriptCommand = struct {
70
+ name: []const u8,
71
+ command: []const u8,
72
+ };
73
+
74
+ pub const Config = struct {
75
+ schema_version: u32,
76
+ app_id: ?[]const u8 = null,
77
+ android: PlatformConfig = .{},
78
+ ios: PlatformConfig = .{},
79
+ tools: ToolsConfig = .{},
80
+ artifacts: ArtifactConfig = .{},
81
+ redaction: RedactionConfig = .{},
82
+ scripts: []ScriptCommand = &.{},
83
+
84
+ pub fn deinit(self: *Config, allocator: std.mem.Allocator) void {
85
+ if (self.app_id) |value| allocator.free(value);
86
+ self.android.deinit(allocator);
87
+ self.ios.deinit(allocator);
88
+ self.tools.deinit(allocator);
89
+ self.redaction.deinit(allocator);
90
+ freeScripts(allocator, self.scripts);
91
+ }
92
+ };
93
+
94
+ pub fn parseFile(allocator: std.mem.Allocator, path: []const u8) !Config {
95
+ const content = try std.fs.cwd().readFileAlloc(allocator, path, 1024 * 1024);
96
+ defer allocator.free(content);
97
+ return try parseSlice(allocator, content);
98
+ }
99
+
100
+ pub fn errorFieldPathForFile(allocator: std.mem.Allocator, path: []const u8, err: anyerror) !?[]const u8 {
101
+ return try config_diagnostics.errorFieldPathForFile(allocator, path, err);
102
+ }
103
+
104
+ pub fn parseSlice(allocator: std.mem.Allocator, content: []const u8) !Config {
105
+ const parsed = try std.json.parseFromSlice(std.json.Value, allocator, content, .{});
106
+ defer parsed.deinit();
107
+ if (parsed.value != .object) return error.ConfigMustBeObject;
108
+ const object = parsed.value.object;
109
+ try rejectUnknownFields(object, &.{ "schemaVersion", "appId", "android", "ios", "artifacts", "redaction", "tools", "scripts" });
110
+ const schema_version = try requiredU32(object, "schemaVersion");
111
+
112
+ var cfg = Config{
113
+ .schema_version = schema_version,
114
+ .app_id = try optionalString(allocator, object, "appId"),
115
+ .android = try platformConfig(allocator, object.get("android")),
116
+ .ios = try platformConfig(allocator, object.get("ios")),
117
+ .tools = try toolsConfig(allocator, object.get("tools")),
118
+ .artifacts = try artifactConfig(object.get("artifacts")),
119
+ .redaction = try redactionConfig(allocator, object.get("redaction")),
120
+ .scripts = try scriptsConfig(allocator, object.get("scripts")),
121
+ };
122
+ errdefer cfg.deinit(allocator);
123
+
124
+ if (cfg.schema_version != 1) return error.UnsupportedConfigVersion;
125
+ return cfg;
126
+ }
127
+
128
+ fn platformConfig(allocator: std.mem.Allocator, maybe_value: ?std.json.Value) !PlatformConfig {
129
+ const value = maybe_value orelse return .{};
130
+ if (value != .object) return error.ConfigPlatformMustBeObject;
131
+ const object = value.object;
132
+ try rejectUnknownFields(object, &.{
133
+ "enabled",
134
+ "defaultDevice",
135
+ "smokeScenario",
136
+ "traceDir",
137
+ "avdName",
138
+ "restoreSnapshot",
139
+ "createAvdIfMissing",
140
+ "avdSystemImage",
141
+ "avdDeviceProfile",
142
+ "resetBeforeRun",
143
+ "waitReady",
144
+ });
145
+ return .{
146
+ .enabled = try optionalBool(object, "enabled") orelse false,
147
+ .default_device = try optionalString(allocator, object, "defaultDevice"),
148
+ .smoke_scenario = try optionalString(allocator, object, "smokeScenario"),
149
+ .trace_dir = try optionalString(allocator, object, "traceDir"),
150
+ .avd_name = try optionalString(allocator, object, "avdName"),
151
+ .restore_snapshot = try optionalString(allocator, object, "restoreSnapshot"),
152
+ .avd_system_image = try optionalString(allocator, object, "avdSystemImage"),
153
+ .avd_device_profile = try optionalString(allocator, object, "avdDeviceProfile"),
154
+ .reset_before_run = try optionalBool(object, "resetBeforeRun") orelse false,
155
+ .wait_ready = try optionalBool(object, "waitReady") orelse false,
156
+ .create_avd_if_missing = try optionalBool(object, "createAvdIfMissing") orelse false,
157
+ };
158
+ }
159
+
160
+ pub fn errorFieldPathForSlice(allocator: std.mem.Allocator, content: []const u8, err: anyerror) !?[]const u8 {
161
+ return try config_diagnostics.errorFieldPathForSlice(allocator, content, err);
162
+ }
163
+
164
+ fn toolsConfig(allocator: std.mem.Allocator, maybe_value: ?std.json.Value) !ToolsConfig {
165
+ const value = maybe_value orelse return .{};
166
+ if (value != .object) return error.ConfigToolsMustBeObject;
167
+ const object = value.object;
168
+ try rejectUnknownFields(object, &.{ "adbPath", "emulatorPath", "avdmanagerPath", "androidShimPath", "xcrunPath", "iosShimPath", "zigPath" });
169
+ return .{
170
+ .adb_path = try optionalString(allocator, object, "adbPath"),
171
+ .emulator_path = try optionalString(allocator, object, "emulatorPath"),
172
+ .avdmanager_path = try optionalString(allocator, object, "avdmanagerPath"),
173
+ .android_shim_path = try optionalString(allocator, object, "androidShimPath"),
174
+ .xcrun_path = try optionalString(allocator, object, "xcrunPath"),
175
+ .ios_shim_path = try optionalString(allocator, object, "iosShimPath"),
176
+ .zig_path = try optionalString(allocator, object, "zigPath"),
177
+ };
178
+ }
179
+
180
+ fn artifactConfig(maybe_value: ?std.json.Value) !ArtifactConfig {
181
+ const value = maybe_value orelse return .{};
182
+ if (value != .object) return error.ConfigArtifactsMustBeObject;
183
+ const object = value.object;
184
+ try rejectUnknownFields(object, &.{ "screenshots", "hierarchy", "logs", "screenRecording" });
185
+ return .{
186
+ .screenshots = try optionalBool(object, "screenshots") orelse true,
187
+ .hierarchy = try optionalBool(object, "hierarchy") orelse true,
188
+ .logs = try optionalBool(object, "logs") orelse true,
189
+ .screen_recording = try optionalBool(object, "screenRecording") orelse false,
190
+ };
191
+ }
192
+
193
+ fn redactionConfig(allocator: std.mem.Allocator, maybe_value: ?std.json.Value) !RedactionConfig {
194
+ const value = maybe_value orelse return .{};
195
+ if (value != .object) return error.ConfigRedactionMustBeObject;
196
+ const object = value.object;
197
+ try rejectUnknownFields(object, &.{ "denylistText", "allowlistText", "denylistResourceIds", "allowlistResourceIds" });
198
+ var parsed = RedactionConfig{};
199
+ errdefer parsed.deinit(allocator);
200
+ parsed.denylist_text = try optionalStringList(allocator, object, "denylistText");
201
+ parsed.allowlist_text = try optionalStringList(allocator, object, "allowlistText");
202
+ parsed.denylist_resource_ids = try optionalStringList(allocator, object, "denylistResourceIds");
203
+ parsed.allowlist_resource_ids = try optionalStringList(allocator, object, "allowlistResourceIds");
204
+ return parsed;
205
+ }
206
+
207
+ fn rejectUnknownFields(object: std.json.ObjectMap, allowed: []const []const u8) !void {
208
+ var iterator = object.iterator();
209
+ while (iterator.next()) |entry| {
210
+ var found = false;
211
+ for (allowed) |key| {
212
+ if (std.mem.eql(u8, entry.key_ptr.*, key)) {
213
+ found = true;
214
+ break;
215
+ }
216
+ }
217
+ if (!found) return error.ConfigUnknownField;
218
+ }
219
+ }
220
+
221
+ fn scriptsConfig(allocator: std.mem.Allocator, maybe_value: ?std.json.Value) ![]ScriptCommand {
222
+ const value = maybe_value orelse return &.{};
223
+ if (value != .object) return error.ConfigScriptsMustBeObject;
224
+ if (value.object.count() == 0) return &.{};
225
+
226
+ var scripts = try allocator.alloc(ScriptCommand, value.object.count());
227
+ errdefer allocator.free(scripts);
228
+
229
+ var written: usize = 0;
230
+ errdefer {
231
+ for (scripts[0..written]) |script| {
232
+ allocator.free(script.name);
233
+ allocator.free(script.command);
234
+ }
235
+ }
236
+
237
+ var iterator = value.object.iterator();
238
+ while (iterator.next()) |entry| {
239
+ if (entry.value_ptr.* != .string) return error.ConfigFieldMustBeString;
240
+ if (entry.value_ptr.string.len == 0) return error.ConfigFieldMustBeNonEmptyString;
241
+ scripts[written] = .{
242
+ .name = try allocator.dupe(u8, entry.key_ptr.*),
243
+ .command = try allocator.dupe(u8, entry.value_ptr.string),
244
+ };
245
+ written += 1;
246
+ }
247
+ return scripts;
248
+ }
249
+
250
+ fn requiredU32(object: std.json.ObjectMap, key: []const u8) !u32 {
251
+ const value = object.get(key) orelse return error.MissingConfigSchemaVersion;
252
+ if (value != .integer) return error.ConfigSchemaVersionMustBeInteger;
253
+ if (value.integer < 0 or value.integer > std.math.maxInt(u32)) return error.ConfigSchemaVersionMustBeInteger;
254
+ return @intCast(value.integer);
255
+ }
256
+
257
+ fn optionalString(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) !?[]const u8 {
258
+ const value = object.get(key) orelse return null;
259
+ if (value == .null) return null;
260
+ if (value != .string) return error.ConfigFieldMustBeString;
261
+ if (value.string.len == 0) return error.ConfigFieldMustBeNonEmptyString;
262
+ return try allocator.dupe(u8, value.string);
263
+ }
264
+
265
+ fn optionalBool(object: std.json.ObjectMap, key: []const u8) !?bool {
266
+ const value = object.get(key) orelse return null;
267
+ if (value != .bool) return error.ConfigFieldMustBeBool;
268
+ return value.bool;
269
+ }
270
+
271
+ fn optionalStringList(allocator: std.mem.Allocator, object: std.json.ObjectMap, key: []const u8) ![]const []const u8 {
272
+ const value = object.get(key) orelse return &.{};
273
+ if (value != .array) return error.ConfigFieldMustBeStringArray;
274
+ if (value.array.items.len == 0) return &.{};
275
+
276
+ var output = try allocator.alloc([]const u8, value.array.items.len);
277
+ errdefer allocator.free(output);
278
+ var written: usize = 0;
279
+ errdefer {
280
+ for (output[0..written]) |item| allocator.free(item);
281
+ }
282
+
283
+ for (value.array.items, 0..) |item, index| {
284
+ if (item != .string) return error.ConfigFieldMustBeStringArray;
285
+ if (item.string.len == 0) return error.ConfigFieldMustBeNonEmptyString;
286
+ output[index] = try allocator.dupe(u8, item.string);
287
+ written += 1;
288
+ }
289
+ return output;
290
+ }
291
+
292
+ fn freeStringList(allocator: std.mem.Allocator, list: []const []const u8) void {
293
+ if (list.len == 0) return;
294
+ for (list) |item| allocator.free(item);
295
+ allocator.free(list);
296
+ }
297
+
298
+ fn freeScripts(allocator: std.mem.Allocator, scripts: []ScriptCommand) void {
299
+ if (scripts.len == 0) return;
300
+ for (scripts) |script| {
301
+ allocator.free(script.name);
302
+ allocator.free(script.command);
303
+ }
304
+ allocator.free(scripts);
305
+ }