@react-native-harness/platform-android 1.1.0-rc.2 → 1.1.0-rc.4

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 (87) 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 +117 -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__/targets.test.d.ts +2 -0
  19. package/dist/__tests__/targets.test.d.ts.map +1 -0
  20. package/dist/__tests__/targets.test.js +49 -0
  21. package/dist/adb.d.ts +23 -0
  22. package/dist/adb.d.ts.map +1 -1
  23. package/dist/adb.js +259 -16
  24. package/dist/app-monitor.d.ts.map +1 -1
  25. package/dist/app-monitor.js +27 -7
  26. package/dist/assertions.d.ts +5 -0
  27. package/dist/assertions.d.ts.map +1 -0
  28. package/dist/assertions.js +6 -0
  29. package/dist/avd-config.d.ts +41 -0
  30. package/dist/avd-config.d.ts.map +1 -0
  31. package/dist/avd-config.js +173 -0
  32. package/dist/config.d.ts +77 -0
  33. package/dist/config.d.ts.map +1 -1
  34. package/dist/config.js +5 -0
  35. package/dist/emulator-startup.d.ts +3 -0
  36. package/dist/emulator-startup.d.ts.map +1 -0
  37. package/dist/emulator-startup.js +17 -0
  38. package/dist/emulator.d.ts +6 -0
  39. package/dist/emulator.d.ts.map +1 -0
  40. package/dist/emulator.js +27 -0
  41. package/dist/environment.d.ts +31 -0
  42. package/dist/environment.d.ts.map +1 -0
  43. package/dist/environment.js +317 -0
  44. package/dist/errors.d.ts +7 -0
  45. package/dist/errors.d.ts.map +1 -0
  46. package/dist/errors.js +14 -0
  47. package/dist/factory.d.ts.map +1 -1
  48. package/dist/factory.js +3 -0
  49. package/dist/index.d.ts +3 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +3 -0
  52. package/dist/instance.d.ts +6 -0
  53. package/dist/instance.d.ts.map +1 -0
  54. package/dist/instance.js +232 -0
  55. package/dist/reader.d.ts +6 -0
  56. package/dist/reader.d.ts.map +1 -0
  57. package/dist/reader.js +57 -0
  58. package/dist/runner.d.ts +2 -2
  59. package/dist/runner.d.ts.map +1 -1
  60. package/dist/runner.js +9 -52
  61. package/dist/targets.d.ts +1 -1
  62. package/dist/targets.d.ts.map +1 -1
  63. package/dist/targets.js +4 -0
  64. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  65. package/dist/types.d.ts +381 -0
  66. package/dist/types.d.ts.map +1 -0
  67. package/dist/types.js +107 -0
  68. package/package.json +4 -4
  69. package/src/__tests__/adb.test.ts +419 -15
  70. package/src/__tests__/avd-config.test.ts +206 -0
  71. package/src/__tests__/ci-action.test.ts +81 -0
  72. package/src/__tests__/emulator-startup.test.ts +32 -0
  73. package/src/__tests__/environment.test.ts +212 -0
  74. package/src/__tests__/instance.test.ts +610 -0
  75. package/src/__tests__/targets.test.ts +53 -0
  76. package/src/adb.ts +430 -28
  77. package/src/app-monitor.ts +56 -18
  78. package/src/avd-config.ts +290 -0
  79. package/src/config.ts +8 -0
  80. package/src/emulator-startup.ts +28 -0
  81. package/src/environment.ts +554 -0
  82. package/src/errors.ts +19 -0
  83. package/src/factory.ts +4 -0
  84. package/src/index.ts +7 -1
  85. package/src/instance.ts +380 -0
  86. package/src/runner.ts +19 -70
  87. package/src/targets.ts +18 -8
package/dist/adb.js CHANGED
@@ -1,4 +1,84 @@
1
1
  import { spawn, SubprocessError } from '@react-native-harness/tools';
