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,154 @@
1
+ const std = @import("std");
2
+ const command = @import("command.zig");
3
+
4
+ const default_timeout_ms = 15_000;
5
+
6
+ pub const PreflightOptions = struct {
7
+ adb_path: []const u8 = "adb",
8
+ emulator_path: []const u8 = "emulator",
9
+ avdmanager_path: []const u8 = "avdmanager",
10
+ device_serial: ?[]const u8 = null,
11
+ avd_name: ?[]const u8 = null,
12
+ restore_snapshot: ?[]const u8 = null,
13
+ create_avd_if_missing: bool = false,
14
+ avd_system_image: ?[]const u8 = null,
15
+ avd_device_profile: ?[]const u8 = null,
16
+ reset_before_run: bool = false,
17
+ wait_ready: bool = false,
18
+ event_log_path: ?[]const u8 = null,
19
+ };
20
+
21
+ pub fn hasWork(options: PreflightOptions) bool {
22
+ 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;
23
+ }
24
+
25
+ pub fn runPreflight(allocator: std.mem.Allocator, options: PreflightOptions) !void {
26
+ if (!hasWork(options)) return;
27
+
28
+ if ((options.reset_before_run or options.restore_snapshot != null or options.create_avd_if_missing or options.avd_name != null) and options.avd_name == null) {
29
+ return error.MissingAndroidAvdName;
30
+ }
31
+ if (options.create_avd_if_missing and options.avd_system_image == null) {
32
+ return error.MissingAndroidAvdSystemImage;
33
+ }
34
+
35
+ if (options.create_avd_if_missing) {
36
+ try createAvdIfMissing(allocator, options, options.avd_name.?);
37
+ }
38
+
39
+ if (options.reset_before_run) {
40
+ const reset = runAdb(allocator, options, &.{ "emu", "kill" }) catch null;
41
+ if (reset) |result| result.deinit(allocator);
42
+ }
43
+
44
+ if (options.avd_name) |avd| {
45
+ try startEmulator(allocator, options, avd);
46
+ }
47
+
48
+ if (options.wait_ready) {
49
+ try waitReady(allocator, options);
50
+ }
51
+ }
52
+
53
+ fn createAvdIfMissing(allocator: std.mem.Allocator, options: PreflightOptions, avd: []const u8) !void {
54
+ var list = try runEmulator(allocator, options, &.{"-list-avds"});
55
+ defer list.deinit(allocator);
56
+ try list.ensureSuccess();
57
+ if (avdListContains(list.stdout, avd)) return;
58
+
59
+ var argv = std.ArrayList([]const u8).empty;
60
+ defer argv.deinit(allocator);
61
+ try argv.appendSlice(allocator, &.{ options.avdmanager_path, "create", "avd", "--name", avd, "--package", options.avd_system_image.? });
62
+ if (options.avd_device_profile) |profile| {
63
+ try argv.appendSlice(allocator, &.{ "--device", profile });
64
+ }
65
+ try argv.append(allocator, "--force");
66
+ try recordCommand(allocator, options.event_log_path, argv.items);
67
+ var result = try command.runWithInputTimeout(allocator, argv.items, "no\n", 1024 * 1024, default_timeout_ms);
68
+ defer result.deinit(allocator);
69
+ try result.ensureSuccess();
70
+ }
71
+
72
+ fn avdListContains(output: []const u8, avd: []const u8) bool {
73
+ var lines = std.mem.splitScalar(u8, output, '\n');
74
+ while (lines.next()) |raw_line| {
75
+ const line = std.mem.trim(u8, raw_line, " \t\r\n");
76
+ if (std.mem.eql(u8, line, avd)) return true;
77
+ }
78
+ return false;
79
+ }
80
+
81
+ fn runEmulator(allocator: std.mem.Allocator, options: PreflightOptions, extra: []const []const u8) !command.ExecResult {
82
+ var argv = std.ArrayList([]const u8).empty;
83
+ defer argv.deinit(allocator);
84
+ try argv.append(allocator, options.emulator_path);
85
+ try argv.appendSlice(allocator, extra);
86
+ try recordCommand(allocator, options.event_log_path, argv.items);
87
+ return try command.runWithTimeout(allocator, argv.items, 1024 * 1024, default_timeout_ms);
88
+ }
89
+
90
+ fn startEmulator(allocator: std.mem.Allocator, options: PreflightOptions, avd: []const u8) !void {
91
+ var argv = std.ArrayList([]const u8).empty;
92
+ defer argv.deinit(allocator);
93
+ try argv.append(allocator, options.emulator_path);
94
+ try argv.appendSlice(allocator, &.{ "-avd", avd });
95
+ if (options.restore_snapshot) |snapshot| {
96
+ try argv.appendSlice(allocator, &.{ "-snapshot", snapshot });
97
+ } else {
98
+ try argv.append(allocator, "-no-snapshot-load");
99
+ }
100
+ try argv.appendSlice(allocator, &.{ "-netdelay", "none", "-netspeed", "full" });
101
+ try recordCommand(allocator, options.event_log_path, argv.items);
102
+
103
+ var child = std.process.Child.init(argv.items, allocator);
104
+ child.stdin_behavior = .Ignore;
105
+ child.stdout_behavior = .Ignore;
106
+ child.stderr_behavior = .Ignore;
107
+ try child.spawn();
108
+ }
109
+
110
+ fn waitReady(allocator: std.mem.Allocator, options: PreflightOptions) !void {
111
+ var wait_result = try runAdb(allocator, options, &.{"wait-for-device"});
112
+ defer wait_result.deinit(allocator);
113
+ try wait_result.ensureSuccess();
114
+
115
+ for (0..120) |_| {
116
+ var prop = try runAdb(allocator, options, &.{ "shell", "getprop", "sys.boot_completed" });
117
+ defer prop.deinit(allocator);
118
+ try prop.ensureSuccess();
119
+ const value = std.mem.trim(u8, prop.stdout, " \t\r\n");
120
+ if (std.mem.eql(u8, value, "1")) return;
121
+ std.Thread.sleep(2 * std.time.ns_per_s);
122
+ }
123
+ return error.AndroidEmulatorBootTimedOut;
124
+ }
125
+
126
+ fn runAdb(allocator: std.mem.Allocator, options: PreflightOptions, extra: []const []const u8) !command.ExecResult {
127
+ var argv = std.ArrayList([]const u8).empty;
128
+ defer argv.deinit(allocator);
129
+ try argv.append(allocator, options.adb_path);
130
+ if (options.device_serial) |serial| {
131
+ try argv.appendSlice(allocator, &.{ "-s", serial });
132
+ }
133
+ try argv.appendSlice(allocator, extra);
134
+ try recordCommand(allocator, options.event_log_path, argv.items);
135
+ return try command.runWithTimeout(allocator, argv.items, 1024 * 1024, default_timeout_ms);
136
+ }
137
+
138
+ fn recordCommand(allocator: std.mem.Allocator, maybe_path: ?[]const u8, argv: []const []const u8) !void {
139
+ const path = maybe_path orelse return;
140
+ var file = std.fs.cwd().openFile(path, .{ .mode = .write_only }) catch |err| switch (err) {
141
+ error.FileNotFound => try std.fs.cwd().createFile(path, .{ .truncate = true }),
142
+ else => return err,
143
+ };
144
+ defer file.close();
145
+ try file.seekFromEnd(0);
146
+ var line = std.ArrayList(u8).empty;
147
+ defer line.deinit(allocator);
148
+ for (argv, 0..) |arg, index| {
149
+ if (index > 0) try line.append(allocator, ' ');
150
+ try line.appendSlice(allocator, arg);
151
+ }
152
+ try line.append(allocator, '\n');
153
+ try file.writeAll(line.items);
154
+ }
@@ -0,0 +1,112 @@
1
+ const std = @import("std");
2
+ const command = @import("command.zig");
3
+ const trace = @import("trace.zig");
4
+ const types = @import("types.zig");
5
+
6
+ const default_max_output = 32 * 1024 * 1024;
7
+ const default_adb_timeout_ms = 15_000;
8
+
9
+ pub fn start(
10
+ allocator: std.mem.Allocator,
11
+ adb_path: []const u8,
12
+ serial: ?[]const u8,
13
+ remote_path: []const u8,
14
+ ) !AndroidScreenRecording {
15
+ const cleanup = runAdbCommand(allocator, adb_path, serial, &.{ "shell", "rm", "-f", remote_path }, 4096) catch null;
16
+ if (cleanup) |result| result.deinit(allocator);
17
+
18
+ var argv = std.ArrayList([]const u8).empty;
19
+ defer argv.deinit(allocator);
20
+ try appendAdbBase(allocator, &argv, adb_path, serial);
21
+ try argv.appendSlice(allocator, &.{ "shell", "screenrecord", remote_path });
22
+
23
+ const owned_adb_path = try allocator.dupe(u8, adb_path);
24
+ errdefer allocator.free(owned_adb_path);
25
+ const owned_serial = try types.dupeOptional(allocator, serial);
26
+ errdefer if (owned_serial) |value| allocator.free(value);
27
+ const owned_remote_path = try allocator.dupe(u8, remote_path);
28
+ errdefer allocator.free(owned_remote_path);
29
+
30
+ var child = std.process.Child.init(argv.items, allocator);
31
+ child.stdin_behavior = .Ignore;
32
+ child.stdout_behavior = .Ignore;
33
+ child.stderr_behavior = .Ignore;
34
+ try child.spawn();
35
+
36
+ return .{
37
+ .allocator = allocator,
38
+ .adb_path = owned_adb_path,
39
+ .serial = owned_serial,
40
+ .remote_path = owned_remote_path,
41
+ .child = child,
42
+ };
43
+ }
44
+
45
+ pub const AndroidScreenRecording = struct {
46
+ allocator: std.mem.Allocator,
47
+ adb_path: []const u8,
48
+ serial: ?[]const u8,
49
+ remote_path: []const u8,
50
+ child: std.process.Child,
51
+ stopped: bool = false,
52
+
53
+ pub fn deinit(self: *AndroidScreenRecording) void {
54
+ self.stopProcess() catch {};
55
+ self.allocator.free(self.adb_path);
56
+ if (self.serial) |value| self.allocator.free(value);
57
+ self.allocator.free(self.remote_path);
58
+ }
59
+
60
+ pub fn stopAndPull(self: *AndroidScreenRecording, writer: *trace.TraceWriter, artifact_name: []const u8) ![]const u8 {
61
+ try self.stopProcess();
62
+ const artifact_path = try writer.artifactPath(artifact_name);
63
+ errdefer self.allocator.free(artifact_path);
64
+
65
+ const pull = try self.runAdb(&.{ "pull", self.remote_path, artifact_path }, default_max_output);
66
+ defer pull.deinit(self.allocator);
67
+ try pull.ensureSuccess();
68
+
69
+ const cleanup = self.runAdb(&.{ "shell", "rm", "-f", self.remote_path }, 4096) catch null;
70
+ if (cleanup) |result| result.deinit(self.allocator);
71
+
72
+ return artifact_path;
73
+ }
74
+
75
+ fn stopProcess(self: *AndroidScreenRecording) !void {
76
+ if (self.stopped) return;
77
+ std.posix.kill(self.child.id, std.posix.SIG.INT) catch {};
78
+ _ = try self.child.wait();
79
+ self.stopped = true;
80
+ }
81
+
82
+ fn runAdb(self: *AndroidScreenRecording, extra: []const []const u8, max_output_bytes: usize) !command.ExecResult {
83
+ return try runAdbCommand(self.allocator, self.adb_path, self.serial, extra, max_output_bytes);
84
+ }
85
+ };
86
+
87
+ fn runAdbCommand(
88
+ allocator: std.mem.Allocator,
89
+ adb_path: []const u8,
90
+ serial: ?[]const u8,
91
+ extra: []const []const u8,
92
+ max_output_bytes: usize,
93
+ ) !command.ExecResult {
94
+ var argv = std.ArrayList([]const u8).empty;
95
+ defer argv.deinit(allocator);
96
+ try appendAdbBase(allocator, &argv, adb_path, serial);
97
+ try argv.appendSlice(allocator, extra);
98
+ return try command.runWithTimeout(allocator, argv.items, max_output_bytes, default_adb_timeout_ms);
99
+ }
100
+
101
+ fn appendAdbBase(
102
+ allocator: std.mem.Allocator,
103
+ argv: *std.ArrayList([]const u8),
104
+ adb_path: []const u8,
105
+ serial: ?[]const u8,
106
+ ) !void {
107
+ try argv.append(allocator, adb_path);
108
+ if (serial) |value| {
109
+ try argv.append(allocator, "-s");
110
+ try argv.append(allocator, value);
111
+ }
112
+ }
@@ -0,0 +1,112 @@
1
+ const std = @import("std");
2
+ const command = @import("command.zig");
3
+
4
+ pub const Args = struct {
5
+ allocator: std.mem.Allocator,
6
+ argv: std.ArrayList([]const u8) = .empty,
7
+ owned: std.ArrayList([]const u8) = .empty,
8
+
9
+ pub fn deinit(self: *Args) void {
10
+ for (self.owned.items) |value| self.allocator.free(value);
11
+ self.owned.deinit(self.allocator);
12
+ self.argv.deinit(self.allocator);
13
+ }
14
+
15
+ pub fn items(self: *const Args) []const []const u8 {
16
+ return self.argv.items;
17
+ }
18
+
19
+ fn appendLiteral(self: *Args, value: []const u8) !void {
20
+ try self.argv.append(self.allocator, value);
21
+ }
22
+
23
+ fn appendOwned(self: *Args, value: []const u8) !void {
24
+ try self.owned.append(self.allocator, value);
25
+ try self.argv.append(self.allocator, value);
26
+ }
27
+
28
+ fn appendFmt(self: *Args, comptime fmt: []const u8, values: anytype) !void {
29
+ const value = try std.fmt.allocPrint(self.allocator, fmt, values);
30
+ errdefer self.allocator.free(value);
31
+ try self.appendOwned(value);
32
+ }
33
+ };
34
+
35
+ fn initArgs(allocator: std.mem.Allocator) Args {
36
+ return .{ .allocator = allocator };
37
+ }
38
+
39
+ pub fn openLinkIntent(allocator: std.mem.Allocator, url: []const u8, app_id: []const u8) !Args {
40
+ var args = initArgs(allocator);
41
+ errdefer args.deinit();
42
+
43
+ const escaped_url = try command.escapeAdbShellArg(allocator, url);
44
+ errdefer allocator.free(escaped_url);
45
+
46
+ try args.appendLiteral("shell");
47
+ try args.appendLiteral("am");
48
+ try args.appendLiteral("start");
49
+ try args.appendLiteral("-a");
50
+ try args.appendLiteral("android.intent.action.VIEW");
51
+ try args.appendLiteral("-d");
52
+ try args.appendOwned(escaped_url);
53
+ try args.appendLiteral(app_id);
54
+ return args;
55
+ }
56
+
57
+ pub fn tap(allocator: std.mem.Allocator, x: i32, y: i32) !Args {
58
+ var args = initArgs(allocator);
59
+ errdefer args.deinit();
60
+ try args.appendLiteral("shell");
61
+ try args.appendLiteral("input");
62
+ try args.appendLiteral("tap");
63
+ try args.appendFmt("{d}", .{x});
64
+ try args.appendFmt("{d}", .{y});
65
+ return args;
66
+ }
67
+
68
+ pub fn typeText(allocator: std.mem.Allocator, text: []const u8) !Args {
69
+ var args = initArgs(allocator);
70
+ errdefer args.deinit();
71
+ const escaped = try command.escapeAdbInputText(allocator, text);
72
+ errdefer allocator.free(escaped);
73
+ try args.appendLiteral("shell");
74
+ try args.appendLiteral("input");
75
+ try args.appendLiteral("text");
76
+ try args.appendOwned(escaped);
77
+ return args;
78
+ }
79
+
80
+ pub fn eraseText(allocator: std.mem.Allocator, max_chars: u32) !Args {
81
+ var args = initArgs(allocator);
82
+ errdefer args.deinit();
83
+ try args.appendLiteral("shell");
84
+ try args.appendLiteral("sh");
85
+ try args.appendLiteral("-c");
86
+ try args.appendFmt("input keyevent KEYCODE_MOVE_END; i=0; while [ $i -lt {d} ]; do input keyevent KEYCODE_DEL; i=$((i+1)); done", .{max_chars});
87
+ return args;
88
+ }
89
+
90
+ pub fn pressBack(allocator: std.mem.Allocator) !Args {
91
+ var args = initArgs(allocator);
92
+ errdefer args.deinit();
93
+ try args.appendLiteral("shell");
94
+ try args.appendLiteral("input");
95
+ try args.appendLiteral("keyevent");
96
+ try args.appendLiteral("BACK");
97
+ return args;
98
+ }
99
+
100
+ pub fn swipe(allocator: std.mem.Allocator, x1: i32, y1: i32, x2: i32, y2: i32, duration_ms: u32) !Args {
101
+ var args = initArgs(allocator);
102
+ errdefer args.deinit();
103
+ try args.appendLiteral("shell");
104
+ try args.appendLiteral("input");
105
+ try args.appendLiteral("swipe");
106
+ try args.appendFmt("{d}", .{x1});
107
+ try args.appendFmt("{d}", .{y1});
108
+ try args.appendFmt("{d}", .{x2});
109
+ try args.appendFmt("{d}", .{y2});
110
+ try args.appendFmt("{d}", .{duration_ms});
111
+ return args;
112
+ }
package/src/bundle.zig ADDED
@@ -0,0 +1,124 @@
1
+ const std = @import("std");
2
+ const bundle_redaction = @import("bundle_redaction.zig");
3
+ const bundle_tar = @import("bundle_tar.zig");
4
+
5
+ pub const ExportOptions = bundle_redaction.Options;
6
+
7
+ pub fn exportTraceBundle(allocator: std.mem.Allocator, trace_dir: []const u8, out_path: []const u8) !void {
8
+ return exportTraceBundleWithOptions(allocator, trace_dir, out_path, .{});
9
+ }
10
+
11
+ pub fn exportTraceBundleWithOptions(
12
+ allocator: std.mem.Allocator,
13
+ trace_dir: []const u8,
14
+ out_path: []const u8,
15
+ options: ExportOptions,
16
+ ) !void {
17
+ try requireTraceFile(allocator, trace_dir, "trace.json", error.MissingTraceManifest);
18
+ try requireTraceFile(allocator, trace_dir, "events.jsonl", error.MissingTraceEvents);
19
+
20
+ var entries = std.ArrayList([]const u8).empty;
21
+ defer {
22
+ for (entries.items) |entry| allocator.free(entry);
23
+ entries.deinit(allocator);
24
+ }
25
+
26
+ try entries.append(allocator, try allocator.dupe(u8, "trace.json"));
27
+ try entries.append(allocator, try allocator.dupe(u8, "events.jsonl"));
28
+ if (traceFileExists(allocator, trace_dir, "report.html") catch |err| return err) {
29
+ try entries.append(allocator, try allocator.dupe(u8, "report.html"));
30
+ }
31
+
32
+ var artifact_entries = std.ArrayList([]const u8).empty;
33
+ defer {
34
+ for (artifact_entries.items) |entry| allocator.free(entry);
35
+ artifact_entries.deinit(allocator);
36
+ }
37
+ try collectArtifactEntries(allocator, trace_dir, "artifacts", &artifact_entries);
38
+ std.mem.sort([]const u8, artifact_entries.items, {}, stringLessThan);
39
+ for (artifact_entries.items) |entry| {
40
+ try entries.append(allocator, try allocator.dupe(u8, entry));
41
+ }
42
+
43
+ var out_file = try std.fs.cwd().createFile(out_path, .{ .truncate = true });
44
+ defer out_file.close();
45
+
46
+ for (entries.items) |archive_path| {
47
+ if (options.redact) {
48
+ if (options.omit_screenshots and bundle_redaction.isVisualArtifactPath(archive_path)) continue;
49
+ if (bundle_redaction.isPlaceholderScreenshotPath(archive_path)) {
50
+ try bundle_tar.writeBytes(archive_path, bundle_redaction.redacted_screenshot_png[0..], &out_file);
51
+ continue;
52
+ }
53
+ if (bundle_redaction.isVisualArtifactPath(archive_path)) continue;
54
+ const bytes = try readTraceFile(allocator, trace_dir, archive_path);
55
+ defer allocator.free(bytes);
56
+ const redacted = try bundle_redaction.redactEntry(allocator, archive_path, bytes, options);
57
+ defer allocator.free(redacted);
58
+ try bundle_tar.writeBytes(archive_path, redacted, &out_file);
59
+ } else {
60
+ try bundle_tar.writeFile(allocator, trace_dir, archive_path, &out_file);
61
+ }
62
+ }
63
+ try out_file.writeAll(&([_]u8{0} ** 1024));
64
+ }
65
+
66
+ fn requireTraceFile(
67
+ allocator: std.mem.Allocator,
68
+ trace_dir: []const u8,
69
+ archive_path: []const u8,
70
+ missing_error: anyerror,
71
+ ) !void {
72
+ if (!try traceFileExists(allocator, trace_dir, archive_path)) return missing_error;
73
+ }
74
+
75
+ fn traceFileExists(allocator: std.mem.Allocator, trace_dir: []const u8, archive_path: []const u8) !bool {
76
+ const path = try std.fs.path.join(allocator, &.{ trace_dir, archive_path });
77
+ defer allocator.free(path);
78
+ const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
79
+ error.FileNotFound => return false,
80
+ else => return err,
81
+ };
82
+ file.close();
83
+ return true;
84
+ }
85
+
86
+ fn readTraceFile(allocator: std.mem.Allocator, trace_dir: []const u8, archive_path: []const u8) ![]u8 {
87
+ const path = try std.fs.path.join(allocator, &.{ trace_dir, archive_path });
88
+ defer allocator.free(path);
89
+ return try std.fs.cwd().readFileAlloc(allocator, path, 64 * 1024 * 1024);
90
+ }
91
+
92
+ fn collectArtifactEntries(
93
+ allocator: std.mem.Allocator,
94
+ trace_dir: []const u8,
95
+ archive_dir: []const u8,
96
+ entries: *std.ArrayList([]const u8),
97
+ ) !void {
98
+ const fs_dir = try std.fs.path.join(allocator, &.{ trace_dir, archive_dir });
99
+ defer allocator.free(fs_dir);
100
+
101
+ var dir = std.fs.cwd().openDir(fs_dir, .{ .iterate = true }) catch |err| switch (err) {
102
+ error.FileNotFound => return,
103
+ else => return err,
104
+ };
105
+ defer dir.close();
106
+
107
+ var iterator = dir.iterate();
108
+ while (try iterator.next()) |entry| {
109
+ const archive_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ archive_dir, entry.name });
110
+ errdefer allocator.free(archive_path);
111
+ switch (entry.kind) {
112
+ .file => try entries.append(allocator, archive_path),
113
+ .directory => {
114
+ try collectArtifactEntries(allocator, trace_dir, archive_path, entries);
115
+ allocator.free(archive_path);
116
+ },
117
+ else => allocator.free(archive_path),
118
+ }
119
+ }
120
+ }
121
+
122
+ fn stringLessThan(_: void, left: []const u8, right: []const u8) bool {
123
+ return std.mem.lessThan(u8, left, right);
124
+ }