@react-native-harness/platform-android 1.3.0 → 1.4.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 (62) hide show
  1. package/dist/__tests__/adb-wait.test.d.ts +2 -0
  2. package/dist/__tests__/adb-wait.test.d.ts.map +1 -0
  3. package/dist/__tests__/adb-wait.test.js +69 -0
  4. package/dist/__tests__/app-session.test.d.ts +2 -0
  5. package/dist/__tests__/app-session.test.d.ts.map +1 -0
  6. package/dist/__tests__/app-session.test.js +57 -0
  7. package/dist/__tests__/avd-config.test.js +58 -0
  8. package/dist/__tests__/crash-diagnostics.test.d.ts +2 -0
  9. package/dist/__tests__/crash-diagnostics.test.d.ts.map +1 -0
  10. package/dist/__tests__/crash-diagnostics.test.js +119 -0
  11. package/dist/__tests__/crash-reporter.test.d.ts +2 -0
  12. package/dist/__tests__/crash-reporter.test.d.ts.map +1 -0
  13. package/dist/__tests__/crash-reporter.test.js +150 -0
  14. package/dist/__tests__/emulator-startup.test.js +30 -3
  15. package/dist/__tests__/instance.test.js +139 -24
  16. package/dist/adb.d.ts +7 -1
  17. package/dist/adb.d.ts.map +1 -1
  18. package/dist/adb.js +72 -23
  19. package/dist/app-session.d.ts +26 -0
  20. package/dist/app-session.d.ts.map +1 -0
  21. package/dist/app-session.js +214 -0
  22. package/dist/avd-config.d.ts +1 -0
  23. package/dist/avd-config.d.ts.map +1 -1
  24. package/dist/avd-config.js +11 -0
  25. package/dist/crash-diagnostics.d.ts +40 -0
  26. package/dist/crash-diagnostics.d.ts.map +1 -0
  27. package/dist/crash-diagnostics.js +256 -0
  28. package/dist/crash-reporter.d.ts +12 -0
  29. package/dist/crash-reporter.d.ts.map +1 -0
  30. package/dist/crash-reporter.js +150 -0
  31. package/dist/emulator-startup.d.ts +23 -0
  32. package/dist/emulator-startup.d.ts.map +1 -1
  33. package/dist/emulator-startup.js +58 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/instance.d.ts +1 -1
  38. package/dist/instance.d.ts.map +1 -1
  39. package/dist/instance.js +51 -72
  40. package/dist/runner.d.ts +2 -2
  41. package/dist/runner.d.ts.map +1 -1
  42. package/dist/runner.js +1 -1
  43. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  44. package/package.json +4 -4
  45. package/src/__tests__/adb-wait.test.ts +95 -0
  46. package/src/__tests__/app-session.test.ts +76 -0
  47. package/src/__tests__/avd-config.test.ts +70 -0
  48. package/src/__tests__/crash-diagnostics.test.ts +144 -0
  49. package/src/__tests__/crash-reporter.test.ts +167 -0
  50. package/src/__tests__/emulator-startup.test.ts +53 -6
  51. package/src/__tests__/instance.test.ts +226 -84
  52. package/src/adb.ts +129 -63
  53. package/src/app-session.ts +316 -0
  54. package/src/avd-config.ts +17 -0
  55. package/src/crash-diagnostics.ts +471 -0
  56. package/src/crash-reporter.ts +263 -0
  57. package/src/emulator-startup.ts +74 -0
  58. package/src/index.ts +1 -0
  59. package/src/instance.ts +84 -105
  60. package/src/runner.ts +11 -11
  61. package/test/setup.ts +1 -0
  62. package/src/app-monitor.ts +0 -577
package/src/adb.ts CHANGED
@@ -1,8 +1,10 @@
1
1
  import { type AndroidAppLaunchOptions } from '@react-native-harness/platforms';
2
2
  import {
3
+ delay,
3
4
  logger,
4
5
  spawn,
5
6
  SubprocessError,
7
+ waitForAbort,
6
8
  type Subprocess,
7
9
  } from '@react-native-harness/tools';
8
10
  import { spawn as nodeSpawn } from 'node:child_process';
@@ -25,6 +27,7 @@ import {
25
27
  getRequiredAndroidSdkPackages,
26
28
  } from './environment.js';
27
29
  import {
30
+ getEmulatorCpuCores,
28
31
  getEmulatorStartupArgs,
29
32
  type EmulatorBootMode,
30
33
  } from './emulator-startup.js';
@@ -33,37 +36,26 @@ import {
33
36
  AdbPermissionGrantError,
34
37
  } from './adb-errors.js';