2
+ import { spawn as nodeSpawn } from 'node:child_process';
3
+ import { access, rm } from 'node:fs/promises';
4
+ import { ensureAndroidEmulatorAvailable, ensureAndroidSdkPackages, getAdbBinaryPath, getAndroidSystemImagePackage, getAvdManagerBinaryPath, getEmulatorBinaryPath, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, } from './environment.js';
5
+ import { getEmulatorStartupArgs, } from './emulator-startup.js';
6
+ const wait = async (ms) => {
7
+ await new Promise((resolve) => {
8
+ setTimeout(resolve, ms);
9
+ });
10
+ };
11
+ const waitForAbort = (signal) => {
12
+ if (signal.aborted) {
13
+ return Promise.reject(signal.reason);
14
+ }
15
+ return new Promise((_, reject) => {
16
+ signal.addEventListener('abort', () => {
17
+ reject(signal.reason);
18
+ }, { once: true });
19
+ });
20
+ };
21
+ const waitWithSignal = async (ms, signal) => {
22
+ if (signal.aborted) {
23
+ throw signal.reason;
24
+ }
25
+ await Promise.race([wait(ms), waitForAbort(signal)]);
26
+ };
27
+ const getAvdConfigPath = (name) => `${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd/config.ini`;
28
+ const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
29
+ const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
30
+ export const emulatorProcess = {
31
+ startDetachedProcess: (file, args) => nodeSpawn(file, args, {
32
+ detached: true,
33
+ stdio: ['ignore', 'pipe', 'pipe'],
34
+ }),
35
+ };
36
+ const appendBoundedOutput = (output, chunk, limit = EMULATOR_OUTPUT_BUFFER_LIMIT) => {
37
+ const nextOutput = output + chunk;
38
+ if (nextOutput.length <= limit) {
39
+ return nextOutput;
40
+ }
41
+ return nextOutput.slice(-limit);
42
+ };
43
+ const formatEmulatorStartupError = ({ name, stdout, stderr, exitCode, signal, error, }) => {
44
+ const sections = [`Failed to start Android emulator @${name}.`];
45
+ if (typeof exitCode === 'number') {
46
+ sections.push(`Exit code: ${exitCode}`);
47
+ }
48
+ if (signal) {
49
+ sections.push(`Signal: ${signal}`);
50
+ }
51
+ if (error instanceof Error) {
52
+ sections.push(`Cause: ${error.message}`);
53
+ }
54
+ const trimmedStdout = stdout.trim();
55
+ const trimmedStderr = stderr.trim();
56
+ if (trimmedStdout !== '') {
57
+ sections.push(`stdout:\n${trimmedStdout}`);
58
+ }
59
+ if (trimmedStderr !== '') {
60
+ sections.push(`stderr:\n${trimmedStderr}`);
61
+ }
62
+ return new Error(sections.join('\n\n'), {
63
+ cause: error instanceof Error ? error : undefined,
64
+ });
65
+ };
66
+ const ensureEmulatorInstalled = async () => {
67
+ await ensureAndroidEmulatorAvailable();
68
+ const emulatorBinaryPath = getEmulatorBinaryPath();
69
+ await access(emulatorBinaryPath);
70
+ return emulatorBinaryPath;
71
+ };
72
+ export const getRequiredEmulatorPackages = (apiLevel) => {
73
+ return getRequiredAndroidSdkPackages({
74
+ apiLevel,
75
+ includeEmulator: true,
76
+ architecture: getHostAndroidSystemImageArch(),
77
+ });
78
+ };
79
+ export const verifyAndroidEmulatorSdk = async (apiLevel) => {
80
+ await ensureAndroidSdkPackages(getRequiredEmulatorPackages(apiLevel));
81
+ };
2
82
  export const getStartAppArgs = (bundleId, activityName, options) => {
3
83
  const args = [
4
84
  'shell',
@@ -29,7 +109,7 @@ export const getStartAppArgs = (bundleId, activityName, options) => {
29
109
  return args;
30
110
  };
31
111
  export const isAppInstalled = async (adbId, bundleId) => {
32
- const { stdout } = await spawn('adb', [
112
+ const { stdout } = await spawn(getAdbBinaryPath(), [
33
113
  '-s',
34
114
  adbId,
35
115
  'shell',
@@ -41,7 +121,7 @@ export const isAppInstalled = async (adbId, bundleId) => {
41
121
  return stdout.trim() !== '';
42
122
  };
43
123
  export const reversePort = async (adbId, port, hostPort = port) => {
44
- await spawn('adb', [
124
+ await spawn(getAdbBinaryPath(), [
45
125
  '-s',
46
126
  adbId,
47
127
  'reverse',
@@ -50,13 +130,24 @@ export const reversePort = async (adbId, port, hostPort = port) => {
50
130
  ]);
51
131
  };
52
132
  export const stopApp = async (adbId, bundleId) => {
53
- await spawn('adb', ['-s', adbId, 'shell', 'am', 'force-stop', bundleId]);
133
+ await spawn(getAdbBinaryPath(), [
134
+ '-s',
135
+ adbId,
136
+ 'shell',
137
+ 'am',
138
+ 'force-stop',
139
+ bundleId,
140
+ ]);
54
141
  };
55
142
  export const startApp = async (adbId, bundleId, activityName, options) => {
56
- await spawn('adb', ['-s', adbId, ...getStartAppArgs(bundleId, activityName, options)]);
143
+ await spawn(getAdbBinaryPath(), [
144
+ '-s',
145
+ adbId,
146
+ ...getStartAppArgs(bundleId, activityName, options),
147
+ ]);
57
148
  };
58
149
  export const getDeviceIds = async () => {
59
- const { stdout } = await spawn('adb', ['devices']);
150
+ const { stdout } = await spawn(getAdbBinaryPath(), ['devices']);
60
151
  return stdout
61
152
  .split('\n')
62
153
  .slice(1) // Skip header
@@ -64,11 +155,17 @@ export const getDeviceIds = async () => {
64
155
  .map((line) => line.split('\t')[0]);
65
156
  };
66
157
  export const getEmulatorName = async (adbId) => {
67
- const { stdout } = await spawn('adb', ['-s', adbId, 'emu', 'avd', 'name']);
158
+ const { stdout } = await spawn(getAdbBinaryPath(), [
159
+ '-s',
160
+ adbId,
161
+ 'emu',
162
+ 'avd',
163
+ 'name',
164
+ ]);
68
165
  return stdout.split('\n')[0].trim() || null;
69
166
  };
70
167
  export const getShellProperty = async (adbId, property) => {
71
- const { stdout } = await spawn('adb', [
168
+ const { stdout } = await spawn(getAdbBinaryPath(), [
72
169
  '-s',
73
170
  adbId,
74
171
  'shell',
@@ -77,21 +174,166 @@ export const getShellProperty = async (adbId, property) => {
77
174
  ]);
78
175
  return stdout.trim() || null;
79
176
  };
177
+ const isTransientAdbShellFailure = (error) => {
178
+ return error instanceof SubprocessError && error.exitCode === 1;
179
+ };
80
180
  export const getDeviceInfo = async (adbId) => {
81
181
  const manufacturer = await getShellProperty(adbId, 'ro.product.manufacturer');
82
182
  const model = await getShellProperty(adbId, 'ro.product.model');
83
183
  return { manufacturer, model };
84
184
  };
85
185
  export const isBootCompleted = async (adbId) => {
86
- const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
87
- return bootCompleted === '1';
186
+ try {
187
+ const bootCompleted = await getShellProperty(adbId, 'sys.boot_completed');
188
+ return bootCompleted === '1';
189
+ }
190
+ catch (error) {
191
+ if (isTransientAdbShellFailure(error)) {
192
+ return false;
193
+ }
194
+ throw error;
195
+ }
88
196
  };
89
197
  export const stopEmulator = async (adbId) => {
90
- await spawn('adb', ['-s', adbId, 'emu', 'kill']);
198
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'emu', 'kill']);
199
+ };
200
+ export const installApp = async (adbId, appPath) => {
201
+ await spawn(getAdbBinaryPath(), ['-s', adbId, 'install', '-r', appPath]);
202
+ };
203
+ export const hasAvd = async (name) => {
204
+ const avds = await getAvds();
205
+ return avds.includes(name);
206
+ };
207
+ export const createAvd = async ({ name, apiLevel, profile, diskSize, heapSize, }) => {
208
+ const systemImagePackage = getAndroidSystemImagePackage(apiLevel, getHostAndroidSystemImageArch());
209
+ await verifyAndroidEmulatorSdk(apiLevel);
210
+ await spawn('bash', [
211
+ '-lc',
212
+ `printf 'no\n' | "${getAvdManagerBinaryPath()}" create avd --force --name "${name}" --package "${systemImagePackage}" --device "${profile}"`,
213
+ ]);
214
+ await spawn('bash', [
215
+ '-lc',
216
+ `printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${getAvdConfigPath(name)}"`,
217
+ ]);
218
+ };
219
+ export const deleteAvd = async (name) => {
220
+ await rm(`${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd`, {
221
+ force: true,
222
+ recursive: true,
223
+ });
224
+ await rm(`${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.ini`, {
225
+ force: true,
226
+ });
227
+ };
228
+ export const startEmulator = async (name, mode = 'default-boot') => {
229
+ const emulatorBinaryPath = await ensureEmulatorInstalled();
230
+ const childProcess = emulatorProcess.startDetachedProcess(emulatorBinaryPath, getEmulatorStartupArgs(name, mode));
231
+ let stdout = '';
232
+ let stderr = '';
233
+ childProcess.stdout?.setEncoding('utf8');
234
+ childProcess.stderr?.setEncoding('utf8');
235
+ const onStdout = (chunk) => {
236
+ stdout = appendBoundedOutput(stdout, chunk.toString());
237
+ };
238
+ const onStderr = (chunk) => {
239
+ stderr = appendBoundedOutput(stderr, chunk.toString());
240
+ };
241
+ childProcess.stdout?.on('data', onStdout);
242
+ childProcess.stderr?.on('data', onStderr);
243
+ const startupAbortController = new AbortController();
244
+ const cleanup = () => {
245
+ startupAbortController.abort();
246
+ childProcess.stdout?.off('data', onStdout);
247
+ childProcess.stderr?.off('data', onStderr);
248
+ childProcess.removeAllListeners('error');
249
+ childProcess.removeAllListeners('close');
250
+ };
251
+ const earlyExit = new Promise((_, reject) => {
252
+ childProcess.once('error', (error) => {
253
+ reject(formatEmulatorStartupError({
254
+ name,
255
+ stdout,
256
+ stderr,
257
+ error,
258
+ }));
259
+ });
260
+ childProcess.once('close', (exitCode, signal) => {
261
+ reject(formatEmulatorStartupError({
262
+ name,
263
+ stdout,
264
+ stderr,
265
+ exitCode,
266
+ signal,
267
+ }));
268
+ });
269
+ });
270
+ const observedBoot = waitForEmulator(name, startupAbortController.signal)
271
+ .then(() => 'booted')
272
+ .catch((error) => {
273
+ if (startupAbortController.signal.aborted) {
274
+ return 'aborted';
275
+ }
276
+ throw error;
277
+ });
278
+ const observationTimeout = wait(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS).then(() => 'timeout');
279
+ try {
280
+ await Promise.race([earlyExit, observedBoot, observationTimeout]);
281
+ }
282
+ finally {
283
+ cleanup();
284
+ }
285
+ childProcess.stdout?.destroy();
286
+ childProcess.stderr?.destroy();
287
+ childProcess.unref();
288
+ };
289
+ export const waitForEmulator = async (name, signal) => {
290
+ while (!signal.aborted) {
291
+ const adbIds = await getDeviceIds();
292
+ for (const adbId of adbIds) {
293
+ if (!adbId.startsWith('emulator-')) {
294
+ continue;
295
+ }
296
+ const emulatorName = await getEmulatorName(adbId);
297
+ if (emulatorName === name) {
298
+ return adbId;
299
+ }
300
+ }
301
+ await waitWithSignal(1000, signal);
302
+ }
303
+ throw signal.reason;
304
+ };
305
+ export const waitForEmulatorDisconnect = async (adbId, signal) => {
306
+ while (!signal.aborted) {
307
+ const adbIds = await getDeviceIds();
308
+ if (!adbIds.includes(adbId)) {
309
+ return;
310
+ }
311
+ await waitWithSignal(1000, signal);
312
+ }
313
+ throw signal.reason;
314
+ };
315
+ export const waitForBoot = async (name, signal) => {
316
+ while (!signal.aborted) {
317
+ const adbIds = await getDeviceIds();
318
+ for (const adbId of adbIds) {
319
+ if (!adbId.startsWith('emulator-')) {
320
+ continue;
321
+ }
322
+ const emulatorName = await getEmulatorName(adbId);
323
+ if (emulatorName !== name) {
324
+ continue;
325
+ }
326
+ if (await isBootCompleted(adbId)) {
327
+ return adbId;
328
+ }
329
+ }
330
+ await waitWithSignal(1000, signal);
331
+ }
332
+ throw signal.reason;
91
333
  };
92
334
  export const isAppRunning = async (adbId, bundleId) => {
93
335
  try {
94
- const { stdout } = await spawn('adb', [
336
+ const { stdout } = await spawn(getAdbBinaryPath(), [
95
337
  '-s',
96
338
  adbId,
97
339
  'shell',
@@ -108,7 +350,7 @@ export const isAppRunning = async (adbId, bundleId) => {
108
350
  }
109
351
  };
110
352
  export const getAppUid = async (adbId, bundleId) => {
111
- const { stdout } = await spawn('adb', [
353
+ const { stdout } = await spawn(getAdbBinaryPath(), [
112
354
  '-s',
113
355
  adbId,
114
356
  'shell',
@@ -127,7 +369,7 @@ export const getAppUid = async (adbId, bundleId) => {
127
369
  return Number(match[1]);
128
370
  };
129
371
  export const setHideErrorDialogs = async (adbId, hide) => {
130
- await spawn('adb', [
372
+ await spawn(getAdbBinaryPath(), [
131
373
  '-s',
132
374
  adbId,
133
375
  'shell',
@@ -139,7 +381,7 @@ export const setHideErrorDialogs = async (adbId, hide) => {
139
381
  ]);
140
382
  };
141
383
  export const getLogcatTimestamp = async (adbId) => {
142
- const { stdout } = await spawn('adb', [
384
+ const { stdout } = await spawn(getAdbBinaryPath(), [
143
385
  '-s',
144
386
  adbId,
145
387
  'shell',
@@ -150,7 +392,8 @@ export const getLogcatTimestamp = async (adbId) => {
150
392
  };
151
393
  export const getAvds = async () => {
152
394
  try {
153
- const { stdout } = await spawn('emulator', ['-list-avds']);
395
+ const emulatorBinaryPath = await ensureEmulatorInstalled();
396
+ const { stdout } = await spawn(emulatorBinaryPath, ['-list-avds']);
154
397
  return stdout
155
398
  .split('\n')
156
399
  .map((line) => line.trim())
@@ -161,7 +404,7 @@ export const getAvds = async () => {
161
404
  }
162
405
  };
163
406
  export const getConnectedDevices = async () => {
164
- const { stdout } = await spawn('adb', ['devices', '-l']);
407
+ const { stdout } = await spawn(getAdbBinaryPath(), ['devices', '-l']);
165
408
  const lines = stdout.split('\n').slice(1);
166
409
  const devices = [];
167
410
  for (const line of lines) {
@@ -1 +1 @@
1
- {"version":3,"file":"app-monitor.d.ts","sourceRoot":"","sources":["../src/app-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,EAErB,MAAM,iCAAiC,CAAC;AAuRzC,QAAA,MAAM,qBAAqB,GACzB,MAAM,MAAM,EACZ,UAAU,MAAM,KACf,eAAe,GAAG,IAwEpB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,mDAKrC;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,KAAG,iBAmKH,CAAC;AAEF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AACjC,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG;IAC3C,eAAe,EAAE,CACf,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CACtC,CAAC"}
1
+ {"version":3,"file":"app-monitor.d.ts","sourceRoot":"","sources":["../src/app-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,UAAU,EACf,KAAK,eAAe,EACpB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,EAC9B,KAAK,eAAe,EAErB,MAAM,iCAAiC,CAAC;AAgTzC,QAAA,MAAM,qBAAqB,GACzB,MAAM,MAAM,EACZ,UAAU,MAAM,KACf,eAAe,GAAG,IAwEpB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,mDAKrC;IACD,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;CAC3C,KAAG,iBAgLH,CAAC;AAEF,OAAO,EAAE,qBAAqB,EAAE,CAAC;AACjC,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG;IAC3C,eAAe,EAAE,CACf,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAAC;CACtC,CAAC"}
@@ -1,8 +1,17 @@
1
- import { escapeRegExp, getEmitter, logger, spawn, SubprocessError } from '@react-native-harness/tools';
1
+ import { escapeRegExp, getEmitter, logger, spawn, SubprocessError, } from '@react-native-harness/tools';
2
2
  import * as adb from './adb.js';
3
3
  import { androidCrashParser } from './crash-parser.js';
4
4
  const androidAppMonitorLogger = logger.child('android-app-monitor');
5
- const getLogcatArgs = (uid, fromTime) => ['logcat', '-v', 'threadtime', '-b', 'crash', `--uid=${uid}`, '-T', fromTime];
5
+ const getLogcatArgs = (uid, fromTime) => [
6
+ 'logcat',
7
+ '-v',
8
+ 'threadtime',
9
+ '-b',
10
+ 'crash',
11
+ `--uid=${uid}`,
12
+ '-T',
13
+ fromTime,
14
+ ];
6
15
  const MAX_RECENT_LOG_LINES = 200;
7
16
  const MAX_RECENT_CRASH_ARTIFACTS = 10;
8
17
  const CRASH_ARTIFACT_SETTLE_DELAY_MS = 100;
@@ -29,7 +38,11 @@ const getAndroidLogLineCrashDetails = ({ line, bundleId, pid, }) => {
29
38
  summary: line.trim(),
30
39
  signal: getSignal(line),
31
40
  exceptionType: fatalExceptionMatch?.[1]?.trim(),
32
- processName: processMatch ? bundleId : line.includes(bundleId) ? bundleId : undefined,
41
+ processName: processMatch
42
+ ? bundleId
43
+ : line.includes(bundleId)
44
+ ? bundleId
45
+ : undefined,
33
46
  pid: pid ?? (processMatch ? Number(processMatch[1]) : undefined),
34
47
  rawLines: [line],
35
48
  };
@@ -116,7 +129,9 @@ const createCrashArtifact = ({ details, recentLogLines, }) => {
116
129
  triggerLine: details.summary ?? '',
117
130
  triggerOccurredAt,
118
131
  artifactType: 'logcat',
119
- rawLines: rawLines.length > 0 ? rawLines : parsedDetails.rawLines ?? details.rawLines,
132
+ rawLines: rawLines.length > 0
133
+ ? rawLines
134
+ : parsedDetails.rawLines ?? details.rawLines,
120
135
  };
121
136
  };
122
137
  const persistCrashArtifact = ({ details, crashArtifactWriter, }) => {
@@ -151,7 +166,8 @@ const getLatestCrashArtifact = ({ crashArtifacts, recentLogLines, processName, p
151
166
  : matchingByProcess.length > 0
152
167
  ? matchingByProcess
153
168
  : crashArtifacts;
154
- const sortedCandidates = [...candidates].sort((left, right) => Math.abs(left.occurredAt - occurredAt) - Math.abs(right.occurredAt - occurredAt));
169
+ const sortedCandidates = [...candidates].sort((left, right) => Math.abs(left.occurredAt - occurredAt) -
170
+ Math.abs(right.occurredAt - occurredAt));
155
171
  const artifact = sortedCandidates[0];
156
172
  if (!artifact) {
157
173
  return null;
@@ -235,7 +251,10 @@ export const createAndroidAppMonitor = ({ adbId, bundleId, appUid, crashArtifact
235
251
  emitter.emit(event);
236
252
  };
237
253
  const recordLogLine = (line) => {
238
- recentLogLines = [...recentLogLines, { line, occurredAt: Date.now() }].slice(-MAX_RECENT_LOG_LINES);
254
+ recentLogLines = [
255
+ ...recentLogLines,
256
+ { line, occurredAt: Date.now() },
257
+ ].slice(-MAX_RECENT_LOG_LINES);
239
258
  };
240
259
  const recordCrashArtifact = (details) => {
241
260
  if (!details) {
@@ -277,7 +296,8 @@ export const createAndroidAppMonitor = ({ adbId, bundleId, appUid, crashArtifact
277
296
  emit({ type: 'log', source: 'logs', line });
278
297
  const event = createAndroidLogEvent(line, bundleId);
279
298
  if (event) {
280
- if (event.type === 'possible_crash' || event.type === 'app_exited') {
299
+ if (event.type === 'possible_crash' ||
300
+ event.type === 'app_exited') {
281
301
  recordCrashArtifact(event.crashDetails);
282
302
  }
283
303
  emit(event);
@@ -0,0 +1,5 @@
1
+ import { Config, NativeTestRunnerConfig } from './types.js';
2
+ export declare function assertNativeRunner(config: Config): asserts config is Config & {
3
+ runner: NativeTestRunnerConfig;
4
+ };
5
+ //# sourceMappingURL=assertions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"assertions.d.ts","sourceRoot":"","sources":["../src/assertions.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAC;AAE5D,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,IAAI,MAAM,GAAG;IAAE,MAAM,EAAE,sBAAsB,CAAA;CAAE,CAO/D"}
@@ -0,0 +1,6 @@
1
+ export function assertNativeRunner(config) {
2
+ if (config.runner.platform !== 'ios' &&
3
+ config.runner.platform !== 'android') {
4
+ throw new Error('Runner is not a native runner');
5
+ }
6
+ }
@@ -0,0 +1,41 @@
1
+ import type { AndroidSystemImageArch } from './environment.js';
2
+ import type { AndroidEmulator, AndroidEmulatorAVDConfig } from './config.js';
3
+ export type AvdConfig = {
4
+ imageSysdir1?: string;
5
+ abiType?: string;
6
+ hwDeviceName?: string;
7
+ diskDataPartitionSize?: string;
8
+ vmHeapSize?: string;
9
+ };
10
+ export type AvdCompatibilityResult = {
11
+ compatible: true;
12
+ } | {
13
+ compatible: false;
14
+ reason: string;
15
+ };
16
+ export declare const getAvdDirectory: (name: string) => string;
17
+ export declare const getAvdConfigPath: (name: string) => string;
18
+ export declare const parseAvdConfig: (contents: string) => AvdConfig;
19
+ export declare const readAvdConfig: (name: string) => Promise<AvdConfig | null>;
20
+ export declare const isAvdCompatible: ({ emulator, avdConfig, hostArch, }: {
21
+ emulator: AndroidEmulator;
22
+ avdConfig: AvdConfig;
23
+ hostArch: AndroidSystemImageArch;
24
+ }) => AvdCompatibilityResult;
25
+ export declare const getNormalizedAvdCacheConfig: ({ emulator, hostArch, }: {
26
+ emulator: AndroidEmulator;
27
+ hostArch: AndroidSystemImageArch;
28
+ }) => {
29
+ name: string;
30
+ apiLevel: number;
31
+ arch: AndroidSystemImageArch;
32
+ profile: string;
33
+ diskSize: string;
34
+ heapSize: string;
35
+ } | null;
36
+ export declare const resolveAvdCachingEnabled: ({ avd, isInteractive, env, }: {
37
+ avd?: AndroidEmulatorAVDConfig;
38
+ isInteractive: boolean;
39
+ env?: NodeJS.ProcessEnv;
40
+ }) => boolean;
41
+ //# sourceMappingURL=avd-config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"avd-config.d.ts","sourceRoot":"","sources":["../src/avd-config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,wBAAwB,EAAE,MAAM,aAAa,CAAC;AAE7E,MAAM,MAAM,SAAS,GAAG;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAC9B;IAAE,UAAU,EAAE,IAAI,CAAA;CAAE,GACpB;IAAE,UAAU,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MAI9C,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAE/C,CAAC;AAkEF,eAAO,MAAM,cAAc,GAAI,UAAU,MAAM,KAAG,SAyCjD,CAAC;AAEF,eAAO,MAAM,aAAa,GACxB,MAAM,MAAM,KACX,OAAO,CAAC,SAAS,GAAG,IAAI,CAU1B,CAAC;AAEF,eAAO,MAAM,eAAe,GAAI,oCAI7B;IACD,QAAQ,EAAE,eAAe,CAAC;IAC1B,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG,sBAmFH,CAAC;AAEF,eAAO,MAAM,2BAA2B,GAAI,yBAGzC;IACD,QAAQ,EAAE,eAAe,CAAC;IAC1B,QAAQ,EAAE,sBAAsB,CAAC;CAClC,KAAG;IACF,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,sBAAsB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,IAeH,CAAC;AAEF,eAAO,MAAM,wBAAwB,GAAI,8BAItC;IACD,GAAG,CAAC,EAAE,wBAAwB,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB,KAAG,OAWH,CAAC"}
@@ -0,0 +1,173 @@
1
+ import { access, readFile } from 'node:fs/promises';
2
+ export const getAvdDirectory = (name) => {
3
+ return `${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd`;
4
+ };
5
+ export const getAvdConfigPath = (name) => {
6
+ return `${getAvdDirectory(name)}/config.ini`;
7
+ };
8
+ const normalizeAvdValue = (value) => {
9
+ if (!value) {
10
+ return undefined;
11
+ }
12
+ return value.trim();
13
+ };
14
+ const normalizeConfigValue = (value) => {
15
+ return value.trim().toLowerCase();
16
+ };
17
+ const parseSizeInBytes = (value) => {
18
+ if (!value) {
19
+ return null;
20
+ }
21
+ const normalizedValue = value.trim().toLowerCase();
22
+ if (/^\d+$/.test(normalizedValue)) {
23
+ return Number(normalizedValue);
24
+ }
25
+ const match = normalizedValue.match(/^(\d+)([kmgt])$/i);
26
+ if (!match) {
27
+ return null;
28
+ }
29
+ const size = Number(match[1]);
30
+ const unit = match[2]?.toLowerCase();
31
+ const multiplier = unit === 'k'
32
+ ? 1024
33
+ : unit === 'm'
34
+ ? 1024 ** 2
35
+ : unit === 'g'
36
+ ? 1024 ** 3
37
+ : unit === 't'
38
+ ? 1024 ** 4
39
+ : null;
40
+ return multiplier == null ? null : size * multiplier;
41
+ };
42
+ const getApiLevelFromImageSysdir = (value) => {
43
+ const match = value?.match(/android-(\d+)/i);
44
+ return match ? Number(match[1]) : null;
45
+ };
46
+ const normalizeProfile = (value) => {
47
+ if (!value) {
48
+ return undefined;
49
+ }
50
+ return value
51
+ .trim()
52
+ .replace(/[\r\n]+/g, ' ')
53
+ .toLowerCase();
54
+ };
55
+ export const parseAvdConfig = (contents) => {
56
+ const config = {};
57
+ for (const line of contents.split(/\r?\n/)) {
58
+ const trimmedLine = line.trim();
59
+ if (trimmedLine === '' || trimmedLine.startsWith('#')) {
60
+ continue;
61
+ }
62
+ const separatorIndex = trimmedLine.indexOf('=');
63
+ if (separatorIndex === -1) {
64
+ continue;
65
+ }
66
+ const key = trimmedLine.slice(0, separatorIndex).trim();
67
+ const value = trimmedLine.slice(separatorIndex + 1).trim();
68
+ switch (key) {
69
+ case 'image.sysdir.1':
70
+ config.imageSysdir1 = value;
71
+ break;
72
+ case 'abi.type':
73
+ config.abiType = value;
74
+ break;
75
+ case 'hw.device.name':
76
+ config.hwDeviceName = value;
77
+ break;
78
+ case 'disk.dataPartition.size':
79
+ config.diskDataPartitionSize = value;
80
+ break;
81
+ case 'vm.heapSize':
82
+ config.vmHeapSize = value;
83
+ break;
84
+ default:
85
+ break;
86
+ }
87
+ }
88
+ return config;
89
+ };
90
+ export const readAvdConfig = async (name) => {
91
+ const configPath = getAvdConfigPath(name);
92
+ try {
93
+ await access(configPath);
94
+ }
95
+ catch {
96
+ return null;
97
+ }
98
+ return parseAvdConfig(await readFile(configPath, 'utf8'));
99
+ };
100
+ export const isAvdCompatible = ({ emulator, avdConfig, hostArch, }) => {
101
+ const requestedAvdConfig = emulator.avd;
102
+ if (!requestedAvdConfig) {
103
+ return { compatible: false, reason: 'AVD config is required.' };
104
+ }
105
+ if (emulator.name.trim() === '') {
106
+ return { compatible: false, reason: 'AVD name is required.' };
107
+ }
108
+ const apiLevel = getApiLevelFromImageSysdir(avdConfig.imageSysdir1);
109
+ if (apiLevel !== requestedAvdConfig.apiLevel) {
110
+ return {
111
+ compatible: false,
112
+ reason: `API level mismatch: expected ${requestedAvdConfig.apiLevel}, got ${apiLevel ?? 'missing'}.`,
113
+ };
114
+ }
115
+ if (normalizeAvdValue(avdConfig.abiType) !== hostArch) {
116
+ return {
117
+ compatible: false,
118
+ reason: `ABI mismatch: expected ${hostArch}, got ${normalizeAvdValue(avdConfig.abiType) ?? 'missing'}.`,
119
+ };
120
+ }
121
+ if (normalizeProfile(avdConfig.hwDeviceName) !==
122
+ normalizeProfile(requestedAvdConfig.profile)) {
123
+ return {
124
+ compatible: false,
125
+ reason: `Profile mismatch: expected ${requestedAvdConfig.profile}, got ${avdConfig.hwDeviceName ?? 'missing'}.`,
126
+ };
127
+ }
128
+ if ((() => {
129
+ const configuredDiskSizeBytes = parseSizeInBytes(avdConfig.diskDataPartitionSize);
130
+ const requestedDiskSizeBytes = parseSizeInBytes(requestedAvdConfig.diskSize);
131
+ if (configuredDiskSizeBytes != null && requestedDiskSizeBytes != null) {
132
+ return configuredDiskSizeBytes < requestedDiskSizeBytes;
133
+ }
134
+ return (normalizeConfigValue(avdConfig.diskDataPartitionSize ?? '') !==
135
+ normalizeConfigValue(requestedAvdConfig.diskSize));
136
+ })()) {
137
+ return {
138
+ compatible: false,
139
+ reason: `Disk size mismatch: expected ${requestedAvdConfig.diskSize}, got ${avdConfig.diskDataPartitionSize ?? 'missing'}.`,
140
+ };
141
+ }
142
+ if (normalizeConfigValue(avdConfig.vmHeapSize ?? '') !==
143
+ normalizeConfigValue(requestedAvdConfig.heapSize)) {
144
+ return {
145
+ compatible: false,
146
+ reason: `Heap size mismatch: expected ${requestedAvdConfig.heapSize}, got ${avdConfig.vmHeapSize ?? 'missing'}.`,
147
+ };
148
+ }
149
+ return { compatible: true };
150
+ };
151
+ export const getNormalizedAvdCacheConfig = ({ emulator, hostArch, }) => {
152
+ const avd = emulator.avd;
153
+ if (!avd) {
154
+ return null;
155
+ }
156
+ return {
157
+ name: emulator.name,
158
+ apiLevel: avd.apiLevel,
159
+ arch: hostArch,
160
+ profile: avd.profile.trim().toLowerCase(),
161
+ diskSize: avd.diskSize.trim().toLowerCase(),
162
+ heapSize: avd.heapSize.trim().toLowerCase(),
163
+ };
164
+ };
165
+ export const resolveAvdCachingEnabled = ({ avd, isInteractive, env = process.env, }) => {
166
+ const override = env.HARNESS_AVD_CACHING;
167
+ const configValue = avd?.snapshot?.enabled;
168
+ const requestedValue = override == null ? configValue : override.toLowerCase() === 'true';
169
+ if (!requestedValue) {
170
+ return false;
171
+ }
172
+ return !isInteractive;
173
+ };