zeno-mobile-runner 0.2.1 → 0.2.2

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 (74) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/FEATURES.md +1 -1
  3. package/README.md +1 -1
  4. package/build.zig.zon +2 -2
  5. package/clients/kotlin/README.md +1 -1
  6. package/clients/kotlin/build.gradle.kts +1 -1
  7. package/clients/python/pyproject.toml +1 -1
  8. package/clients/rust/Cargo.lock +1 -1
  9. package/clients/rust/Cargo.toml +1 -1
  10. package/clients/typescript/package.json +1 -1
  11. package/docs/protocol-fixtures/core-session.responses.jsonl +1 -1
  12. package/docs/protocol.md +10 -10
  13. package/package.json +3 -1
  14. package/prebuilds/darwin-arm64/zmr +0 -0
  15. package/prebuilds/darwin-x64/zmr +0 -0
  16. package/prebuilds/linux-arm64/zmr +0 -0
  17. package/prebuilds/linux-x64/zmr +0 -0
  18. package/scripts/create-react-native-expo-demo-app.sh +11 -13
  19. package/shims/ios/ZMRShim.swift +40 -12
  20. package/shims/ios/ZMRShimUITestCase.swift +135 -15
  21. package/src/android.zig +10 -9
  22. package/src/android_emulator.zig +22 -11
  23. package/src/android_screen_recording.zig +11 -7
  24. package/src/bundle.zig +10 -9
  25. package/src/bundle_redaction.zig +29 -28
  26. package/src/bundle_tar.zig +15 -12
  27. package/src/cli_devices.zig +7 -3
  28. package/src/cli_discover.zig +7 -3
  29. package/src/cli_doctor.zig +7 -3
  30. package/src/cli_draft.zig +51 -47
  31. package/src/cli_explore.zig +7 -3
  32. package/src/cli_import.zig +8 -4
  33. package/src/cli_info.zig +13 -6
  34. package/src/cli_init.zig +9 -5
  35. package/src/cli_inspect.zig +8 -4
  36. package/src/cli_run.zig +22 -16
  37. package/src/cli_serve.zig +3 -3
  38. package/src/cli_trace.zig +25 -12
  39. package/src/cli_validate.zig +8 -4
  40. package/src/command.zig +81 -99
  41. package/src/config.zig +2 -1
  42. package/src/config_diagnostics.zig +2 -1
  43. package/src/config_paths.zig +2 -1
  44. package/src/doctor.zig +8 -7
  45. package/src/doctor_hints.zig +1 -1
  46. package/src/errors.zig +5 -5
  47. package/src/importer.zig +8 -7
  48. package/src/ios.zig +19 -18
  49. package/src/ios_devices.zig +6 -5
  50. package/src/ios_lifecycle.zig +4 -4
  51. package/src/json_rpc.zig +39 -40
  52. package/src/json_rpc_methods.zig +8 -8
  53. package/src/json_rpc_observation.zig +9 -8
  54. package/src/json_rpc_params.zig +1 -1
  55. package/src/json_rpc_trace.zig +22 -21
  56. package/src/main.zig +19 -10
  57. package/src/mcp.zig +28 -19
  58. package/src/mcp_trace.zig +30 -29
  59. package/src/report.zig +39 -36
  60. package/src/report_html.zig +5 -4
  61. package/src/runner.zig +2 -1
  62. package/src/runner_actions.zig +20 -17
  63. package/src/runner_diagnostics.zig +4 -4
  64. package/src/runner_events.zig +55 -51
  65. package/src/runner_native.zig +21 -19
  66. package/src/runner_waits.zig +46 -41
  67. package/src/scaffold.zig +25 -24
  68. package/src/scenario.zig +4 -3
  69. package/src/stdio.zig +129 -0
  70. package/src/trace.zig +34 -26
  71. package/src/trace_summary.zig +3 -2
  72. package/src/trace_summary_diagnostic.zig +15 -13
  73. package/src/validation.zig +5 -4
  74. package/src/version.zig +1 -1
@@ -187,7 +187,7 @@ final class ZMRShimUITestCase: XCTestCase {
187
187
  case "appState":
188
188
  return ["status": "ok", "state": app.state.rawValue]
189
189
  case "acceptSystemAlert":
190
- return acceptSystemAlert(buttonText: command.text ?? "Open")
190
+ return acceptSystemAlert(buttonText: command.text ?? "Open", app: app)
191
191
  default:
192
192
  return error("unknown.command", "unsupported command: \(command.cmd)")
193
193
  }
@@ -229,7 +229,7 @@ final class ZMRShimUITestCase: XCTestCase {
229
229
  ["status": "error", "code": code, "message": message]
230
230
  }