35
38
 
36
- const wait = async (ms: number): Promise<void> => {
37
- await new Promise((resolve) => {
38
- setTimeout(resolve, ms);
39
- });
40
- };
41
-
42
- const waitForAbort = (signal: AbortSignal): Promise<never> => {
43
- if (signal.aborted) {
44
- return Promise.reject(signal.reason);
45
- }
46
-
47
- return new Promise((_, reject) => {
48
- signal.addEventListener(
49
- 'abort',
50
- () => {
51
- reject(signal.reason);
52
- },
53
- { once: true },
54
- );
55
- });
56
- };
57
-
58
39
  const waitWithSignal = async (
59
40
  ms: number,
60
- signal: AbortSignal,
41
+ signal: AbortSignal
61
42
  ): Promise<void> => {
62
43
  if (signal.aborted) {
63
44
  throw signal.reason;
64
45
  }
65
46
 
66
- await Promise.race([wait(ms), waitForAbort(signal)]);
47
+ // Both sides of this race must be torn down explicitly when they lose:
48
+ // an uncancelled timer keeps a ref'd Timeout alive, and an uncancelled
49
+ // abort wait leaks a listener on `signal`, which is a session-lifetime
50
+ // signal here and gets polled against once per second.
51
+ const timer = delay(ms);
52
+ const abort = waitForAbort(signal);
53
+ try {
54
+ await Promise.race([timer.promise, abort.promise]);
55
+ } finally {
56
+ timer.cancel();
57
+ abort.cancel();
58
+ }
67
59
  };
68
60
 
69
61
  const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
