@react-native-harness/platform-android 1.4.0-rc.1 → 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.
- package/dist/__tests__/adb-wait.test.d.ts +2 -0
- package/dist/__tests__/adb-wait.test.d.ts.map +1 -0
- package/dist/__tests__/adb-wait.test.js +69 -0
- package/dist/__tests__/app-session.test.d.ts +2 -0
- package/dist/__tests__/app-session.test.d.ts.map +1 -0
- package/dist/__tests__/app-session.test.js +57 -0
- package/dist/__tests__/avd-config.test.js +58 -0
- package/dist/__tests__/emulator-startup.test.js +30 -3
- package/dist/__tests__/instance.test.js +10 -4
- package/dist/adb.d.ts +3 -1
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +30 -21
- package/dist/app-session.d.ts +11 -2
- package/dist/app-session.d.ts.map +1 -1
- package/dist/app-session.js +17 -12
- package/dist/avd-config.d.ts +1 -0
- package/dist/avd-config.d.ts.map +1 -1
- package/dist/avd-config.js +11 -0
- package/dist/emulator-startup.d.ts +23 -0
- package/dist/emulator-startup.d.ts.map +1 -1
- package/dist/emulator-startup.js +58 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/instance.d.ts.map +1 -1
- package/dist/instance.js +21 -15
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/__tests__/adb-wait.test.ts +95 -0
- package/src/__tests__/app-session.test.ts +76 -0
- package/src/__tests__/avd-config.test.ts +70 -0
- package/src/__tests__/emulator-startup.test.ts +53 -6
- package/src/__tests__/instance.test.ts +107 -108
- package/src/adb.ts +72 -65
- package/src/app-session.ts +28 -12
- package/src/avd-config.ts +17 -0
- package/src/emulator-startup.ts +74 -0
- package/src/index.ts +1 -0
- package/src/instance.ts +30 -16
- package/src/runner.ts +5 -9
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adb-wait.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adb-wait.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
const mocks = vi.hoisted(() => ({
|
|
3
|
+
spawn: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
// The global test/setup.ts mocks '../adb.js' wholesale (including
|
|
6
|
+
// waitForEmulator itself), which is right for consumers of this module but
|
|
7
|
+
// defeats the purpose here: this suite exercises the *real* polling loop
|
|
8
|
+
// (and its private waitWithSignal helper) to catch timer/listener leaks.
|
|
9
|
+
// Re-declaring the mock for this file restores the actual implementation,
|
|
10
|
+
// while `spawn` and `getAdbBinaryPath` (genuine cross-module imports) are
|
|
11
|
+
// swapped out to keep `getDeviceIds()` from touching a real adb binary.
|
|
12
|
+
vi.mock('../adb.js', async () => {
|
|
13
|
+
const actual = await vi.importActual('../adb.js');
|
|
14
|
+
return actual;
|
|
15
|
+
});
|
|
16
|
+
vi.mock('../environment.js', async () => {
|
|
17
|
+
const actual = await vi.importActual('../environment.js');
|
|
18
|
+
return { ...actual, getAdbBinaryPath: () => '/fake/sdk/platform-tools/adb' };
|
|
19
|
+
});
|
|
20
|
+
vi.mock('@react-native-harness/tools', async () => {
|
|
21
|
+
const actual = await vi.importActual('@react-native-harness/tools');
|
|
22
|
+
return { ...actual, spawn: mocks.spawn };
|
|
23
|
+
});
|
|
24
|
+
const { waitForEmulator } = await import('../adb.js');
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.useRealTimers();
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
describe('waitForEmulator polling', () => {
|
|
30
|
+
it('does not leak an abort listener per poll iteration when the delay wins', async () => {
|
|
31
|
+
vi.useFakeTimers();
|
|
32
|
+
// No matching device on any poll: `adb devices` reports an empty list.
|
|
33
|
+
mocks.spawn.mockResolvedValue({ stdout: 'List of devices attached\n' });
|
|
34
|
+
const controller = new AbortController();
|
|
35
|
+
const addSpy = vi.spyOn(controller.signal, 'addEventListener');
|
|
36
|
+
const removeSpy = vi.spyOn(controller.signal, 'removeEventListener');
|
|
37
|
+
const resultPromise = waitForEmulator('Test_AVD', controller.signal);
|
|
38
|
+
resultPromise.catch(() => undefined);
|
|
39
|
+
for (let i = 0; i < 3; i += 1) {
|
|
40
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
41
|
+
}
|
|
42
|
+
// Regression: each 1s poll iteration used to add an 'abort' listener
|
|
43
|
+
// to the local waitForAbort helper that was never removed when the
|
|
44
|
+
// delay (not the abort) won the race, leaking one listener per second
|
|
45
|
+
// of polling against this session-lifetime signal.
|
|
46
|
+
expect(addSpy.mock.calls.length).toBeGreaterThanOrEqual(3);
|
|
47
|
+
const addCallsBeforeAbort = addSpy.mock.calls.length;
|
|
48
|
+
controller.abort(new Error('teardown'));
|
|
49
|
+
await expect(resultPromise).rejects.toThrow('teardown');
|
|
50
|
+
// Every listener added while losing the race — including the one still
|
|
51
|
+
// pending when the abort itself won — must be removed again.
|
|
52
|
+
expect(removeSpy).toHaveBeenCalledTimes(addCallsBeforeAbort);
|
|
53
|
+
});
|
|
54
|
+
it('clears the pending 1s poll timer when the signal aborts mid-wait', async () => {
|
|
55
|
+
vi.useFakeTimers();
|
|
56
|
+
mocks.spawn.mockResolvedValue({ stdout: 'List of devices attached\n' });
|
|
57
|
+
const controller = new AbortController();
|
|
58
|
+
const resultPromise = waitForEmulator('Test_AVD', controller.signal);
|
|
59
|
+
resultPromise.catch(() => undefined);
|
|
60
|
+
// Let the loop enter its 1s wait, then abort before the timer fires.
|
|
61
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
62
|
+
controller.abort(new Error('teardown'));
|
|
63
|
+
await expect(resultPromise).rejects.toThrow('teardown');
|
|
64
|
+
// Regression: the bare setTimeout-backed `wait()` used to have no
|
|
65
|
+
// handle to clear, so it stayed alive in the event loop for the rest
|
|
66
|
+
// of the 1s interval even after the abort branch won.
|
|
67
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app-session.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/app-session.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { createAndroidAppSession } from '../app-session.js';
|
|
3
|
+
// Mimics nano-spawn: the async iterator only ends once its abort signal
|
|
4
|
+
// fires, matching how startLogcat's real subprocess reacts to abort.
|
|
5
|
+
const createAbortableLogcatProcess = (signal) => ({
|
|
6
|
+
[Symbol.asyncIterator]: () => ({
|
|
7
|
+
next: () => new Promise((resolve) => {
|
|
8
|
+
if (signal.aborted) {
|
|
9
|
+
resolve({ done: true, value: undefined });
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
signal.addEventListener('abort', () => resolve({ done: true, value: undefined }), { once: true });
|
|
13
|
+
}),
|
|
14
|
+
}),
|
|
15
|
+
catch: () => undefined,
|
|
16
|
+
});
|
|
17
|
+
describe('createAndroidAppSession', () => {
|
|
18
|
+
it('combines the session-lifetime signal with its own logcat abort controller', async () => {
|
|
19
|
+
let capturedSignal;
|
|
20
|
+
const controller = new AbortController();
|
|
21
|
+
const session = await createAndroidAppSession({
|
|
22
|
+
appUid: 1,
|
|
23
|
+
bundleId: 'com.example',
|
|
24
|
+
startApp: async () => undefined,
|
|
25
|
+
stopApp: async () => undefined,
|
|
26
|
+
getAppPid: async () => null,
|
|
27
|
+
getLogcatTimestamp: async () => '00:00:00.000',
|
|
28
|
+
startLogcat: (_args, options) => {
|
|
29
|
+
capturedSignal = options.signal;
|
|
30
|
+
return createAbortableLogcatProcess(options.signal);
|
|
31
|
+
},
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
});
|
|
34
|
+
expect(capturedSignal?.aborted).toBe(false);
|
|
35
|
+
controller.abort();
|
|
36
|
+
expect(capturedSignal?.aborted).toBe(true);
|
|
37
|
+
await session.dispose();
|
|
38
|
+
});
|
|
39
|
+
it('still aborts the logcat signal via dispose() with no external signal provided', async () => {
|
|
40
|
+
let capturedSignal;
|
|
41
|
+
const session = await createAndroidAppSession({
|
|
42
|
+
appUid: 1,
|
|
43
|
+
bundleId: 'com.example',
|
|
44
|
+
startApp: async () => undefined,
|
|
45
|
+
stopApp: async () => undefined,
|
|
46
|
+
getAppPid: async () => null,
|
|
47
|
+
getLogcatTimestamp: async () => '00:00:00.000',
|
|
48
|
+
startLogcat: (_args, options) => {
|
|
49
|
+
capturedSignal = options.signal;
|
|
50
|
+
return createAbortableLogcatProcess(options.signal);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
expect(capturedSignal?.aborted).toBe(false);
|
|
54
|
+
await session.dispose();
|
|
55
|
+
expect(capturedSignal?.aborted).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
2
|
import { getNormalizedAvdCacheConfig, isAvdCompatible, parseAvdConfig, resolveAvdCachingEnabled, } from '../avd-config.js';
|
|
3
3
|
import { AndroidPlatformConfigSchema } from '../config.js';
|
|
4
|
+
import { getEmulatorCpuCores } from '../emulator-startup.js';
|
|
5
|
+
const emulatorCpuCores = getEmulatorCpuCores();
|
|
4
6
|
describe('AVD config helpers', () => {
|
|
5
7
|
it('parses snapshot config from Android schema', () => {
|
|
6
8
|
const config = AndroidPlatformConfigSchema.parse({
|
|
@@ -59,7 +61,9 @@ abi.type=x86_64
|
|
|
59
61
|
hw.device.name=pixel_8
|
|
60
62
|
disk.dataPartition.size=1G
|
|
61
63
|
vm.heapSize=512M
|
|
64
|
+
hw.cpu.ncore=${emulatorCpuCores}
|
|
62
65
|
`);
|
|
66
|
+
expect(avdConfig.hwCpuNcore).toBe(String(emulatorCpuCores));
|
|
63
67
|
expect(isAvdCompatible({
|
|
64
68
|
emulator: {
|
|
65
69
|
type: 'emulator',
|
|
@@ -75,6 +79,59 @@ vm.heapSize=512M
|
|
|
75
79
|
hostArch: 'x86_64',
|
|
76
80
|
})).toEqual({ compatible: true });
|
|
77
81
|
});
|
|
82
|
+
it('rejects AVDs missing hw.cpu.ncore', () => {
|
|
83
|
+
const avdConfig = parseAvdConfig(`
|
|
84
|
+
image.sysdir.1=system-images/android-35/default/x86_64/
|
|
85
|
+
abi.type=x86_64
|
|
86
|
+
hw.device.name=pixel_8
|
|
87
|
+
disk.dataPartition.size=1G
|
|
88
|
+
vm.heapSize=512M
|
|
89
|
+
`);
|
|
90
|
+
expect(isAvdCompatible({
|
|
91
|
+
emulator: {
|
|
92
|
+
type: 'emulator',
|
|
93
|
+
name: 'Pixel_8_API_35',
|
|
94
|
+
avd: {
|
|
95
|
+
apiLevel: 35,
|
|
96
|
+
profile: 'pixel_8',
|
|
97
|
+
diskSize: '1G',
|
|
98
|
+
heapSize: '512M',
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
avdConfig,
|
|
102
|
+
hostArch: 'x86_64',
|
|
103
|
+
})).toMatchObject({
|
|
104
|
+
compatible: false,
|
|
105
|
+
reason: `CPU core count mismatch: expected ${emulatorCpuCores}, got missing.`,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
it('rejects AVDs with a different hw.cpu.ncore', () => {
|
|
109
|
+
const avdConfig = parseAvdConfig(`
|
|
110
|
+
image.sysdir.1=system-images/android-35/default/x86_64/
|
|
111
|
+
abi.type=x86_64
|
|
112
|
+
hw.device.name=pixel_8
|
|
113
|
+
disk.dataPartition.size=1G
|
|
114
|
+
vm.heapSize=512M
|
|
115
|
+
hw.cpu.ncore=${emulatorCpuCores === 4 ? 3 : 4}
|
|
116
|
+
`);
|
|
117
|
+
expect(isAvdCompatible({
|
|
118
|
+
emulator: {
|
|
119
|
+
type: 'emulator',
|
|
120
|
+
name: 'Pixel_8_API_35',
|
|
121
|
+
avd: {
|
|
122
|
+
apiLevel: 35,
|
|
123
|
+
profile: 'pixel_8',
|
|
124
|
+
diskSize: '1G',
|
|
125
|
+
heapSize: '512M',
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
avdConfig,
|
|
129
|
+
hostArch: 'x86_64',
|
|
130
|
+
})).toMatchObject({
|
|
131
|
+
compatible: false,
|
|
132
|
+
reason: `CPU core count mismatch: expected ${emulatorCpuCores}, got ${emulatorCpuCores === 4 ? 3 : 4}.`,
|
|
133
|
+
});
|
|
134
|
+
});
|
|
78
135
|
it('accepts disk partition sizes rewritten to bytes', () => {
|
|
79
136
|
const avdConfig = parseAvdConfig(`
|
|
80
137
|
image.sysdir.1=system-images/android-35/default/x86_64/
|
|
@@ -82,6 +139,7 @@ abi.type=x86_64
|
|
|
82
139
|
hw.device.name=pixel_8
|
|
83
140
|
disk.dataPartition.size=6442450944
|
|
84
141
|
vm.heapSize=512M
|
|
142
|
+
hw.cpu.ncore=${emulatorCpuCores}
|
|
85
143
|
`);
|
|
86
144
|
expect(isAvdCompatible({
|
|
87
145
|
emulator: {
|
|
@@ -1,20 +1,47 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getEmulatorStartupArgs } from '../emulator-startup.js';
|
|
2
|
+
import { getEmulatorCpuCores, getEmulatorStartupArgs, isAccelerationUsable, } from '../emulator-startup.js';
|
|
3
|
+
import { getDeviceCoreBudget } from '@react-native-harness/tools';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
describe('getEmulatorCpuCores', () => {
|
|
6
|
+
it('uses the shared device-core budget for the current host', () => {
|
|
7
|
+
expect(getEmulatorCpuCores()).toBe(getDeviceCoreBudget(os.availableParallelism()));
|
|
8
|
+
});
|
|
9
|
+
});
|
|
3
10
|
describe('emulator startup modes', () => {
|
|
4
11
|
it('builds default boot args', () => {
|
|
5
12
|
expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).toEqual(expect.arrayContaining([
|
|
6
13
|
'@Pixel_8_API_35',
|
|
7
14
|
'-no-snapshot-load',
|
|
8
15
|
'-no-snapshot-save',
|
|
16
|
+
'-no-metrics',
|
|
9
17
|
]));
|
|
10
18
|
expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).not.toEqual(expect.arrayContaining(['-camera-back', 'none']));
|
|
11
19
|
});
|
|
12
20
|
it('builds clean snapshot generation args', () => {
|
|
13
|
-
expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).toEqual(expect.arrayContaining([
|
|
21
|
+
expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).toEqual(expect.arrayContaining([
|
|
22
|
+
'@Pixel_8_API_35',
|
|
23
|
+
'-no-snapshot-load',
|
|
24
|
+
'-no-metrics',
|
|
25
|
+
]));
|
|
14
26
|
expect(getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')).not.toContain('-no-snapshot-save');
|
|
15
27
|
});
|
|
16
28
|
it('builds snapshot reuse args', () => {
|
|
17
|
-
expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).toEqual(expect.arrayContaining([
|
|
29
|
+
expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).toEqual(expect.arrayContaining([
|
|
30
|
+
'@Pixel_8_API_35',
|
|
31
|
+
'-no-snapshot-save',
|
|
32
|
+
'-no-metrics',
|
|
33
|
+
]));
|
|
18
34
|
expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).not.toContain('-no-snapshot-load');
|
|
19
35
|
});
|
|
20
36
|
});
|
|
37
|
+
describe('isAccelerationUsable', () => {
|
|
38
|
+
it('returns true when the emulator reports acceleration is usable', () => {
|
|
39
|
+
expect(isAccelerationUsable('accel:\n0\nKVM (version 12) is installed and usable.\naccel\n')).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
it('returns false when acceleration is disabled', () => {
|
|
42
|
+
expect(isAccelerationUsable('accel:\n0\nKVM is not installed on this machine.\naccel\n')).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
it('returns false for empty output', () => {
|
|
45
|
+
expect(isAccelerationUsable('')).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
});
|
|
@@ -5,8 +5,12 @@ import * as adb from '../adb.js';
|
|
|
5
5
|
import * as avdConfig from '../avd-config.js';
|
|
6
6
|
import * as sharedPrefs from '../shared-prefs.js';
|
|
7
7
|
import { HarnessAppPathError, HarnessEmulatorConfigError } from '../errors.js';
|
|
8
|
+
import * as emulatorStartup from '../emulator-startup.js';
|
|
9
|
+
const { getEmulatorCpuCores } = emulatorStartup;
|
|
8
10
|
const createLogcatProcess = (lines = []) => {
|
|
9
|
-
|
|
11
|
+
// Mirrors nano-spawn's real Subprocess, which is both a Promise and an
|
|
12
|
+
// async iterable, since app-session.ts relies on it being thenable.
|
|
13
|
+
const process = Object.assign(Promise.resolve(undefined), {
|
|
10
14
|
nodeChildProcess: Promise.resolve({
|
|
11
15
|
kill: vi.fn(),
|
|
12
16
|
}),
|
|
@@ -15,7 +19,7 @@ const createLogcatProcess = (lines = []) => {
|
|
|
15
19
|
yield line;
|
|
16
20
|
}
|
|
17
21
|
},
|
|
18
|
-
};
|
|
22
|
+
});
|
|
19
23
|
return process;
|
|
20
24
|
};
|
|
21
25
|
const harnessConfig = {
|
|
@@ -34,6 +38,7 @@ describe('Android platform instance', () => {
|
|
|
34
38
|
vi.unstubAllEnvs();
|
|
35
39
|
vi.useRealTimers();
|
|
36
40
|
vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorAvailable').mockResolvedValue('/tmp/android-sdk');
|
|
41
|
+
vi.spyOn(emulatorStartup, 'warnIfEmulatorAccelerationUnavailable').mockResolvedValue(undefined);
|
|
37
42
|
});
|
|
38
43
|
it('reuses a running emulator and does not shut it down on dispose', async () => {
|
|
39
44
|
const ensureAndroidEmulatorEnvironment = vi
|
|
@@ -162,6 +167,7 @@ describe('Android platform instance', () => {
|
|
|
162
167
|
hwDeviceName: 'pixel_8',
|
|
163
168
|
diskDataPartitionSize: '1G',
|
|
164
169
|
vmHeapSize: '1G',
|
|
170
|
+
hwCpuNcore: String(getEmulatorCpuCores()),
|
|
165
171
|
});
|
|
166
172
|
vi.spyOn(adb, 'startEmulator').mockResolvedValue(undefined);
|
|
167
173
|
vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
|
|
@@ -381,7 +387,7 @@ describe('Android platform instance', () => {
|
|
|
381
387
|
'--uid=10234',
|
|
382
388
|
'-T',
|
|
383
389
|
'01-01 00:00:00.000',
|
|
384
|
-
]);
|
|
390
|
+
], { signal: expect.any(AbortSignal) });
|
|
385
391
|
expect(startLogcat.mock.invocationCallOrder[0]).toBeLessThan(startApp.mock.invocationCallOrder[0]);
|
|
386
392
|
await expect(appSession.getState()).resolves.toEqual({
|
|
387
393
|
status: 'running',
|
|
@@ -487,7 +493,7 @@ describe('Android platform instance', () => {
|
|
|
487
493
|
'--uid=10234',
|
|
488
494
|
'-T',
|
|
489
495
|
'01-01 00:00:00.000',
|
|
490
|
-
]);
|
|
496
|
+
], { signal: expect.any(AbortSignal) });
|
|
491
497
|
await expect(appSession.getState()).resolves.toEqual({
|
|
492
498
|
status: 'running',
|
|
493
499
|
pid: 8765,
|
package/dist/adb.d.ts
CHANGED
|
@@ -44,7 +44,9 @@ export declare const getAppPid: (adbId: string, bundleId: string) => Promise<num
|
|
|
44
44
|
export declare const getAppUid: (adbId: string, bundleId: string) => Promise<number>;
|
|
45
45
|
export declare const setHideErrorDialogs: (adbId: string, hide: boolean) => Promise<void>;
|
|
46
46
|
export declare const getLogcatTimestamp: (adbId: string) => Promise<string>;
|
|
47
|
-
export declare const startLogcat: (adbId: string, args: readonly string[]
|
|
47
|
+
export declare const startLogcat: (adbId: string, args: readonly string[], options?: {
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}) => Subprocess;
|
|
48
50
|
export declare const DROPBOX_CRASH_TAGS: readonly ["data_app_crash", "data_app_native_crash"];
|
|
49
51
|
export declare const getDropboxPrint: (adbId: string, tags?: readonly string[]) => Promise<string>;
|
|
50
52
|
export declare const getActivityExitInfo: (adbId: string, bundleId: string) => Promise<string>;
|
package/dist/adb.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAC/E,OAAO,
|
|
1
|
+
{"version":3,"file":"adb.d.ts","sourceRoot":"","sources":["../src/adb.ts"],"names":[],"mappings":"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"}
|
package/dist/adb.js
CHANGED
|
@@ -1,30 +1,27 @@
|
|
|
1
|
-
import { logger, spawn, SubprocessError, } from '@react-native-harness/tools';
|
|
1
|
+
import { delay, logger, spawn, SubprocessError, waitForAbort, } from '@react-native-harness/tools';
|
|
2
2
|
import { spawn as nodeSpawn } from 'node:child_process';
|
|
3
3
|
import { access, rm } from 'node:fs/promises';
|
|
4
4
|
import { getAvdConfigPath, getAvdDirectory, readAvdConfig, } from './avd-config.js';
|
|
5
5
|
import { ensureAndroidEmulatorAvailable, ensureAndroidSdkPackages, getAdbBinaryPath, getAndroidSystemImagePackage, getAvdManagerBinaryPath, getEmulatorBinaryPath, getHostAndroidSystemImageArch, getRequiredAndroidSdkPackages, } from './environment.js';
|
|
6
|
-
import { getEmulatorStartupArgs, } from './emulator-startup.js';
|
|
6
|
+
import { getEmulatorCpuCores, getEmulatorStartupArgs, } from './emulator-startup.js';
|
|
7
7
|
import { AdbAppNotInstalledError, AdbPermissionGrantError, } from './adb-errors.js';
|
|
8
|
-
const wait = async (ms) => {
|
|
9
|
-
await new Promise((resolve) => {
|
|
10
|
-
setTimeout(resolve, ms);
|
|
11
|
-
});
|
|
12
|
-
};
|
|
13
|
-
const waitForAbort = (signal) => {
|
|
14
|
-
if (signal.aborted) {
|
|
15
|
-
return Promise.reject(signal.reason);
|
|
16
|
-
}
|
|
17
|
-
return new Promise((_, reject) => {
|
|
18
|
-
signal.addEventListener('abort', () => {
|
|
19
|
-
reject(signal.reason);
|
|
20
|
-
}, { once: true });
|
|
21
|
-
});
|
|
22
|
-
};
|
|
23
8
|
const waitWithSignal = async (ms, signal) => {
|
|
24
9
|
if (signal.aborted) {
|
|
25
10
|
throw signal.reason;
|
|
26
11
|
}
|
|
27
|
-
|
|
12
|
+
// Both sides of this race must be torn down explicitly when they lose:
|
|
13
|
+
// an uncancelled timer keeps a ref'd Timeout alive, and an uncancelled
|
|
14
|
+
// abort wait leaks a listener on `signal`, which is a session-lifetime
|
|
15
|
+
// signal here and gets polled against once per second.
|
|
16
|
+
const timer = delay(ms);
|
|
17
|
+
const abort = waitForAbort(signal);
|
|
18
|
+
try {
|
|
19
|
+
await Promise.race([timer.promise, abort.promise]);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
timer.cancel();
|
|
23
|
+
abort.cancel();
|
|
24
|
+
}
|
|
28
25
|
};
|
|
29
26
|
const EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS = 5000;
|
|
30
27
|
const EMULATOR_OUTPUT_BUFFER_LIMIT = 16 * 1024;
|
|
@@ -311,9 +308,14 @@ export const createAvd = async ({ name, apiLevel, profile, diskSize, heapSize, }
|
|
|
311
308
|
]);
|
|
312
309
|
await ensureAvdIniExists({ name, apiLevel });
|
|
313
310
|
const configPath = await ensureAvdConfigExists(name);
|
|
311
|
+
// hw.cpu.ncore is baked into config.ini (rather than passed as a `-cores`
|
|
312
|
+
// boot flag) so it's part of the AVD's persisted hardware profile: boot
|
|
313
|
+
// snapshots require an identical hardware config to load, so the vCPU
|
|
314
|
+
// count must be fixed at AVD-creation time and stay consistent across
|
|
315
|
+
// boots.
|
|
314
316
|
await spawn('bash', [
|
|
315
317
|
'-lc',
|
|
316
|
-
`printf '%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' >> "${configPath}"`,
|
|
318
|
+
`printf '%s\n%s\n%s\n' 'disk.dataPartition.size=${diskSize}' 'vm.heapSize=${heapSize}' 'hw.cpu.ncore=${getEmulatorCpuCores()}' >> "${configPath}"`,
|
|
317
319
|
]);
|
|
318
320
|
};
|
|
319
321
|
export const deleteAvd = async (name) => {
|
|
@@ -375,11 +377,17 @@ export const startEmulator = async (name, mode = 'default-boot') => {
|
|
|
375
377
|
}
|
|
376
378
|
throw error;
|
|
377
379
|
});
|
|
378
|
-
const
|
|
380
|
+
const observationTimer = delay(EMULATOR_STARTUP_OBSERVATION_TIMEOUT_MS);
|
|
381
|
+
const observationTimeout = observationTimer.promise.then(() => 'timeout');
|
|
379
382
|
try {
|
|
380
383
|
await Promise.race([earlyExit, observedBoot, observationTimeout]);
|
|
381
384
|
}
|
|
382
385
|
finally {
|
|
386
|
+
// If `earlyExit` or `observedBoot` won the race, this timer would
|
|
387
|
+
// otherwise sit ref'd in the event loop for the rest of its 5s;
|
|
388
|
+
// cancelling here is a no-op on the happy "proceed detached" path
|
|
389
|
+
// where the timer itself won.
|
|
390
|
+
observationTimer.cancel();
|
|
383
391
|
cleanup();
|
|
384
392
|
}
|
|
385
393
|
childProcess.stdout?.destroy();
|
|
@@ -497,9 +505,10 @@ export const getLogcatTimestamp = async (adbId) => {
|
|
|
497
505
|
]);
|
|
498
506
|
return stdout.trim().replace(/^'+|'+$/g, '');
|
|
499
507
|
};
|
|
500
|
-
export const startLogcat = (adbId, args) => spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
|
|
508
|
+
export const startLogcat = (adbId, args, options) => spawn(getAdbBinaryPath(), ['-s', adbId, ...args], {
|
|
501
509
|
stdout: 'pipe',
|
|
502
510
|
stderr: 'pipe',
|
|
511
|
+
signal: options?.signal,
|
|
503
512
|
});
|
|
504
513
|
export const DROPBOX_CRASH_TAGS = [
|
|
505
514
|
'data_app_crash',
|
package/dist/app-session.d.ts
CHANGED
|
@@ -7,11 +7,20 @@ type CreateAndroidAppSessionOptions = {
|
|
|
7
7
|
stopApp: () => Promise<void>;
|
|
8
8
|
getAppPid: () => Promise<number | null>;
|
|
9
9
|
getLogcatTimestamp: () => Promise<string>;
|
|
10
|
-
startLogcat: (args: readonly string[]
|
|
10
|
+
startLogcat: (args: readonly string[], options: {
|
|
11
|
+
signal: AbortSignal;
|
|
12
|
+
}) => Subprocess;
|
|
11
13
|
getDropboxOutput?: () => Promise<string>;
|
|
12
14
|
getExitInfo?: () => Promise<string>;
|
|
13
15
|
crashArtifactWriter?: CrashArtifactWriter;
|
|
16
|
+
/**
|
|
17
|
+
* Session-lifetime abort signal (see HarnessPlatformInitOptions.signal).
|
|
18
|
+
* Combined with the app session's own logcatAbortController so that
|
|
19
|
+
* aborting the session stops the logcat stream even if dispose() hasn't
|
|
20
|
+
* been called yet (e.g. SIGTERM during teardown of other resources).
|
|
21
|
+
*/
|
|
22
|
+
signal?: AbortSignal;
|
|
14
23
|
};
|
|
15
|
-
export declare const createAndroidAppSession: ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, }: CreateAndroidAppSessionOptions) => Promise<AppSession>;
|
|
24
|
+
export declare const createAndroidAppSession: ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, signal, }: CreateAndroidAppSessionOptions) => Promise<AppSession>;
|
|
16
25
|
export {};
|
|
17
26
|
//# sourceMappingURL=app-session.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-session.d.ts","sourceRoot":"","sources":["../src/app-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,UAAU,EAEf,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"app-session.d.ts","sourceRoot":"","sources":["../src/app-session.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,UAAU,EAEf,KAAK,mBAAmB,EACzB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EAGL,KAAK,UAAU,EAChB,MAAM,6BAA6B,CAAC;AA0DrC,KAAK,8BAA8B,GAAG;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,SAAS,EAAE,MAAM,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACxC,kBAAkB,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IAC1C,WAAW,EAAE,CACX,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,OAAO,EAAE;QAAE,MAAM,EAAE,WAAW,CAAA;KAAE,KAC7B,UAAU,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,CAAC;IACpC,mBAAmB,CAAC,EAAE,mBAAmB,CAAC;IAC1C;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAU,kJAY3C,8BAA8B,KAAG,OAAO,CAAC,UAAU,CAmNrD,CAAC"}
|
package/dist/app-session.js
CHANGED
|
@@ -30,16 +30,8 @@ const isCrashSignal = (line, bundleId) => {
|
|
|
30
30
|
(line.includes(bundleId) &&
|
|
31
31
|
/fatal|crash|signal 11|signal 6|backtrace/i.test(line)));
|
|
32
32
|
};
|
|
33
|
-
const stopSubprocess = async (child) => {
|
|
34
|
-
try {
|
|
35
|
-
(await child.nodeChildProcess).kill();
|
|
36
|
-
}
|
|
37
|
-
catch {
|
|
38
|
-
// Ignore termination failures for already-ended background processes.
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
33
|
const isExitedState = (state) => state.status === 'exited';
|
|
42
|
-
export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, }) => {
|
|
34
|
+
export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stopApp, getAppPid, getLogcatTimestamp, startLogcat, getDropboxOutput, getExitInfo, crashArtifactWriter, signal, }) => {
|
|
43
35
|
const emitter = createAppSessionEmitter();
|
|
44
36
|
const logBuffer = createBoundedLogBuffer();
|
|
45
37
|
let state = { status: 'running' };
|
|
@@ -113,7 +105,20 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
|
|
|
113
105
|
};
|
|
114
106
|
const logcatTimestamp = await getLogcatTimestamp();
|
|
115
107
|
const sessionStartedAt = Date.now();
|
|
116
|
-
const
|
|
108
|
+
const logcatAbortController = new AbortController();
|
|
109
|
+
const logcatSignal = signal
|
|
110
|
+
? AbortSignal.any([signal, logcatAbortController.signal])
|
|
111
|
+
: logcatAbortController.signal;
|
|
112
|
+
const logcatProcess = startLogcat(getLogcatArgs(appUid, logcatTimestamp), {
|
|
113
|
+
signal: logcatSignal,
|
|
114
|
+
});
|
|
115
|
+
// Aborting is nano-spawn's own cancellation path, so the resulting
|
|
116
|
+
// SubprocessError is settled the same way regardless of which of the
|
|
117
|
+
// merged stdout/stderr iterators observes it first. Without this, killing
|
|
118
|
+
// the underlying process directly can make both iterators reject at once,
|
|
119
|
+
// and nano-spawn's internal `Promise.race` only reports one of them,
|
|
120
|
+
// leaving the other an unhandled rejection.
|
|
121
|
+
logcatProcess.catch(() => undefined);
|
|
117
122
|
const crashReporter = createAndroidCrashReporter({
|
|
118
123
|
bundleId,
|
|
119
124
|
crashArtifactWriter,
|
|
@@ -156,7 +161,7 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
|
|
|
156
161
|
disposed = true;
|
|
157
162
|
stopPolling = true;
|
|
158
163
|
emitter.clear();
|
|
159
|
-
|
|
164
|
+
logcatAbortController.abort();
|
|
160
165
|
await Promise.allSettled([logTask]);
|
|
161
166
|
throw error;
|
|
162
167
|
}
|
|
@@ -196,7 +201,7 @@ export const createAndroidAppSession = async ({ appUid, bundleId, startApp, stop
|
|
|
196
201
|
}
|
|
197
202
|
cancelPendingPollDelay();
|
|
198
203
|
emitter.clear();
|
|
199
|
-
|
|
204
|
+
logcatAbortController.abort();
|
|
200
205
|
await stopApp();
|
|
201
206
|
await Promise.allSettled([logTask, pollTask]);
|
|
202
207
|
},
|
package/dist/avd-config.d.ts
CHANGED
package/dist/avd-config.d.ts.map
CHANGED
|
@@ -1 +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;
|
|
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;AAG7E,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;IACpB,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,SA4CjD,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,sBA+FH,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"}
|
package/dist/avd-config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { access, readFile } from 'node:fs/promises';
|
|
2
|
+
import { getEmulatorCpuCores } from './emulator-startup.js';
|
|
2
3
|
export const getAvdDirectory = (name) => {
|
|
3
4
|
return `${process.env.ANDROID_AVD_HOME ?? `${process.env.HOME}/.android/avd`}/${name}.avd`;
|
|
4
5
|
};
|
|
@@ -81,6 +82,9 @@ export const parseAvdConfig = (contents) => {
|
|
|
81
82
|
case 'vm.heapSize':
|
|
82
83
|
config.vmHeapSize = value;
|
|
83
84
|
break;
|
|
85
|
+
case 'hw.cpu.ncore':
|
|
86
|
+
config.hwCpuNcore = value;
|
|
87
|
+
break;
|
|
84
88
|
default:
|
|
85
89
|
break;
|
|
86
90
|
}
|
|
@@ -146,6 +150,13 @@ export const isAvdCompatible = ({ emulator, avdConfig, hostArch, }) => {
|
|
|
146
150
|
reason: `Heap size mismatch: expected ${requestedAvdConfig.heapSize}, got ${avdConfig.vmHeapSize ?? 'missing'}.`,
|
|
147
151
|
};
|
|
148
152
|
}
|
|
153
|
+
if (normalizeConfigValue(avdConfig.hwCpuNcore ?? '') !==
|
|
154
|
+
normalizeConfigValue(String(getEmulatorCpuCores()))) {
|
|
155
|
+
return {
|
|
156
|
+
compatible: false,
|
|
157
|
+
reason: `CPU core count mismatch: expected ${getEmulatorCpuCores()}, got ${avdConfig.hwCpuNcore ?? 'missing'}.`,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
149
160
|
return { compatible: true };
|
|
150
161
|
};
|
|
151
162
|
export const getNormalizedAvdCacheConfig = ({ emulator, hostArch, }) => {
|
|
@@ -1,3 +1,26 @@
|
|
|
1
1
|
export type EmulatorBootMode = 'default-boot' | 'clean-snapshot-generation' | 'snapshot-reuse';
|
|
2
|
+
/**
|
|
3
|
+
* Returns the vCPU count baked into an AVD for this runner.
|
|
4
|
+
*
|
|
5
|
+
* AVD snapshots require an identical hardware configuration to load, so all
|
|
6
|
+
* AVD creation, compatibility, and cache-key callers must use this value on
|
|
7
|
+
* the same host.
|
|
8
|
+
*/
|
|
9
|
+
export declare const getEmulatorCpuCores: () => number;
|
|
2
10
|
export declare const getEmulatorStartupArgs: (name: string, mode: EmulatorBootMode) => string[];
|
|
11
|
+
/**
|
|
12
|
+
* Parses the output of `emulator -accel-check` to determine whether
|
|
13
|
+
* hardware acceleration (KVM on Linux, HVF on macOS) is usable. The
|
|
14
|
+
* emulator prints a message containing "is installed and usable" when
|
|
15
|
+
* acceleration is available.
|
|
16
|
+
*/
|
|
17
|
+
export declare const isAccelerationUsable: (accelCheckOutput: string) => boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Runs `emulator -accel-check` and logs a non-fatal warning when hardware
|
|
20
|
+
* acceleration (KVM/HVF) appears unavailable, in which case the emulator
|
|
21
|
+
* falls back to very slow software emulation. This check must never fail or
|
|
22
|
+
* block emulator startup - any error running it is swallowed and only
|
|
23
|
+
* debug-logged.
|
|
24
|
+
*/
|
|
25
|
+
export declare const warnIfEmulatorAccelerationUnavailable: () => Promise<void>;
|
|
3
26
|
//# sourceMappingURL=emulator-startup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"emulator-startup.d.ts","sourceRoot":"","sources":["../src/emulator-startup.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"emulator-startup.d.ts","sourceRoot":"","sources":["../src/emulator-startup.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,gBAAgB,GACxB,cAAc,GACd,2BAA2B,GAC3B,gBAAgB,CAAC;AAErB;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,QAAO,MAEtC,CAAC;AAWF,eAAO,MAAM,sBAAsB,GACjC,MAAM,MAAM,EACZ,MAAM,gBAAgB,KACrB,MAAM,EASR,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,GAAI,kBAAkB,MAAM,KAAG,OAE/D,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,qCAAqC,QACtC,OAAO,CAAC,IAAI,CA+BrB,CAAC"}
|