231
231
 
232
- private func acceptSystemAlert(buttonText: String) -> [String: Any] {
232
+ private func acceptSystemAlert(buttonText: String, app: XCUIApplication) -> [String: Any] {
233
233
  let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
234
234
  var labels = [buttonText, "Open", "Allow", "OK", "Continue"]
235
235
  labels = labels.reduce(into: [String]()) { unique, label in
@@ -264,12 +264,121 @@ final class ZMRShimUITestCase: XCTestCase {
264
264
  }
265
265
  }
266
266
 
267
+ let expoDeepLinkSelection = acceptExpoDevClientDeepLink(app: app)
268
+ if expoDeepLinkSelection.accepted {
269
+ acceptedCount += 1
270
+ lastAcceptedLabel = expoDeepLinkSelection.label
271
+ }
272
+
273
+ let expoHomeSelection = resumeExpoDevClientHome(app: app)
274
+ if expoHomeSelection.accepted {
275
+ acceptedCount += 1
276
+ lastAcceptedLabel = expoHomeSelection.label
277
+ }
278
+
267
279
  if acceptedCount > 0 {
268
280
  return ["status": "ok", "accepted": true, "label": lastAcceptedLabel, "count": acceptedCount]
269
281
  }
270
282
  return ["status": "ok", "accepted": false, "count": 0]
271
283
  }
272
284
 
285
+ private func acceptExpoDevClientDeepLink(app: XCUIApplication) -> (accepted: Bool, label: String) {
286
+ guard app.staticTexts["Deep link received:"].waitForExistence(timeout: 1) else {
287
+ return (false, "")
288
+ }
289
+
290
+ let candidateQueries = [
291
+ app.buttons.allElementsBoundByIndex,
292
+ app.cells.allElementsBoundByIndex,
293
+ app.staticTexts.allElementsBoundByIndex
294
+ ]
295
+
296
+ for elements in candidateQueries {
297
+ for element in elements {
298
+ let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
299
+ guard isExpoDevClientDeepLinkTarget(label: label), element.isHittable else {
300
+ continue
301
+ }
302
+
303
+ element.tap()
304
+ Thread.sleep(forTimeInterval: 1.0)
305
+ return (true, label)
306
+ }
307
+ }
308
+
309
+ return (false, "")
310
+ }
311
+
312
+ private func isExpoDevClientDeepLinkTarget(label: String) -> Bool {
313
+ if label.isEmpty {
314
+ return false
315
+ }
316
+
317
+ let rejectedExactLabels = [
318
+ "Deep link received:",
319
+ "Select an app to open it:",
320
+ "Go back"
321
+ ]
322
+ if rejectedExactLabels.contains(label) {
323
+ return false
324
+ }
325
+
326
+ if label.contains("://") || label.hasPrefix("Note:") || label.contains("next app you open") {
327
+ return false
328
+ }
329
+
330
+ return true
331
+ }
332
+
333
+ private func resumeExpoDevClientHome(app: XCUIApplication) -> (accepted: Bool, label: String) {
334
+ guard app.staticTexts["Development servers"].waitForExistence(timeout: 1) else {
335
+ return (false, "")
336
+ }
337
+
338
+ let candidateQueries = [
339
+ app.buttons.allElementsBoundByIndex,
340
+ app.cells.allElementsBoundByIndex,
341
+ app.staticTexts.allElementsBoundByIndex
342
+ ]
343
+
344
+ for elements in candidateQueries {
345
+ for element in elements {
346
+ let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
347
+ guard isExpoDevClientProjectTarget(label: label), element.exists else {
348
+ continue
349
+ }
350
+
351
+ element.coordinate(withNormalizedOffset: CGVector(dx: 0.5, dy: 0.5)).tap()
352
+ Thread.sleep(forTimeInterval: 1.0)
353
+ return (true, label)
354
+ }
355
+ }
356
+
357
+ return (false, "")
358
+ }
359
+
360
+ private func isExpoDevClientProjectTarget(label: String) -> Bool {
361
+ if label.isEmpty {
362
+ return false
363
+ }
364
+
365
+ let rejectedExactLabels = [
366
+ "Development servers",
367
+ "Recently opened",
368
+ "Fetch development servers",
369
+ "Enter URL manually"
370
+ ]
371
+ if rejectedExactLabels.contains(label) {
372
+ return false
373
+ }
374
+
375
+ if label.hasPrefix("http://") || label.hasPrefix("https://") {
376
+ return false
377
+ }
378
+
379
+ return label.contains(" http://") || label.contains(" https://")
380
+ }
381
+
273
382
  private func hideKeyboard(app: XCUIApplication) -> [String: Any] {
274
383
  guard app.keyboards.firstMatch.exists else {
275
384
  return ok()
@@ -375,41 +484,39 @@ final class ZMRShimUITestCase: XCTestCase {
375
484
  return nil
376
485
  }
377
486
 
378
- let query: XCUIElementQuery?
487
+ let queries: [XCUIElementQuery]
379
488
  switch parts.field {
380
489
  case "text", "label":
381
490
  let predicate = parts.contains
382
491
  ? NSPredicate(format: "label CONTAINS[c] %@", parts.value)
383
492
  : NSPredicate(format: "label == %@", parts.value)
384
- query = app.descendants(matching: .any).matching(predicate)
493
+ queries = allDescendantQueries(app: app, type: .any).map { $0.matching(predicate) }
385
494
  case "identifier", "resourceId":
386
495
  let predicate = parts.contains
387
496
  ? NSPredicate(format: "identifier CONTAINS[c] %@", parts.value)
388
497
  : NSPredicate(format: "identifier == %@", parts.value)
389
- query = app.descendants(matching: .any).matching(predicate)
498
+ queries = allDescendantQueries(app: app, type: .any).map { $0.matching(predicate) }
390
499
  case "value":
391
500
  let predicate = parts.contains
392
501
  ? NSPredicate(format: "value CONTAINS[c] %@", parts.value)
393
502
  : NSPredicate(format: "value == %@", parts.value)
394
- query = app.descendants(matching: .any).matching(predicate)
503
+ queries = allDescendantQueries(app: app, type: .any).map { $0.matching(predicate) }
395
504
  case "id":
396
505
  if parts.value.hasPrefix("id:") {
397
506
  let identifier = String(parts.value.dropFirst("id:".count))
398
- query = app.descendants(matching: .any).matching(identifier: identifier)
507
+ queries = allDescendantQueries(app: app, type: .any).map { $0.matching(identifier: identifier) }
399
508
  } else if parts.value.hasPrefix("label:") {
400
509
  let label = String(parts.value.dropFirst("label:".count))
401
- query = app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", label))
510
+ let predicate = NSPredicate(format: "label == %@", label)
511
+ queries = allDescendantQueries(app: app, type: .any).map { $0.matching(predicate) }
402
512
  } else {
403
- query = nil
513
+ queries = []
404
514
  }
405
515
  default:
406
- query = nil
516
+ queries = []
407
517
  }
408
518
 
409
- guard let element = query?.firstMatch, element.exists else {
410
- return nil
411
- }
412
- return element
519
+ return firstExistingElement(queries: queries)
413
520
  }
414
521
 
415
522
  private func resolveFastElement(selector: String, app: XCUIApplication, preferredTypes: [XCUIElement.ElementType]) -> XCUIElement? {
@@ -449,9 +556,15 @@ final class ZMRShimUITestCase: XCTestCase {
449
556
  private func fastTextQueries(app: XCUIApplication, preferredTypes: [XCUIElement.ElementType]) -> [XCUIElementQuery] {
450
557
  var queries: [XCUIElementQuery] = []
451
558
  if !preferredTypes.isEmpty {
452
- queries.append(contentsOf: preferredTypes.map { app.descendants(matching: $0) })
559
+ queries.append(contentsOf: preferredTypes.flatMap { allDescendantQueries(app: app, type: $0) })
453
560
  }
454
561
  queries.append(contentsOf: [
562
+ app.windows.descendants(matching: .button),
563
+ app.windows.descendants(matching: .staticText),
564
+ app.windows.descendants(matching: .textField),
565
+ app.windows.descendants(matching: .secureTextField),
566
+ app.windows.descendants(matching: .textView),
567
+ app.windows.descendants(matching: .image),
455
568
  app.buttons,
456
569
  app.staticTexts,
457
570
  app.textFields,
@@ -462,6 +575,13 @@ final class ZMRShimUITestCase: XCTestCase {
462
575
  return queries
463
576
  }
464
577
 
578
+ private func allDescendantQueries(app: XCUIApplication, type: XCUIElement.ElementType) -> [XCUIElementQuery] {
579
+ [
580
+ app.windows.descendants(matching: type),
581
+ app.descendants(matching: type)
582
+ ]
583
+ }
584
+
465
585
  private func fastIdentifierQueries(
466
586
  app: XCUIApplication,
467
587
  preferredTypes: [XCUIElement.ElementType],
package/src/android.zig CHANGED
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const command = @import("command.zig");
3
4
  const android_device_info = @import("android_device_info.zig");
4
5
  const android_shell = @import("android_shell.zig");
@@ -93,7 +94,7 @@ pub const AndroidDevice = struct {
93
94
 
94
95
  if (self.isAppForeground() catch false) return;
95
96
  if (attempt + 1 < open_link_attempts) {
96
- std.Thread.sleep(open_link_retry_delay_ms * std.time.ns_per_ms);
97
+ stdio.sleepNs(open_link_retry_delay_ms * std.time.ns_per_ms);
97
98
  }
98
99
  }
99
100
  return error.AppDidNotOpen;
@@ -164,11 +165,11 @@ pub const AndroidDevice = struct {
164
165
  .duration_ms = @as(u32, @intCast(@min(timeout_ms, std.math.maxInt(u32)))),
165
166
  });
166
167
  }
167
- std.Thread.sleep(timeout_ms * std.time.ns_per_ms);
168
+ stdio.sleepNs(timeout_ms * std.time.ns_per_ms);
168
169
  }
169
170
 
170
171
  pub fn snapshot(self: *AndroidDevice, writer: ?*trace.TraceWriter) !types.ObservationSnapshot {
171
- const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{std.time.milliTimestamp()});
172
+ const id = if (writer) |tw| try tw.nextSnapshotId() else try std.fmt.allocPrint(self.allocator, "snapshot-{d}", .{stdio.nowMs()});
172
173
  errdefer self.allocator.free(id);
173
174
 
174
175
  const xml = if (self.shim_path == null) try self.dumpHierarchy() else null;
@@ -215,7 +216,7 @@ pub const AndroidDevice = struct {
215
216
 
216
217
  return .{
217
218
  .id = id,
218
- .timestamp_ms = std.time.milliTimestamp(),
219
+ .timestamp_ms = stdio.nowMs(),
219
220
  .viewport = screen,
220
221
  .display_density_dpi = display_density_dpi,
221
222
  .active_package = active.package,
@@ -273,7 +274,7 @@ pub const AndroidDevice = struct {
273
274
  fn logDelta(self: *AndroidDevice) !?[]const u8 {
274
275
  const result = try self.runAdb(&.{ "logcat", "-d", "-t", "80" }, 1024 * 1024);
275
276
  defer result.deinit(self.allocator);
276
- if (result.term != .Exited or result.term.Exited != 0) return null;
277
+ if (result.term != .exited or result.term.exited != 0) return null;
277
278
  return try self.allocator.dupe(u8, result.stdout);
278
279
  }
279
280
 
@@ -292,11 +293,11 @@ pub const AndroidDevice = struct {
292
293
  fn runShim(self: *AndroidDevice, shim_command: ios_shim.Command) ![]u8 {
293
294
  const path = self.shim_path orelse return error.AndroidShimRequired;
294
295
 
295
- var input = std.ArrayList(u8).empty;
296
- defer input.deinit(self.allocator);
297
- try ios_shim.writeCommandJson(input.writer(self.allocator), shim_command);
296
+ var input: std.Io.Writer.Allocating = .init(self.allocator);
297
+ defer input.deinit();
298
+ try ios_shim.writeCommandJson(&input.writer, shim_command);
298
299
 
299
- const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.items, 4 * 1024 * 1024, shim_timeout_ms);
300
+ const result = try command.runWithInputTimeout(self.allocator, &.{path}, input.writer.buffered(), 4 * 1024 * 1024, shim_timeout_ms);
300
301
  defer result.deinit(self.allocator);
301
302
  try result.ensureSuccess();
302
303
  return try self.allocator.dupe(u8, result.stdout);
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const command = @import("command.zig");
3
4
 
4
5
  const default_timeout_ms = 15_000;
@@ -100,11 +101,12 @@ fn startEmulator(allocator: std.mem.Allocator, options: PreflightOptions, avd: [
100
101
  try argv.appendSlice(allocator, &.{ "-netdelay", "none", "-netspeed", "full" });
101
102
  try recordCommand(allocator, options.event_log_path, argv.items);
102
103
 
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();
104
+ _ = try std.process.spawn(stdio.io(), .{
105
+ .argv = argv.items,
106
+ .stdin = .ignore,
107
+ .stdout = .ignore,
108
+ .stderr = .ignore,
109
+ });
108
110
  }
109
111
 
110
112
  fn waitReady(allocator: std.mem.Allocator, options: PreflightOptions) !void {
@@ -118,7 +120,7 @@ fn waitReady(allocator: std.mem.Allocator, options: PreflightOptions) !void {
118
120
  try prop.ensureSuccess();
119
121
  const value = std.mem.trim(u8, prop.stdout, " \t\r\n");
120
122
  if (std.mem.eql(u8, value, "1")) return;
121
- std.Thread.sleep(2 * std.time.ns_per_s);
123
+ stdio.sleepNs(2 * std.time.ns_per_s);
122
124
  }
123
125
  return error.AndroidEmulatorBootTimedOut;
124
126
  }
@@ -137,12 +139,20 @@ fn runAdb(allocator: std.mem.Allocator, options: PreflightOptions, extra: []cons
137
139
 
138
140
  fn recordCommand(allocator: std.mem.Allocator, maybe_path: ?[]const u8, argv: []const []const u8) !void {
139
141
  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
+ const existing = stdio.readFileAlloc(allocator, path, 4 * 1024 * 1024) catch |err| switch (err) {
143
+ error.FileNotFound => "",
142
144
  else => return err,
143
145
  };
144
- defer file.close();
145
- try file.seekFromEnd(0);
146
+ const had_existing_file = existing.ptr != "".ptr;
147
+ defer if (had_existing_file) allocator.free(existing);
148
+
149
+ var file = try std.Io.Dir.cwd().createFile(stdio.io(), path, .{ .truncate = true });
150
+ defer file.close(stdio.io());
151
+ var write_buffer: [8192]u8 = undefined;
152
+ var file_writer = file.writerStreaming(stdio.io(), &write_buffer);
153
+ const writer = &file_writer.interface;
154
+ if (existing.len > 0) try writer.writeAll(existing);
155
+
146
156
  var line = std.ArrayList(u8).empty;
147
157
  defer line.deinit(allocator);
148
158
  for (argv, 0..) |arg, index| {
@@ -150,5 +160,6 @@ fn recordCommand(allocator: std.mem.Allocator, maybe_path: ?[]const u8, argv: []
150
160
  try line.appendSlice(allocator, arg);
151
161
  }
152
162
  try line.append(allocator, '\n');
153
- try file.writeAll(line.items);
163
+ try writer.writeAll(line.items);
164
+ try writer.flush();
154
165
  }
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const command = @import("command.zig");
3
4
  const trace = @import("trace.zig");
4
5
  const types = @import("types.zig");
@@ -27,11 +28,12 @@ pub fn start(
27
28
  const owned_remote_path = try allocator.dupe(u8, remote_path);
28
29
  errdefer allocator.free(owned_remote_path);
29
30
 
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();
31
+ const child = try std.process.spawn(stdio.io(), .{
32
+ .argv = argv.items,
33
+ .stdin = .ignore,
34
+ .stdout = .ignore,
35
+ .stderr = .ignore,
36
+ });
35
37
 
36
38
  return .{
37
39
  .allocator = allocator,
@@ -74,8 +76,10 @@ pub const AndroidScreenRecording = struct {
74
76
 
75
77
  fn stopProcess(self: *AndroidScreenRecording) !void {
76
78
  if (self.stopped) return;
77
- std.posix.kill(self.child.id, std.posix.SIG.INT) catch {};
78
- _ = try self.child.wait();
79
+ if (self.child.id) |child_id| {
80
+ std.posix.kill(child_id, std.posix.SIG.INT) catch {};
81
+ _ = try self.child.wait(stdio.io());
82
+ }
79
83
  self.stopped = true;
80
84
  }
81
85
 
package/src/bundle.zig CHANGED
@@ -1,6 +1,7 @@
1
1
  const std = @import("std");
2
2
  const bundle_redaction = @import("bundle_redaction.zig");
3
3
  const bundle_tar = @import("bundle_tar.zig");
4
+ const stdio = @import("stdio.zig");
4
5
 
5
6
  pub const ExportOptions = bundle_redaction.Options;
6
7
 
@@ -40,8 +41,8 @@ pub fn exportTraceBundleWithOptions(
40
41
  try entries.append(allocator, try allocator.dupe(u8, entry));
41
42
  }
42
43
 
43
- var out_file = try std.fs.cwd().createFile(out_path, .{ .truncate = true });
44
- defer out_file.close();
44
+ var out_file = try std.Io.Dir.cwd().createFile(stdio.io(), out_path, .{ .truncate = true });
45
+ defer out_file.close(stdio.io());
45
46
 
46
47
  for (entries.items) |archive_path| {
47
48
  if (options.redact) {
@@ -60,7 +61,7 @@ pub fn exportTraceBundleWithOptions(
60
61
  try bundle_tar.writeFile(allocator, trace_dir, archive_path, &out_file);
61
62
  }
62
63
  }
63
- try out_file.writeAll(&([_]u8{0} ** 1024));
64
+ try std.Io.File.writeStreamingAll(out_file, stdio.io(), &([_]u8{0} ** 1024));
64
65
  }
65
66
 
66
67
  fn requireTraceFile(
@@ -75,18 +76,18 @@ fn requireTraceFile(
75
76
  fn traceFileExists(allocator: std.mem.Allocator, trace_dir: []const u8, archive_path: []const u8) !bool {
76
77
  const path = try std.fs.path.join(allocator, &.{ trace_dir, archive_path });
77
78
  defer allocator.free(path);
78
- const file = std.fs.cwd().openFile(path, .{}) catch |err| switch (err) {
79
+ const file = std.Io.Dir.cwd().openFile(stdio.io(), path, .{}) catch |err| switch (err) {
79
80
  error.FileNotFound => return false,
80
81
  else => return err,
81
82
  };
82
- file.close();
83
+ file.close(stdio.io());
83
84
  return true;
84
85
  }
85
86
 
86
87
  fn readTraceFile(allocator: std.mem.Allocator, trace_dir: []const u8, archive_path: []const u8) ![]u8 {
87
88
  const path = try std.fs.path.join(allocator, &.{ trace_dir, archive_path });
88
89
  defer allocator.free(path);
89
- return try std.fs.cwd().readFileAlloc(allocator, path, 64 * 1024 * 1024);
90
+ return try stdio.readFileAlloc(allocator, path, 64 * 1024 * 1024);
90
91
  }
91
92
 
92
93
  fn collectArtifactEntries(
@@ -98,14 +99,14 @@ fn collectArtifactEntries(
98
99
  const fs_dir = try std.fs.path.join(allocator, &.{ trace_dir, archive_dir });
99
100
  defer allocator.free(fs_dir);
100
101
 
101
- var dir = std.fs.cwd().openDir(fs_dir, .{ .iterate = true }) catch |err| switch (err) {
102
+ var dir = std.Io.Dir.cwd().openDir(stdio.io(), fs_dir, .{ .iterate = true }) catch |err| switch (err) {
102
103
  error.FileNotFound => return,
103
104
  else => return err,
104
105
  };
105
- defer dir.close();
106
+ defer dir.close(stdio.io());
106
107
 
107
108
  var iterator = dir.iterate();
108
- while (try iterator.next()) |entry| {
109
+ while (try iterator.next(stdio.io())) |entry| {
109
110
  const archive_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ archive_dir, entry.name });
110
111
  errdefer allocator.free(archive_path);
111
112
  switch (entry.kind) {
@@ -24,21 +24,21 @@ fn redactTraceManifest(allocator: std.mem.Allocator, bytes: []const u8, options:
24
24
  if (parsed.value != .object) return try redactJsonishText(allocator, bytes);
25
25
 
26
26
  const arena_allocator = parsed.arena.allocator();
27
- var redaction = std.json.ObjectMap.init(arena_allocator);
28
- try redaction.put("enabled", .{ .bool = true });
29
- try redaction.put("screenshots", .{ .string = if (options.omit_screenshots) "omitted" else "placeholder" });
30
- try redaction.put("screenRecordings", .{ .string = "omitted" });
31
- try redaction.put("textArtifacts", .{ .string = "scrubbed" });
32
- try redaction.put("screenshotsOmitted", .{ .bool = options.omit_screenshots });
33
- try redaction.put("screenshotsRedacted", .{ .bool = !options.omit_screenshots });
34
- try redaction.put("screenRecordingsOmitted", .{ .bool = true });
35
- try parsed.value.object.put("redaction", .{ .object = redaction });
27
+ var redaction = std.json.ObjectMap{};
28
+ try redaction.put(arena_allocator, "enabled", .{ .bool = true });
29
+ try redaction.put(arena_allocator, "screenshots", .{ .string = if (options.omit_screenshots) "omitted" else "placeholder" });
30
+ try redaction.put(arena_allocator, "screenRecordings", .{ .string = "omitted" });
31
+ try redaction.put(arena_allocator, "textArtifacts", .{ .string = "scrubbed" });
32
+ try redaction.put(arena_allocator, "screenshotsOmitted", .{ .bool = options.omit_screenshots });
33
+ try redaction.put(arena_allocator, "screenshotsRedacted", .{ .bool = !options.omit_screenshots });
34
+ try redaction.put(arena_allocator, "screenRecordingsOmitted", .{ .bool = true });
35
+ try parsed.value.object.put(arena_allocator, "redaction", .{ .object = redaction });
36
36
 
37
- var out = std.ArrayList(u8).empty;
38
- errdefer out.deinit(allocator);
39
- try writeJsonValueRedacted(out.writer(allocator), parsed.value, null);
40
- try out.writer(allocator).writeByte('\n');
41
- return try out.toOwnedSlice(allocator);
37
+ var out: std.Io.Writer.Allocating = .init(allocator);
38
+ errdefer out.deinit();
39
+ try writeJsonValueRedacted(&out.writer, parsed.value, null);
40
+ try out.writer.writeByte('\n');
41
+ return try out.toOwnedSlice();
42
42
  }
43
43
 
44
44
  fn redactJsonishText(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
@@ -46,11 +46,11 @@ fn redactJsonishText(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
46
46
  return redactFreeText(allocator, bytes);
47
47
  };
48
48
  defer parsed.deinit();
49
- var out = std.ArrayList(u8).empty;
50
- errdefer out.deinit(allocator);
51
- try writeJsonValueRedacted(out.writer(allocator), parsed.value, null);
52
- try out.writer(allocator).writeByte('\n');
53
- return try out.toOwnedSlice(allocator);
49
+ var out: std.Io.Writer.Allocating = .init(allocator);
50
+ errdefer out.deinit();
51
+ try writeJsonValueRedacted(&out.writer, parsed.value, null);
52
+ try out.writer.writeByte('\n');
53
+ return try out.toOwnedSlice();
54
54
  }
55
55
 
56
56
  fn writeJsonValueRedacted(writer: anytype, value: std.json.Value, key_context: ?[]const u8) !void {
@@ -98,8 +98,9 @@ fn writeRedactedJsonStringForKey(writer: anytype, key: []const u8, value: []cons
98
98
  }
99
99
 
100
100
  fn redactFreeText(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
101
- var out = std.ArrayList(u8).empty;
102
- errdefer out.deinit(allocator);
101
+ var out: std.Io.Writer.Allocating = .init(allocator);
102
+ errdefer out.deinit();
103
+ const writer = &out.writer;
103
104
  var sensitive_tag = false;
104
105
  var index: usize = 0;
105
106
  while (index < bytes.len) {
@@ -111,31 +112,31 @@ fn redactFreeText(allocator: std.mem.Allocator, bytes: []const u8) ![]u8 {
111
112
 
112
113
  if (sensitive_tag and (startsWithAt(bytes, index, "text=\"") or startsWithAt(bytes, index, "content-desc=\""))) {
113
114
  const prefix_len: usize = if (startsWithAt(bytes, index, "text=\"")) 6 else 14;
114
- try out.writer(allocator).writeAll(bytes[index .. index + prefix_len]);
115
- try out.writer(allocator).writeAll("[REDACTED:secret]");
115
+ try writer.writeAll(bytes[index .. index + prefix_len]);
116
+ try writer.writeAll("[REDACTED:secret]");
116
117
  index += prefix_len;
117
118
  while (index < bytes.len and bytes[index] != '"') : (index += 1) {}
118
119
  if (index < bytes.len) {
119
- try out.writer(allocator).writeByte('"');
120
+ try writer.writeByte('"');
120
121
  index += 1;
121
122
  }
122
123
  continue;
123
124
  }
124
125
 
125
126
  if (emailEnd(bytes, index)) |end| {
126
- try out.writer(allocator).writeAll("[REDACTED:email]");
127
+ try writer.writeAll("[REDACTED:email]");
127
128
  index = end;
128
129
  continue;
129
130
  }
130
131
  if (bearerEnd(bytes, index)) |end| {
131
- try out.writer(allocator).writeAll("[REDACTED:token]");
132
+ try writer.writeAll("[REDACTED:token]");
132
133
  index = end;
133
134
  continue;
134
135
  }
135
- try out.writer(allocator).writeByte(bytes[index]);
136
+ try writer.writeByte(bytes[index]);
136
137
  index += 1;
137
138
  }
138
- return try out.toOwnedSlice(allocator);
139
+ return try out.toOwnedSlice();
139
140
  }
140
141
 
141
142
  fn emailEnd(bytes: []const u8, start: usize) ?usize {
@@ -1,42 +1,45 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
 
3
4
  pub fn writeFile(
4
5
  allocator: std.mem.Allocator,
5
6
  trace_dir: []const u8,
6
7
  archive_path: []const u8,
7
- out_file: *std.fs.File,
8
+ out_file: *std.Io.File,
8
9
  ) !void {
9
10
  if (isUnsafeArchivePath(archive_path)) return error.UnsafeArchivePath;
10
11
  const fs_path = try std.fs.path.join(allocator, &.{ trace_dir, archive_path });
11
12
  defer allocator.free(fs_path);
12
13
 
13
- var in_file = try std.fs.cwd().openFile(fs_path, .{});
14
- defer in_file.close();
15
- const stat = try in_file.stat();
14
+ var in_file = try std.Io.Dir.cwd().openFile(stdio.io(), fs_path, .{});
15
+ defer in_file.close(stdio.io());
16
+ const stat = try in_file.stat(stdio.io());
16
17
 
17
18
  var header = [_]u8{0} ** 512;
18
19
  try writeHeader(&header, archive_path, stat.size);
19
20
 
20
- try out_file.writeAll(&header);
21
+ try std.Io.File.writeStreamingAll(out_file.*, stdio.io(), &header);
22
+ var reader_buffer: [16 * 1024]u8 = undefined;
23
+ var reader = in_file.readerStreaming(stdio.io(), &reader_buffer);
21
24
  var buffer: [16 * 1024]u8 = undefined;
22
25
  var remaining = stat.size;
23
26
  while (remaining > 0) {
24
27
  const read_len = @min(buffer.len, remaining);
25
- const n = try in_file.read(buffer[0..read_len]);
28
+ const n = try reader.interface.readSliceShort(buffer[0..read_len]);
26
29
  if (n == 0) return error.UnexpectedEndOfStream;
27
- try out_file.writeAll(buffer[0..n]);
30
+ try std.Io.File.writeStreamingAll(out_file.*, stdio.io(), buffer[0..n]);
28
31
  remaining -= n;
29
32
  }
30
33
  try writePadding(out_file, stat.size);
31
34
  }
32
35
 
33
- pub fn writeBytes(archive_path: []const u8, bytes: []const u8, out_file: *std.fs.File) !void {
36
+ pub fn writeBytes(archive_path: []const u8, bytes: []const u8, out_file: *std.Io.File) !void {
34
37
  if (isUnsafeArchivePath(archive_path)) return error.UnsafeArchivePath;
35
38
  var header = [_]u8{0} ** 512;
36
39
  try writeHeader(&header, archive_path, bytes.len);
37
40
 
38
- try out_file.writeAll(&header);
39
- try out_file.writeAll(bytes);
41
+ try std.Io.File.writeStreamingAll(out_file.*, stdio.io(), &header);
42
+ try std.Io.File.writeStreamingAll(out_file.*, stdio.io(), bytes);
40
43
  try writePadding(out_file, bytes.len);
41
44
  }
42
45
 
@@ -57,9 +60,9 @@ fn writeHeader(header: *[512]u8, archive_path: []const u8, size: u64) !void {
57
60
  writeChecksum(header[148..156], checksum);
58
61
  }
59
62
 
60
- fn writePadding(out_file: *std.fs.File, size: u64) !void {
63
+ fn writePadding(out_file: *std.Io.File, size: u64) !void {
61
64
  const padding = (512 - (size % 512)) % 512;
62
- if (padding > 0) try out_file.writeAll((&([_]u8{0} ** 512))[0..padding]);
65
+ if (padding > 0) try std.Io.File.writeStreamingAll(out_file.*, stdio.io(), (&([_]u8{0} ** 512))[0..padding]);
63
66
  }
64
67
 
65
68
  fn isUnsafeArchivePath(archive_path: []const u8) bool {
@@ -1,4 +1,5 @@
1
1
  const std = @import("std");
2
+ const stdio = @import("stdio.zig");
2
3
  const android = @import("android.zig");
3
4
  const device_registry = @import("device_registry.zig");
4
5
  const ios = @import("ios.zig");
@@ -11,7 +12,7 @@ pub const IosDevicesScope = enum {
11
12
  all,
12
13
  };
13
14
 
14
- pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
15
+ pub fn run(allocator: std.mem.Allocator, args: *std.process.Args.Iterator) !void {
15
16
  var platform: run_options.Platform = .android;
16
17
  var ios_devices_scope: IosDevicesScope = .simulator;
17
18
  var adb_path: []const u8 = "adb";
@@ -30,7 +31,7 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
30
31
  } else if (std.mem.eql(u8, arg, "--json")) {
31
32
  json = true;
32
33
  } else {
33
- return error.UnknownFlag;
34
+ return error.unknownFlag;
34
35
  }
35
36
  }
36
37
 
@@ -43,7 +44,10 @@ pub fn run(allocator: std.mem.Allocator, args: *std.process.ArgIterator) !void {
43
44
  allocator.free(devices);
44
45
  }
45
46
 
46
- const stdout = std.fs.File.stdout().deprecatedWriter();
47
+ var stdout_io: stdio.Output = .{};
48
+ stdout_io.init(.stdout());
49
+ defer stdout_io.deinit();
50
+ const stdout = stdout_io.writer();
47
51
  if (json) return try device_registry.writeJson(stdout, registryPlatform(platform), devices);
48
52
  if (devices.len == 0) return try stdout.print("No {s} devices found.\n", .{@tagName(platform)});
49
53
  for (devices) |device| {