@react-native-harness/platform-android 1.1.0-rc.1 → 1.1.0-rc.3

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 (81) hide show
  1. package/README.md +9 -2
  2. package/dist/__tests__/adb.test.js +283 -10
  3. package/dist/__tests__/avd-config.test.d.ts +2 -0
  4. package/dist/__tests__/avd-config.test.d.ts.map +1 -0
  5. package/dist/__tests__/avd-config.test.js +174 -0
  6. package/dist/__tests__/ci-action.test.d.ts +2 -0
  7. package/dist/__tests__/ci-action.test.d.ts.map +1 -0
  8. package/dist/__tests__/ci-action.test.js +46 -0
  9. package/dist/__tests__/emulator-startup.test.d.ts +2 -0
  10. package/dist/__tests__/emulator-startup.test.d.ts.map +1 -0
  11. package/dist/__tests__/emulator-startup.test.js +19 -0
  12. package/dist/__tests__/environment.test.d.ts +2 -0
  13. package/dist/__tests__/environment.test.d.ts.map +1 -0
  14. package/dist/__tests__/environment.test.js +51 -0
  15. package/dist/__tests__/instance.test.d.ts +2 -0
  16. package/dist/__tests__/instance.test.d.ts.map +1 -0
  17. package/dist/__tests__/instance.test.js +423 -0
  18. package/dist/__tests__/shared-prefs.test.d.ts +2 -0
  19. package/dist/__tests__/shared-prefs.test.d.ts.map +1 -0
  20. package/dist/__tests__/shared-prefs.test.js +87 -0
  21. package/dist/adb.d.ts +23 -0
  22. package/dist/adb.d.ts.map +1 -1
  23. package/dist/adb.js +265 -16
  24. package/dist/app-monitor.d.ts.map +1 -1
  25. package/dist/app-monitor.js +29 -8
  26. package/dist/avd-config.d.ts +41 -0
  27. package/dist/avd-config.d.ts.map +1 -0
  28. package/dist/avd-config.js +173 -0
  29. package/dist/config.d.ts +77 -0
  30. package/dist/config.d.ts.map +1 -1
  31. package/dist/config.js +5 -0
  32. package/dist/emulator-startup.d.ts +3 -0
  33. package/dist/emulator-startup.d.ts.map +1 -0
  34. package/dist/emulator-startup.js +17 -0
  35. package/dist/environment.d.ts +28 -0
  36. package/dist/environment.d.ts.map +1 -0
  37. package/dist/environment.js +295 -0
  38. package/dist/errors.d.ts +4 -12
  39. package/dist/errors.d.ts.map +1 -1
  40. package/dist/errors.js +10 -24
  41. package/dist/factory.d.ts.map +1 -1
  42. package/dist/factory.js +3 -0
  43. package/dist/index.d.ts +3 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +3 -0
  46. package/dist/instance.d.ts +6 -0
  47. package/dist/instance.d.ts.map +1 -0
  48. package/dist/instance.js +234 -0
  49. package/dist/runner.d.ts +3 -3
  50. package/dist/runner.d.ts.map +1 -1
  51. package/dist/runner.js +12 -48
  52. package/dist/shared-prefs.d.ts +3 -0
  53. package/dist/shared-prefs.d.ts.map +1 -0
  54. package/dist/shared-prefs.js +92 -0
  55. package/dist/targets.d.ts +1 -1
  56. package/dist/targets.d.ts.map +1 -1
  57. package/dist/targets.js +2 -0
  58. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  59. package/package.json +4 -4
  60. package/src/__tests__/adb.test.ts +419 -15
  61. package/src/__tests__/avd-config.test.ts +206 -0
  62. package/src/__tests__/ci-action.test.ts +81 -0
  63. package/src/__tests__/emulator-startup.test.ts +32 -0
  64. package/src/__tests__/environment.test.ts +87 -0
  65. package/src/__tests__/instance.test.ts +610 -0
  66. package/src/__tests__/shared-prefs.test.ts +144 -0
  67. package/src/adb.ts +423 -16
  68. package/src/app-monitor.ts +58 -18
  69. package/src/avd-config.ts +290 -0
  70. package/src/config.ts +8 -0
  71. package/src/emulator-startup.ts +28 -0
  72. package/src/environment.ts +510 -0
  73. package/src/errors.ts +19 -0
  74. package/src/factory.ts +4 -0
  75. package/src/index.ts +7 -1
  76. package/src/instance.ts +380 -0
  77. package/src/runner.ts +25 -63
  78. package/src/shared-prefs.ts +205 -0
  79. package/src/targets.ts +11 -8
  80. package/tsconfig.json +2 -2
  81. package/tsconfig.lib.json +2 -2