@@ -73,7 +65,7 @@ const androidAdbLogger = logger.child('android-adb');
73
65
  export const emulatorProcess = {
74
66
  startDetachedProcess: (
75
67
  file: string,
76
- args: readonly string[],
68
+ args: readonly string[]
77
69
  ): ChildProcessByStdio<null, Readable, Readable> =>
78
70
  nodeSpawn(file, args, {
79
71
  detached: true,
@@ -84,7 +76,7 @@ export const emulatorProcess = {
84
76
  const appendBoundedOutput = (
85
77
  output: string,
86
78
  chunk: string,
87
- limit: number = EMULATOR_OUTPUT_BUFFER_LIMIT,
79
+ limit: number = EMULATOR_OUTPUT_BUFFER_LIMIT
88
80
  ): string => {
89
81
  const nextOutput = output + chunk;
90
82
 
@@ -165,7 +157,7 @@ export const getRequiredEmulatorPackages = (apiLevel: number): string[] => {
165
157
  };
166
158
 
167
159
  export const verifyAndroidEmulatorSdk = async (
168
- apiLevel: number,
160
+ apiLevel: number
169
161
  ): Promise<void> => {
170
162
  await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel));
171
163
  };
@@ -192,7 +184,7 @@ const ensureAvdProfileAvailable = async (profile: string): Promise<void> => {
192
184
  : 'None reported by avdmanager.';
193
185
 
194
186
  throw new Error(
195
- `Android AVD profile "${profile}" is not available on this machine. Available profiles: ${availableProfilesList}`,
187
+ `Android AVD profile "${profile}" is not available on this machine. Available profiles: ${availableProfilesList}`
196
188
  );
197
189
  };
198
190
 
@@ -201,7 +193,7 @@ const ensureAvdConfigExists = async (name: string): Promise<string> => {
201
193
 
202
194
  if ((await readAvdConfig(name)) == null) {
203
195
  throw new Error(
204
- `Android AVD "${name}" was created, but config.ini was not found at ${configPath}.`,
196
+ `Android AVD "${name}" was created, but config.ini was not found at ${configPath}.`
205
197
  );
206
198
  }
207
199
 
@@ -235,7 +227,7 @@ const ensureAvdIniExists = async ({
235
227
  export const getStartAppArgs = (
236
228
  bundleId: string,
237
229
  activityName: string,
238
- options?: AndroidAppLaunchOptions,
230
+ options?: AndroidAppLaunchOptions
239
231
  ): string[] => {
240
232
  const args = [
241
233
  'shell',
@@ -264,7 +256,7 @@ export const getStartAppArgs = (
264
256
 
265
257
  if (!Number.isSafeInteger(value)) {
266
258
  throw new Error(
267
- `Android app launch option "${key}" must be a safe integer.`,
259
+ `Android app launch option "${key}" must be a safe integer.`
268
260
  );
269
261
  }
270
262
 
@@ -276,7 +268,7 @@ export const getStartAppArgs = (
276
268
 
277
269
  export const isAppInstalled = async (
278
270
  adbId: string,
279
- bundleId: string,
271
+ bundleId: string
280
272
  ): Promise<boolean> => {
281
273
  const { stdout } = await spawn(getAdbBinaryPath(), [
282
274
  '-s',
@@ -293,7 +285,7 @@ export const isAppInstalled = async (
293
285
  export const reversePort = async (
294
286
  adbId: string,
295
287
  port: number,
296
- hostPort: number = port,
288
+ hostPort: number = port
297
289
  ): Promise<void> => {
298
290
  await spawn(getAdbBinaryPath(), [
299
291
  '-s',
@@ -306,7 +298,7 @@ export const reversePort = async (
306
298
 
307
299
  export const stopApp = async (
308
300
  adbId: string,
309
- bundleId: string,
301
+ bundleId: string
310
302
  ): Promise<void> => {
311
303
  await spawn(getAdbBinaryPath(), [
312
304
  '-s',
@@ -322,7 +314,7 @@ export const startApp = async (
322
314
  adbId: string,
323
315
  bundleId: string,
324
316
  activityName: string,
325
- options?: AndroidAppLaunchOptions,
317
+ options?: AndroidAppLaunchOptions
326
318
  ): Promise<void> => {
327
319
  await spawn(getAdbBinaryPath(), [
328
320
  '-s',
@@ -341,7 +333,7 @@ export const getDeviceIds = async (): Promise<string[]> => {
341
333
  };
342
334
 
343
335
  export const getEmulatorName = async (
344
- adbId: string,
336
+ adbId: string
345
337
  ): Promise<string | null> => {
346
338
  const { stdout } = await spawn(getAdbBinaryPath(), [
347
339
  '-s',
@@ -355,7 +347,7 @@ export const getEmulatorName = async (
355
347
 
356
348
  export const getShellProperty = async (
357
349
  adbId: string,
358
- property: string,
350
+ property: string
359
351
  ): Promise<string | null> => {
360
352
  const { stdout } = await spawn(getAdbBinaryPath(), [
361
353
  '-s',
@@ -377,7 +369,7 @@ export type DeviceInfo = {
377
369
  };
378
370
 
379
371
  export const getDeviceInfo = async (
380
- adbId: string,
372
+ adbId: string
381
373
  ): Promise<DeviceInfo | null> => {
382
374
  const manufacturer = await getShellProperty(adbId, 'ro.product.manufacturer');
383
375
  const model = await getShellProperty(adbId, 'ro.product.model');
@@ -386,7 +378,7 @@ export const getDeviceInfo = async (
386
378
 
387
379
  const getRequestedPermissions = async (
388
380
  adbId: string,
389
- bundleId: string,
381
+ bundleId: string
390
382
  ): Promise<string[]> => {
391
383
  const { stdout } = await spawn(getAdbBinaryPath(), [
392
384
  '-s',
@@ -476,14 +468,14 @@ export const stopEmulator = async (adbId: string): Promise<void> => {
476
468
 
477
469
  export const installApp = async (
478
470
  adbId: string,
479
- appPath: string,
471
+ appPath: string
480
472
  ): Promise<void> => {
481
473
  await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
482
474
  };
483
475
 
484
476
  export const uninstallApp = async (
485
477
  adbId: string,
486
- bundleId: string,
478
+ bundleId: string
487
479
  ): Promise<void> => {
488
480
  await spawn(getAdbBinaryPath(), ['-s', adbId, 'uninstall', bundleId]);
489
481
  };
@@ -502,20 +494,27 @@ export const createAvd = async ({
502
494
  }: CreateAvdOptions): Promise<void> => {
503
495
  const systemImagePackage = getAndroidSystemImagePackage(
504
496
  apiLevel,
505
- getHostAndroidSystemImageArch(),
497
+ getHostAndroidSystemImageArch()
506
498
  );
507
499
 
508
500
  await verifyAndroidEmulatorSdk(apiLevel);
509
501
  await ensureAvdProfileAvailable(profile);
510
502
  await spawn('bash', [
511
503
  '-lc',
512
- `printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}" -p "${getAvdDirectory(name)}"`,
504
+ `printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}" -p "${getAvdDirectory(
505
+ name
506
+ )}"`,
513
507
  ]);
514
508
  await ensureAvdIniExists({ name, apiLevel });
515
509
  const configPath = await ensureAvdConfigExists(name);
510
+ // hw.cpu.ncore is baked into config.ini (rather than passed as a `-cores`
511
+ // boot flag) so it's part of the AVD's persisted hardware profile: boot
512
+ // snapshots require an identical hardware config to load, so the vCPU
513
+ // count must be fixed at AVD-creation time and stay consistent across
514
+ // boots.
516
515
  await spawn('bash', [
517
516
  '-lc',
518
- `printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${configPath}"`,
517
+ `printf '%s\n%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' 'hw.cpu.ncore=${getEmulatorCpuCores()}' >> "${configPath}"`,
519
518
  ]);
520
519
  };
521
520
 
@@ -527,7 +526,7 @@ export const deleteAvd = async (name: string): Promise<void> => {
527
526
  {
528
527
  force: true,
529
528
  recursive: true,
530
- },
529
+ }
531
530
  );
532
531
  await rm(
533
532
  `${
@@ -535,18 +534,18 @@ export const deleteAvd = async (name: string): Promise<void> => {
535
534
  }/${name}.ini`,
536
535
  {
537
536
  force: true,
538
- },
537
+ }
539
538
  );
540
539
  };
541
540
 
542
541
  export const startEmulator = async (
543
542
  name: string,
544
- mode: EmulatorBootMode = 'default-boot',
543
+ mode: EmulatorBootMode = 'default-boot'
545
544
  ): Promise<void> => {
546
545
  const emulatorBinaryPath = await ensureEmulatorInstalled();
547
546
  const childProcess = emulatorProcess.startDetachedProcess(
548
547
  emulatorBinaryPath,
549
- getEmulatorStartupArgs(name, mode),
548
+ getEmulatorStartupArgs(name, mode)
550
549
  );
551
550
 
552
551
  let stdout = '';
@@ -582,7 +581,7 @@ export const startEmulator = async (
582
581
  stdout,
583
582
  stderr,
584
583
  error,
585
- }),
584
+ })
586
585
  );
587
586
  });
588
587
 
@@ -594,7 +593,7 @@ export const startEmulator = async (
594
593
  stderr,
595
594
  exitCode,
596
595
  signal,
597
- }),
596
+ })
598
597
  );
599
598
  });
600
599
  });
@@ -609,13 +608,19 @@ export const startEmulator = async (
609
608
  throw error;
610
609
  });
611
610
 
612
- const observationTimeout = wait(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS).then(
613
- () => 'timeout' as const,
611
+ const observationTimer = delay(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS);
612
+ const observationTimeout = observationTimer.promise.then(
613
+ () => 'timeout' as const
614
614
  );
615
615
 
616
616
  try {
617
617
  await Promise.race([earlyExit, observedBoot, observationTimeout]);
618
618
  } finally {
619
+ // If `earlyExit` or `observedBoot` won the race, this timer would
620
+ // otherwise sit ref'd in the event loop for the rest of its 5s;
621
+ // cancelling here is a no-op on the happy "proceed detached" path
622
+ // where the timer itself won.
623
+ observationTimer.cancel();
619
624
  cleanup();
620
625
  }
621
626
 
@@ -626,7 +631,7 @@ export const startEmulator = async (
626
631
 
627
632
  export const waitForEmulator = async (
628
633
  name: string,
629
- signal: AbortSignal,
634
+ signal: AbortSignal
630
635
  ): Promise<string> => {
631
636
  while (!signal.aborted) {
632
637
  const adbIds = await getDeviceIds();
@@ -651,7 +656,7 @@ export const waitForEmulator = async (
651
656
 
652
657
  export const waitForEmulatorDisconnect = async (
653
658
  adbId: string,
654
- signal: AbortSignal,
659
+ signal: AbortSignal
655
660
  ): Promise<void> => {
656
661
  while (!signal.aborted) {
657
662
  const adbIds = await getDeviceIds();
@@ -668,7 +673,7 @@ export const waitForEmulatorDisconnect = async (
668
673
 
669
674
  export const waitForBoot = async (
670
675
  name: string,
671
- signal: AbortSignal,
676
+ signal: AbortSignal
672
677
  ): Promise<string> => {
673
678
  while (!signal.aborted) {
674
679
  const adbIds = await getDeviceIds();
@@ -697,8 +702,15 @@ export const waitForBoot = async (
697
702
 
698
703
  export const isAppRunning = async (
699
704
  adbId: string,
700
- bundleId: string,
705
+ bundleId: string
701
706
  ): Promise<boolean> => {
707
+ return (await getAppPid(adbId, bundleId)) != null;
708
+ };
709
+
710
+ export const getAppPid = async (
711
+ adbId: string,
712
+ bundleId: string
713
+ ): Promise<number | null> => {
702
714
  try {
703
715
  const { stdout } = await spawn(getAdbBinaryPath(), [
704
716
  '-s',
@@ -707,10 +719,15 @@ export const isAppRunning = async (
707
719
  'pidof',
708
720
  bundleId,
709
721
  ]);
710
- return stdout.trim() !== '';
722
+ const pid = stdout
723
+ .trim()
724
+ .split(/\s+/)
725
+ .find((value) => value !== '');
726
+
727
+ return pid ? Number(pid) : null;
711
728
  } catch (error) {
712
729
  if (error instanceof SubprocessError && error.exitCode === 1) {
713
- return false;
730
+ return null;
714
731
  }
715
732
 
716
733
  throw error;
@@ -719,7 +736,7 @@ export const isAppRunning = async (
719
736
 
720
737
  export const getAppUid = async (
721
738
  adbId: string,
722
- bundleId: string,
739
+ bundleId: string
723
740
  ): Promise<number> => {
724
741
  const { stdout } = await spawn(getAdbBinaryPath(), [
725
742
  '-s',
@@ -744,7 +761,7 @@ export const getAppUid = async (
744
761
 
745
762
  export const setHideErrorDialogs = async (
746
763
  adbId: string,
747
- hide: boolean,
764
+ hide: boolean
748
765
  ): Promise<void> => {
749
766
  await spawn(getAdbBinaryPath(), [
750
767
  '-s',
@@ -773,12 +790,61 @@ export const getLogcatTimestamp = async (adbId: string): Promise<string> => {
773
790
  export const startLogcat = (
774
791
  adbId: string,
775
792
  args: readonly string[],
793
+ options?: { signal?: AbortSignal }
776
794
  ): Subprocess =>
777
795
  spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
778
796
  stdout: 'pipe',
779
797
  stderr: 'pipe',
798
+ signal: options?.signal,
780
799
  });
781
800
 
801
+ export const DROPBOX_CRASH_TAGS = [
802
+ 'data_app_crash',
803
+ 'data_app_native_crash',
804
+ ] as const;
805
+
806
+ export const getDropboxPrint = async (
807
+ adbId: string,
808
+ tags: readonly string[] = DROPBOX_CRASH_TAGS
809
+ ): Promise<string> => {
810
+ // Android treats multiple args after --print as one search string, so each
811
+ // tag must be queried separately and merged on the host.
812
+ const outputs = await Promise.all(
813
+ tags.map(async (tag) => {
814
+ const { stdout } = await spawn(getAdbBinaryPath(), [
815
+ '-s',
816
+ adbId,
817
+ 'shell',
818
+ 'dumpsys',
819
+ 'dropbox',
820
+ '--print',
821
+ tag,
822
+ ]);
823
+
824
+ return stdout;
825
+ })
826
+ );
827
+
828
+ return outputs.join('\n');
829
+ };
830
+
831
+ export const getActivityExitInfo = async (
832
+ adbId: string,
833
+ bundleId: string
834
+ ): Promise<string> => {
835
+ const { stdout } = await spawn(getAdbBinaryPath(), [
836
+ '-s',
837
+ adbId,
838
+ 'shell',
839
+ 'dumpsys',
840
+ 'activity',
841
+ 'exit-info',
842
+ bundleId,
843
+ ]);
844
+
845
+ return stdout;
846
+ };
847
+
782
848
  export const getAvds = async (): Promise<string[]> => {
783
849
  try {
784
850
  const emulatorBinaryPath = await ensureEmulatorInstalled();
@@ -832,7 +898,7 @@ export const getConnectedDevices = async (): Promise<AdbDevice[]> => {
832
898
 
833
899
  export const grantPermissions = async (
834
900
  adbId: string,
835
- bundleId: string,
901
+ bundleId: string
836
902
  ): Promise<void> => {
837
903
  androidAdbLogger.debug('grantPermissions:start %o', {
838
904
  adbId,
@@ -849,7 +915,7 @@ export const grantPermissions = async (
849
915
  getDangerousPermissions(adbId),
850
916
  ]);
851
917
  const permissions = requestedPermissions.filter((permission) =>
852
- dangerousPermissions.has(permission),
918
+ dangerousPermissions.has(permission)
853
919
  );
854
920
 
855
921
  androidAdbLogger.debug('grantPermissions:resolved %o', {
@@ -885,7 +951,7 @@ export const grantPermissions = async (
885
951
  });
886
952
 
887
953
  await Promise.all(
888
- grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[])),
954
+ grantCommands.map((args) => spawn(getAdbBinaryPath(), args as string[]))
889
955
  );
890
956
 
891
957
  androidAdbLogger.debug('grantPermissions:success %o', {