@@ -6,12 +6,30 @@ import {
6
6
  type AppMonitorEvent,
7
7
  type AppMonitorListener,
8
8
  } from '@react-native-harness/platforms';
9
- import { escapeRegExp, getEmitter, logger, spawn, SubprocessError, type Subprocess } from '@react-native-harness/tools';
9
+ import {
10
+ escapeRegExp,
11
+ getEmitter,
12
+ logger,
13
+ spawn,
14
+ SubprocessError,
15
+ type Subprocess,
16
+ } from '@react-native-harness/tools';
10
17
  import * as adb from './adb.js';
11
18
  import { androidCrashParser } from './crash-parser.js';
12
19
 
20
+ const androidAppMonitorLogger = logger.child('android-app-monitor');
21
+
13
22
  const getLogcatArgs = (uid: number, fromTime: string) =>
14
- ['logcat', '-v', 'threadtime', '-b', 'crash', `--uid=${uid}`, '-T', fromTime] as const;
23
+ [
24
+ 'logcat',
25
+ '-v',
26
+ 'threadtime',
27
+ '-b',
28
+ 'crash',
29
+ `--uid=${uid}`,
30
+ '-T',
31
+ fromTime,
32
+ ] as const;
15
33
  const MAX_RECENT_LOG_LINES = 200;
16
34
  const MAX_RECENT_CRASH_ARTIFACTS = 10;
17
35
  const CRASH_ARTIFACT_SETTLE_DELAY_MS = 100;
@@ -27,7 +45,9 @@ const nativeCrashPattern = (bundleId: string) =>
27
45
 
28
46
  const processDiedPattern = (bundleId: string) =>
29
47
  new RegExp(
30
- `Process\\s+${escapeRegExp(bundleId)}\\s+\\(pid\\s+(\\d+)\\)\\s+has\\s+died`,
48
+ `Process\\s+${escapeRegExp(
49
+ bundleId
50
+ )}\\s+\\(pid\\s+(\\d+)\\)\\s+has\\s+died`,
31
51
  'i'
32
52
  );
33
53
 
@@ -64,7 +84,11 @@ const getAndroidLogLineCrashDetails = ({
64
84
  summary: line.trim(),
65
85
  signal: getSignal(line),
66
86
  exceptionType: fatalExceptionMatch?.[1]?.trim(),
67
- processName: processMatch ? bundleId : line.includes(bundleId) ? bundleId : undefined,
87
+ processName: processMatch
88
+ ? bundleId
89
+ : line.includes(bundleId)
90
+ ? bundleId
91
+ : undefined,
68
92
  pid: pid ?? (processMatch ? Number(processMatch[1]) : undefined),
69
93
  rawLines: [line],
70
94
  };
@@ -209,7 +233,9 @@ const createCrashArtifact = ({
209
233
  triggerOccurredAt,
210
234
  artifactType: 'logcat',
211
235
  rawLines:
212
- rawLines.length > 0 ? rawLines : parsedDetails.rawLines ?? details.rawLines,
236
+ rawLines.length > 0
237
+ ? rawLines
238
+ : parsedDetails.rawLines ?? details.rawLines,
213
239
  };
214
240
  };
215
241
 
@@ -263,11 +289,12 @@ const getLatestCrashArtifact = ({
263
289
  matchingByPid.length > 0
264
290
  ? matchingByPid
265
291
  : matchingByProcess.length > 0
266
- ? matchingByProcess
267
- : crashArtifacts;
292
+ ? matchingByProcess
293
+ : crashArtifacts;
268
294
  const sortedCandidates = [...candidates].sort(
269
295
  (left, right) =>
270
- Math.abs(left.occurredAt - occurredAt) - Math.abs(right.occurredAt - occurredAt)
296
+ Math.abs(left.occurredAt - occurredAt) -
297
+ Math.abs(right.occurredAt - occurredAt)
271
298
  );
272
299
 
273
300
  const artifact = sortedCandidates[0];
@@ -383,9 +410,10 @@ export const createAndroidAppMonitor = ({
383
410
  };
384
411
 
385
412
  const recordLogLine = (line: string) => {
386
- recentLogLines = [...recentLogLines, { line, occurredAt: Date.now() }].slice(
387
- -MAX_RECENT_LOG_LINES
388
- );
413
+ recentLogLines = [
414
+ ...recentLogLines,
415
+ { line, occurredAt: Date.now() },
416
+ ].slice(-MAX_RECENT_LOG_LINES);
389
417
  };
390
418
 
391
419
  const recordCrashArtifact = (details?: AppCrashDetails) => {
@@ -417,10 +445,14 @@ export const createAndroidAppMonitor = ({
417
445
  const startLogcat = async () => {
418
446
  const logcatTimestamp = await adb.getLogcatTimestamp(adbId);
419
447
 
420
- logcatProcess = spawn('adb', ['-s', adbId, ...getLogcatArgs(appUid, logcatTimestamp)], {
421
- stdout: 'pipe',
422
- stderr: 'pipe',
423
- });
448
+ logcatProcess = spawn(
449
+ 'adb',
450
+ ['-s', adbId, ...getLogcatArgs(appUid, logcatTimestamp)],
451
+ {
452
+ stdout: 'pipe',
453
+ stderr: 'pipe',
454
+ }
455
+ );
424
456
 
425
457
  const currentProcess = logcatProcess;
426
458
 
@@ -437,15 +469,23 @@ export const createAndroidAppMonitor = ({
437
469
  const event = createAndroidLogEvent(line, bundleId);
438
470
 
439
471
  if (event) {
440
- if (event.type === 'possible_crash' || event.type === 'app_exited') {
472
+ if (
473
+ event.type === 'possible_crash' ||
474
+ event.type === 'app_exited'
475
+ ) {
441
476
  recordCrashArtifact(event.crashDetails);
442
477
  }
443
478
  emit(event);
444
479
  }
445
480
  }
446
481
  } catch (error) {
447
- if (!(error instanceof SubprocessError && error.signalName === 'SIGTERM')) {
448
- logger.debug('Android logcat monitor stopped', error);
482
+ if (
483
+ !(error instanceof SubprocessError && error.signalName === 'SIGTERM')
484
+ ) {
485
+ androidAppMonitorLogger.debug(
486
+ 'Android logcat monitor stopped',
487
+ error
488
+ );
449
489
  }
450
490
  }
451
491
  })();
@@ -0,0 +1,290 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ import type { AndroidSystemImageArch } from './environment.js';
3
+ import type { AndroidEmulator, AndroidEmulatorAVDConfig } from './config.js';
4
+
5
+ export type AvdConfig = {
6
+ imageSysdir1?: string;
7
+ abiType?: string;
8
+ hwDeviceName?: string;
9
+ diskDataPartitionSize?: string;
10
+ vmHeapSize?: string;
11
+ };
12
+
13
+ export type AvdCompatibilityResult =
14
+ | { compatible: true }
15
+ | { compatible: false; reason: string };
16
+
17
+ export const getAvdDirectory = (name: string): string => {
18
+ return `${
19
+ process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`
20
+ }/${name}.avd`;
21
+ };
22
+
23
+ export const getAvdConfigPath = (name: string): string => {
24
+ return `${getAvdDirectory(name)}/config.ini`;
25
+ };
26
+
27
+ const normalizeAvdValue = (value: string | undefined): string | undefined => {
28
+ if (!value) {
29
+ return undefined;
30
+ }
31
+
32
+ return value.trim();
33
+ };
34
+
35
+ const normalizeConfigValue = (value: string): string => {
36
+ return value.trim().toLowerCase();
37
+ };
38
+
39
+ const parseSizeInBytes = (value: string | undefined): number | null => {
40
+ if (!value) {
41
+ return null;
42
+ }
43
+
44
+ const normalizedValue = value.trim().toLowerCase();
45
+
46
+ if (/^\d+$/.test(normalizedValue)) {
47
+ return Number(normalizedValue);
48
+ }
49
+
50
+ const match = normalizedValue.match(/^(\d+)([kmgt])$/i);
51
+
52
+ if (!match) {
53
+ return null;
54
+ }
55
+
56
+ const size = Number(match[1]);
57
+ const unit = match[2]?.toLowerCase();
58
+
59
+ const multiplier =
60
+ unit === 'k'
61
+ ? 1024
62
+ : unit === 'm'
63
+ ? 1024 ** 2
64
+ : unit === 'g'
65
+ ? 1024 ** 3
66
+ : unit === 't'
67
+ ? 1024 ** 4
68
+ : null;
69
+
70
+ return multiplier == null ? null : size * multiplier;
71
+ };
72
+
73
+ const getApiLevelFromImageSysdir = (
74
+ value: string | undefined
75
+ ): number | null => {
76
+ const match = value?.match(/android-(\d+)/i);
77
+ return match ? Number(match[1]) : null;
78
+ };
79
+
80
+ const normalizeProfile = (value: string | undefined): string | undefined => {
81
+ if (!value) {
82
+ return undefined;
83
+ }
84
+
85
+ return value
86
+ .trim()
87
+ .replace(/[\r\n]+/g, ' ')
88
+ .toLowerCase();
89
+ };
90
+
91
+ export const parseAvdConfig = (contents: string): AvdConfig => {
92
+ const config: AvdConfig = {};
93
+
94
+ for (const line of contents.split(/\r?\n/)) {
95
+ const trimmedLine = line.trim();
96
+
97
+ if (trimmedLine === '' || trimmedLine.startsWith('#')) {
98
+ continue;
99
+ }
100
+
101
+ const separatorIndex = trimmedLine.indexOf('=');
102
+
103
+ if (separatorIndex === -1) {
104
+ continue;
105
+ }
106
+
107
+ const key = trimmedLine.slice(0, separatorIndex).trim();
108
+ const value = trimmedLine.slice(separatorIndex + 1).trim();
109
+
110
+ switch (key) {
111
+ case 'image.sysdir.1':
112
+ config.imageSysdir1 = value;
113
+ break;
114
+ case 'abi.type':
115
+ config.abiType = value;
116
+ break;
117
+ case 'hw.device.name':
118
+ config.hwDeviceName = value;
119
+ break;
120
+ case 'disk.dataPartition.size':
121
+ config.diskDataPartitionSize = value;
122
+ break;
123
+ case 'vm.heapSize':
124
+ config.vmHeapSize = value;
125
+ break;
126
+ default:
127
+ break;
128
+ }
129
+ }
130
+
131
+ return config;
132
+ };
133
+
134
+ export const readAvdConfig = async (
135
+ name: string
136
+ ): Promise<AvdConfig | null> => {
137
+ const configPath = getAvdConfigPath(name);
138
+
139
+ try {
140
+ await access(configPath);
141
+ } catch {
142
+ return null;
143
+ }
144
+
145
+ return parseAvdConfig(await readFile(configPath, 'utf8'));
146
+ };
147
+
148
+ export const isAvdCompatible = ({
149
+ emulator,
150
+ avdConfig,
151
+ hostArch,
152
+ }: {
153
+ emulator: AndroidEmulator;
154
+ avdConfig: AvdConfig;
155
+ hostArch: AndroidSystemImageArch;
156
+ }): AvdCompatibilityResult => {
157
+ const requestedAvdConfig = emulator.avd;
158
+
159
+ if (!requestedAvdConfig) {
160
+ return { compatible: false, reason: 'AVD config is required.' };
161
+ }
162
+
163
+ if (emulator.name.trim() === '') {
164
+ return { compatible: false, reason: 'AVD name is required.' };
165
+ }
166
+
167
+ const apiLevel = getApiLevelFromImageSysdir(avdConfig.imageSysdir1);
168
+
169
+ if (apiLevel !== requestedAvdConfig.apiLevel) {
170
+ return {
171
+ compatible: false,
172
+ reason: `API level mismatch: expected ${
173
+ requestedAvdConfig.apiLevel
174
+ }, got ${apiLevel ?? 'missing'}.`,
175
+ };
176
+ }
177
+
178
+ if (normalizeAvdValue(avdConfig.abiType) !== hostArch) {
179
+ return {
180
+ compatible: false,
181
+ reason: `ABI mismatch: expected ${hostArch}, got ${
182
+ normalizeAvdValue(avdConfig.abiType) ?? 'missing'
183
+ }.`,
184
+ };
185
+ }
186
+
187
+ if (
188
+ normalizeProfile(avdConfig.hwDeviceName) !==
189
+ normalizeProfile(requestedAvdConfig.profile)
190
+ ) {
191
+ return {
192
+ compatible: false,
193
+ reason: `Profile mismatch: expected ${requestedAvdConfig.profile}, got ${
194
+ avdConfig.hwDeviceName ?? 'missing'
195
+ }.`,
196
+ };
197
+ }
198
+
199
+ if (
200
+ (() => {
201
+ const configuredDiskSizeBytes = parseSizeInBytes(
202
+ avdConfig.diskDataPartitionSize
203
+ );
204
+ const requestedDiskSizeBytes = parseSizeInBytes(
205
+ requestedAvdConfig.diskSize
206
+ );
207
+
208
+ if (configuredDiskSizeBytes != null && requestedDiskSizeBytes != null) {
209
+ return configuredDiskSizeBytes < requestedDiskSizeBytes;
210
+ }
211
+
212
+ return (
213
+ normalizeConfigValue(avdConfig.diskDataPartitionSize ?? '') !==
214
+ normalizeConfigValue(requestedAvdConfig.diskSize)
215
+ );
216
+ })()
217
+ ) {
218
+ return {
219
+ compatible: false,
220
+ reason: `Disk size mismatch: expected ${
221
+ requestedAvdConfig.diskSize
222
+ }, got ${avdConfig.diskDataPartitionSize ?? 'missing'}.`,
223
+ };
224
+ }
225
+
226
+ if (
227
+ normalizeConfigValue(avdConfig.vmHeapSize ?? '') !==
228
+ normalizeConfigValue(requestedAvdConfig.heapSize)
229
+ ) {
230
+ return {
231
+ compatible: false,
232
+ reason: `Heap size mismatch: expected ${
233
+ requestedAvdConfig.heapSize
234
+ }, got ${avdConfig.vmHeapSize ?? 'missing'}.`,
235
+ };
236
+ }
237
+
238
+ return { compatible: true };
239
+ };
240
+
241
+ export const getNormalizedAvdCacheConfig = ({
242
+ emulator,
243
+ hostArch,
244
+ }: {
245
+ emulator: AndroidEmulator;
246
+ hostArch: AndroidSystemImageArch;
247
+ }): {
248
+ name: string;
249
+ apiLevel: number;
250
+ arch: AndroidSystemImageArch;
251
+ profile: string;
252
+ diskSize: string;
253
+ heapSize: string;
254
+ } | null => {
255
+ const avd = emulator.avd;
256
+
257
+ if (!avd) {
258
+ return null;
259
+ }
260
+
261
+ return {
262
+ name: emulator.name,
263
+ apiLevel: avd.apiLevel,
264
+ arch: hostArch,
265
+ profile: avd.profile.trim().toLowerCase(),
266
+ diskSize: avd.diskSize.trim().toLowerCase(),
267
+ heapSize: avd.heapSize.trim().toLowerCase(),
268
+ };
269
+ };
270
+
271
+ export const resolveAvdCachingEnabled = ({
272
+ avd,
273
+ isInteractive,
274
+ env = process.env,
275
+ }: {
276
+ avd?: AndroidEmulatorAVDConfig;
277
+ isInteractive: boolean;
278
+ env?: NodeJS.ProcessEnv;
279
+ }): boolean => {
280
+ const override = env.HARNESS_AVD_CACHING;
281
+ const configValue = avd?.snapshot?.enabled;
282
+ const requestedValue =
283
+ override == null ? configValue : override.toLowerCase() === 'true';
284
+
285
+ if (!requestedValue) {
286
+ return false;
287
+ }
288
+
289
+ return !isInteractive;
290
+ };
package/src/config.ts CHANGED
@@ -11,6 +11,11 @@ export const AndroidEmulatorAVDConfigSchema = z.object({
11
11
  profile: z.string().min(1, 'Profile is required'),
12
12
  diskSize: z.string().min(1, 'Disk size is required').default('1G'),
13
13
  heapSize: z.string().min(1, 'Heap size is required').default('1G'),
14
+ snapshot: z
15
+ .object({
16
+ enabled: z.boolean().optional(),
17
+ })
18
+ .optional(),
14
19
  });
15
20
 
16
21
  export const AndroidEmulatorSchema = z.object({
@@ -51,6 +56,9 @@ export type AndroidAppLaunchOptions = z.infer<
51
56
  export type AndroidEmulatorAVDConfig = z.infer<
52
57
  typeof AndroidEmulatorAVDConfigSchema
53
58
  >;
59
+ export type AndroidEmulatorAVDSnapshotConfig = NonNullable<
60
+ AndroidEmulatorAVDConfig['snapshot']
61
+ >;
54
62
 
55
63
  export const isAndroidDeviceEmulator = (
56
64
  device: AndroidDevice
@@ -0,0 +1,28 @@
1
+ export type EmulatorBootMode =
2
+ | 'default-boot'
3
+ | 'clean-snapshot-generation'
4
+ | 'snapshot-reuse';
5
+
6
+ const COMMON_EMULATOR_ARGS = [
7
+ '-no-window',
8
+ '-gpu',
9
+ 'swiftshader_indirect',
10
+ '-noaudio',
11
+ '-no-boot-anim',
12
+ '-camera-back',
13
+ 'none',
14
+ ] as const;
15
+
16
+ export const getEmulatorStartupArgs = (
17
+ name: string,
18
+ mode: EmulatorBootMode
19
+ ): string[] => {
20
+ const modeArgs =
21
+ mode === 'clean-snapshot-generation'
22
+ ? ['-no-snapshot-load']
23
+ : mode === 'snapshot-reuse'
24
+ ? ['-no-snapshot-save']
25
+ : ['-no-snapshot-load', '-no-snapshot-save'];
26
+
27
+ return [`@${name}`, ...modeArgs, ...COMMON_EMULATOR_ARGS];
28
+ };