@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.
- package/README.md +9 -2
- package/dist/__tests__/adb.test.js +283 -10
- package/dist/__tests__/avd-config.test.d.ts +2 -0
- package/dist/__tests__/avd-config.test.d.ts.map +1 -0
- package/dist/__tests__/avd-config.test.js +174 -0
- package/dist/__tests__/ci-action.test.d.ts +2 -0
- package/dist/__tests__/ci-action.test.d.ts.map +1 -0
- package/dist/__tests__/ci-action.test.js +46 -0
- package/dist/__tests__/emulator-startup.test.d.ts +2 -0
- package/dist/__tests__/emulator-startup.test.d.ts.map +1 -0
- package/dist/__tests__/emulator-startup.test.js +19 -0
- package/dist/__tests__/environment.test.d.ts +2 -0
- package/dist/__tests__/environment.test.d.ts.map +1 -0
- package/dist/__tests__/environment.test.js +117 -0
- package/dist/__tests__/instance.test.d.ts +2 -0
- package/dist/__tests__/instance.test.d.ts.map +1 -0
- package/dist/__tests__/instance.test.js +423 -0
- package/dist/__tests__/targets.test.d.ts +2 -0
- package/dist/__tests__/targets.test.d.ts.map +1 -0
- package/dist/__tests__/targets.test.js +49 -0
- package/dist/adb.d.ts +23 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +259 -16
- package/dist/app-monitor.d.ts.map +1 -1
- package/dist/app-monitor.js +27 -7
- package/dist/assertions.d.ts +5 -0
- package/dist/assertions.d.ts.map +1 -0
- package/dist/assertions.js +6 -0
- package/dist/avd-config.d.ts +41 -0
- package/dist/avd-config.d.ts.map +1 -0
- package/dist/avd-config.js +173 -0
- package/dist/config.d.ts +77 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/emulator-startup.d.ts +3 -0
- package/dist/emulator-startup.d.ts.map +1 -0
- package/dist/emulator-startup.js +17 -0
- package/dist/emulator.d.ts +6 -0
- package/dist/emulator.d.ts.map +1 -0
- package/dist/emulator.js +27 -0
- package/dist/environment.d.ts +31 -0
- package/dist/environment.d.ts.map +1 -0
- package/dist/environment.js +317 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +14 -0
- package/dist/factory.d.ts.map +1 -1
- package/dist/factory.js +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/instance.d.ts +6 -0
- package/dist/instance.d.ts.map +1 -0
- package/dist/instance.js +232 -0
- package/dist/reader.d.ts +6 -0
- package/dist/reader.d.ts.map +1 -0
- package/dist/reader.js +57 -0
- package/dist/runner.d.ts +2 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +9 -52
- package/dist/targets.d.ts +1 -1
- package/dist/targets.d.ts.map +1 -1
- package/dist/targets.js +4 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/types.d.ts +381 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +107 -0
- package/package.json +4 -4
- package/src/__tests__/adb.test.ts +419 -15
- package/src/__tests__/avd-config.test.ts +206 -0
- package/src/__tests__/ci-action.test.ts +81 -0
- package/src/__tests__/emulator-startup.test.ts +32 -0
- package/src/__tests__/environment.test.ts +212 -0
- package/src/__tests__/instance.test.ts +610 -0
- package/src/__tests__/targets.test.ts +53 -0
- package/src/adb.ts +430 -28
- package/src/app-monitor.ts +56 -18
- package/src/avd-config.ts +290 -0
- package/src/config.ts +8 -0
- package/src/emulator-startup.ts +28 -0
- package/src/environment.ts +554 -0
- package/src/errors.ts +19 -0
- package/src/factory.ts +4 -0
- package/src/index.ts +7 -1
- package/src/instance.ts +380 -0
- package/src/runner.ts +19 -70
- package/src/targets.ts +18 -8
|
@@ -1,6 +1,43 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import { PassThrough } from 'node:stream';
|
|
3
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { SubprocessError } from '@react-native-harness/tools';
|
|
5
|
+
import {
|
|
6
|
+
createAvd,
|
|
7
|
+
deleteAvd,
|
|
8
|
+
emulatorProcess,
|
|
9
|
+
getAppUid,
|
|
10
|
+
getLogcatTimestamp,
|
|
11
|
+
getStartAppArgs,
|
|
12
|
+
hasAvd,
|
|
13
|
+
installApp,
|
|
14
|
+
startEmulator,
|
|
15
|
+
waitForBoot,
|
|
16
|
+
waitForEmulatorDisconnect,
|
|
17
|
+
} from '../adb.js';
|
|
3
18
|
import * as tools from '@react-native-harness/tools';
|
|
19
|
+
import * as environment from '../environment.js';
|
|
20
|
+
|
|
21
|
+
const createAbortError = () =>
|
|
22
|
+
new DOMException('The operation was aborted', 'AbortError');
|
|
23
|
+
|
|
24
|
+
const createMockChildProcess = () => {
|
|
25
|
+
const process = new EventEmitter() as EventEmitter & {
|
|
26
|
+
stdout: PassThrough;
|
|
27
|
+
stderr: PassThrough;
|
|
28
|
+
unref: ReturnType<typeof vi.fn>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
process.stdout = new PassThrough();
|
|
32
|
+
process.stderr = new PassThrough();
|
|
33
|
+
process.unref = vi.fn();
|
|
34
|
+
|
|
35
|
+
return process;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
vi.restoreAllMocks();
|
|
40
|
+
});
|
|
4
41
|
|
|
5
42
|
describe('getStartAppArgs', () => {
|
|
6
43
|
it('maps supported extras to adb am start flags', () => {
|
|
@@ -45,18 +82,16 @@ describe('getStartAppArgs', () => {
|
|
|
45
82
|
});
|
|
46
83
|
|
|
47
84
|
it('extracts app uid from pm list packages output', async () => {
|
|
48
|
-
const spawnSpy = vi
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'package:com.other.app uid:10123\npackage:com.example.app uid:10234\n',
|
|
53
|
-
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
85
|
+
const spawnSpy = vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
|
|
86
|
+
stdout:
|
|
87
|
+
'package:com.other.app uid:10123\npackage:com.example.app uid:10234\n',
|
|
88
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
54
89
|
|
|
55
90
|
await expect(getAppUid('emulator-5554', 'com.example.app')).resolves.toBe(
|
|
56
91
|
10234
|
|
57
92
|
);
|
|
58
93
|
|
|
59
|
-
expect(spawnSpy).toHaveBeenCalledWith(
|
|
94
|
+
expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
|
|
60
95
|
'-s',
|
|
61
96
|
'emulator-5554',
|
|
62
97
|
'shell',
|
|
@@ -68,17 +103,15 @@ describe('getStartAppArgs', () => {
|
|
|
68
103
|
});
|
|
69
104
|
|
|
70
105
|
it('reads the device timestamp in logcat format', async () => {
|
|
71
|
-
const spawnSpy = vi
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
stdout: "'03-12 11:35:08.000'\n",
|
|
75
|
-
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
106
|
+
const spawnSpy = vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
|
|
107
|
+
stdout: "'03-12 11:35:08.000'\n",
|
|
108
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
76
109
|
|
|
77
110
|
await expect(getLogcatTimestamp('emulator-5554')).resolves.toBe(
|
|
78
111
|
'03-12 11:35:08.000'
|
|
79
112
|
);
|
|
80
113
|
|
|
81
|
-
expect(spawnSpy).toHaveBeenCalledWith(
|
|
114
|
+
expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
|
|
82
115
|
'-s',
|
|
83
116
|
'emulator-5554',
|
|
84
117
|
'shell',
|
|
@@ -86,4 +119,375 @@ describe('getStartAppArgs', () => {
|
|
|
86
119
|
"+'%m-%d %H:%M:%S.000'",
|
|
87
120
|
]);
|
|
88
121
|
});
|
|
122
|
+
|
|
123
|
+
it('checks whether an AVD exists', async () => {
|
|
124
|
+
vi.spyOn(tools, 'spawn').mockResolvedValueOnce({
|
|
125
|
+
stdout: 'Pixel_6_API_33\nPixel_8_API_35\n',
|
|
126
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
127
|
+
|
|
128
|
+
await expect(hasAvd('Pixel_8_API_35')).resolves.toBe(true);
|
|
129
|
+
await expect(hasAvd('Missing_AVD')).resolves.toBe(false);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('installs the app via adb', async () => {
|
|
133
|
+
const spawnSpy = vi
|
|
134
|
+
.spyOn(tools, 'spawn')
|
|
135
|
+
.mockResolvedValueOnce({} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
136
|
+
|
|
137
|
+
await installApp('emulator-5554', '/tmp/app.apk');
|
|
138
|
+
|
|
139
|
+
expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
|
|
140
|
+
'-s',
|
|
141
|
+
'emulator-5554',
|
|
142
|
+
'install',
|
|
143
|
+
'-r',
|
|
144
|
+
'/tmp/app.apk',
|
|
145
|
+
]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('creates an AVD and appends config overrides', async () => {
|
|
149
|
+
const spawnSpy = vi
|
|
150
|
+
.spyOn(tools, 'spawn')
|
|
151
|
+
.mockResolvedValue({} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
152
|
+
const verifyAndroidEmulatorSdk = vi
|
|
153
|
+
.spyOn(environment, 'ensureAndroidSdkPackages')
|
|
154
|
+
.mockResolvedValue('/tmp/android-sdk');
|
|
155
|
+
vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
|
|
156
|
+
'x86_64'
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
await createAvd({
|
|
160
|
+
name: 'Pixel_8_API_35',
|
|
161
|
+
apiLevel: 35,
|
|
162
|
+
profile: 'pixel_8',
|
|
163
|
+
diskSize: '1G',
|
|
164
|
+
heapSize: '1G',
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(verifyAndroidEmulatorSdk).toHaveBeenCalledWith([
|
|
168
|
+
'platform-tools',
|
|
169
|
+
'emulator',
|
|
170
|
+
'platforms;android-35',
|
|
171
|
+
'system-images;android-35;default;x86_64',
|
|
172
|
+
]);
|
|
173
|
+
expect(spawnSpy).toHaveBeenNthCalledWith(1, 'bash', [
|
|
174
|
+
'-lc',
|
|
175
|
+
expect.stringContaining(
|
|
176
|
+
'create avd --force --name "Pixel_8_API_35" --package "system-images;android-35;default;x86_64" --device "pixel_8"'
|
|
177
|
+
),
|
|
178
|
+
]);
|
|
179
|
+
expect(spawnSpy).toHaveBeenNthCalledWith(2, 'bash', [
|
|
180
|
+
'-lc',
|
|
181
|
+
expect.stringContaining(
|
|
182
|
+
`'disk.dataPartition.size=1G' 'vm.heapSize=1G' >> `
|
|
183
|
+
),
|
|
184
|
+
]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('creates an AVD with arm64 system image packages on arm64 hosts', async () => {
|
|
188
|
+
vi.spyOn(tools, 'spawn').mockResolvedValue(
|
|
189
|
+
{} as Awaited<ReturnType<typeof tools.spawn>>
|
|
190
|
+
);
|
|
191
|
+
const ensureAndroidSdkPackages = vi
|
|
192
|
+
.spyOn(environment, 'ensureAndroidSdkPackages')
|
|
193
|
+
.mockResolvedValue('/tmp/android-sdk');
|
|
194
|
+
vi.spyOn(environment, 'getHostAndroidSystemImageArch').mockReturnValue(
|
|
195
|
+
'arm64-v8a'
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
await createAvd({
|
|
199
|
+
name: 'Pixel_8_API_35',
|
|
200
|
+
apiLevel: 35,
|
|
201
|
+
profile: 'pixel_8',
|
|
202
|
+
diskSize: '1G',
|
|
203
|
+
heapSize: '1G',
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(ensureAndroidSdkPackages).toHaveBeenCalledWith([
|
|
207
|
+
'platform-tools',
|
|
208
|
+
'emulator',
|
|
209
|
+
'platforms;android-35',
|
|
210
|
+
'system-images;android-35;default;arm64-v8a',
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it.skip('deletes both AVD directory and ini file', async () => {
|
|
215
|
+
await deleteAvd('Pixel_8_API_35');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('surfaces emulator stdout when startup fails immediately', async () => {
|
|
219
|
+
const child = createMockChildProcess();
|
|
220
|
+
let launcherReadyResolve: (() => void) | undefined;
|
|
221
|
+
const launcherReady = new Promise<void>((resolve) => {
|
|
222
|
+
launcherReadyResolve = resolve;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
vi.spyOn(tools, 'spawn').mockResolvedValue({
|
|
226
|
+
stdout: 'List of devices attached\n\n',
|
|
227
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
228
|
+
vi.spyOn(emulatorProcess, 'startDetachedProcess').mockImplementation(() => {
|
|
229
|
+
launcherReadyResolve?.();
|
|
230
|
+
return child as unknown as ReturnType<
|
|
231
|
+
typeof emulatorProcess.startDetachedProcess
|
|
232
|
+
>;
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
const startPromise = startEmulator('Pixel_8_API_35');
|
|
236
|
+
await launcherReady;
|
|
237
|
+
|
|
238
|
+
child.stdout.write('Unknown AVD name [Pixel_8_API_35]\n');
|
|
239
|
+
child.stdout.end();
|
|
240
|
+
child.stderr.end();
|
|
241
|
+
child.emit('close', 1, null);
|
|
242
|
+
|
|
243
|
+
await expect(startPromise).rejects.toThrow(
|
|
244
|
+
'Unknown AVD name [Pixel_8_API_35]'
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('surfaces emulator stderr when startup fails immediately', async () => {
|
|
249
|
+
const child = createMockChildProcess();
|
|
250
|
+
let launcherReadyResolve: (() => void) | undefined;
|
|
251
|
+
const launcherReady = new Promise<void>((resolve) => {
|
|
252
|
+
launcherReadyResolve = resolve;
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
vi.spyOn(tools, 'spawn').mockResolvedValue({
|
|
256
|
+
stdout: 'List of devices attached\n\n',
|
|
257
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
258
|
+
vi.spyOn(emulatorProcess, 'startDetachedProcess').mockImplementation(() => {
|
|
259
|
+
launcherReadyResolve?.();
|
|
260
|
+
return child as unknown as ReturnType<
|
|
261
|
+
typeof emulatorProcess.startDetachedProcess
|
|
262
|
+
>;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
const startPromise = startEmulator('Pixel_8_API_35');
|
|
266
|
+
await launcherReady;
|
|
267
|
+
|
|
268
|
+
child.stderr.write('emulator: panic: broken config\n');
|
|
269
|
+
child.stdout.end();
|
|
270
|
+
child.stderr.end();
|
|
271
|
+
child.emit('close', 1, null);
|
|
272
|
+
|
|
273
|
+
await expect(startPromise).rejects.toThrow(
|
|
274
|
+
'emulator: panic: broken config'
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns after the emulator appears without waiting for process exit', async () => {
|
|
279
|
+
vi.useFakeTimers();
|
|
280
|
+
const child = createMockChildProcess();
|
|
281
|
+
const spawnSpy = vi.spyOn(tools, 'spawn');
|
|
282
|
+
|
|
283
|
+
spawnSpy
|
|
284
|
+
.mockResolvedValueOnce({
|
|
285
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
286
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
287
|
+
.mockResolvedValueOnce({
|
|
288
|
+
stdout: 'Pixel_8_API_35\n',
|
|
289
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
290
|
+
|
|
291
|
+
vi.spyOn(emulatorProcess, 'startDetachedProcess').mockReturnValue(
|
|
292
|
+
child as unknown as ReturnType<
|
|
293
|
+
typeof emulatorProcess.startDetachedProcess
|
|
294
|
+
>
|
|
295
|
+
);
|
|
296
|
+
|
|
297
|
+
const startPromise = startEmulator('Pixel_8_API_35');
|
|
298
|
+
|
|
299
|
+
await vi.runAllTimersAsync();
|
|
300
|
+
|
|
301
|
+
await expect(startPromise).resolves.toBeUndefined();
|
|
302
|
+
expect(child.unref).toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it('passes default boot args to the emulator process', async () => {
|
|
306
|
+
vi.useFakeTimers();
|
|
307
|
+
const child = createMockChildProcess();
|
|
308
|
+
vi.spyOn(tools, 'spawn')
|
|
309
|
+
.mockResolvedValueOnce({
|
|
310
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
311
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
312
|
+
.mockResolvedValueOnce({
|
|
313
|
+
stdout: 'Pixel_8_API_35\n',
|
|
314
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
315
|
+
const startDetachedProcess = vi
|
|
316
|
+
.spyOn(emulatorProcess, 'startDetachedProcess')
|
|
317
|
+
.mockReturnValue(
|
|
318
|
+
child as unknown as ReturnType<
|
|
319
|
+
typeof emulatorProcess.startDetachedProcess
|
|
320
|
+
>
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const startPromise = startEmulator('Pixel_8_API_35');
|
|
324
|
+
await vi.runAllTimersAsync();
|
|
325
|
+
await startPromise;
|
|
326
|
+
|
|
327
|
+
expect(startDetachedProcess).toHaveBeenCalledWith(
|
|
328
|
+
expect.stringMatching(/emulator$/),
|
|
329
|
+
expect.arrayContaining(['-no-snapshot-load', '-no-snapshot-save'])
|
|
330
|
+
);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('passes clean snapshot generation args to the emulator process', async () => {
|
|
334
|
+
vi.useFakeTimers();
|
|
335
|
+
const child = createMockChildProcess();
|
|
336
|
+
vi.spyOn(tools, 'spawn')
|
|
337
|
+
.mockResolvedValueOnce({
|
|
338
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
339
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
340
|
+
.mockResolvedValueOnce({
|
|
341
|
+
stdout: 'Pixel_8_API_35\n',
|
|
342
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
343
|
+
const startDetachedProcess = vi
|
|
344
|
+
.spyOn(emulatorProcess, 'startDetachedProcess')
|
|
345
|
+
.mockReturnValue(
|
|
346
|
+
child as unknown as ReturnType<
|
|
347
|
+
typeof emulatorProcess.startDetachedProcess
|
|
348
|
+
>
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
const startPromise = startEmulator(
|
|
352
|
+
'Pixel_8_API_35',
|
|
353
|
+
'clean-snapshot-generation'
|
|
354
|
+
);
|
|
355
|
+
await vi.runAllTimersAsync();
|
|
356
|
+
await startPromise;
|
|
357
|
+
|
|
358
|
+
expect(startDetachedProcess).toHaveBeenCalledWith(
|
|
359
|
+
expect.stringMatching(/emulator$/),
|
|
360
|
+
expect.arrayContaining(['-no-snapshot-load'])
|
|
361
|
+
);
|
|
362
|
+
expect(startDetachedProcess.mock.calls[0]?.[1]).not.toContain(
|
|
363
|
+
'-no-snapshot-save'
|
|
364
|
+
);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('passes snapshot reuse args to the emulator process', async () => {
|
|
368
|
+
vi.useFakeTimers();
|
|
369
|
+
const child = createMockChildProcess();
|
|
370
|
+
vi.spyOn(tools, 'spawn')
|
|
371
|
+
.mockResolvedValueOnce({
|
|
372
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
373
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
374
|
+
.mockResolvedValueOnce({
|
|
375
|
+
stdout: 'Pixel_8_API_35\n',
|
|
376
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
377
|
+
const startDetachedProcess = vi
|
|
378
|
+
.spyOn(emulatorProcess, 'startDetachedProcess')
|
|
379
|
+
.mockReturnValue(
|
|
380
|
+
child as unknown as ReturnType<
|
|
381
|
+
typeof emulatorProcess.startDetachedProcess
|
|
382
|
+
>
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const startPromise = startEmulator('Pixel_8_API_35', 'snapshot-reuse');
|
|
386
|
+
await vi.runAllTimersAsync();
|
|
387
|
+
await startPromise;
|
|
388
|
+
|
|
389
|
+
expect(startDetachedProcess).toHaveBeenCalledWith(
|
|
390
|
+
expect.stringMatching(/emulator$/),
|
|
391
|
+
expect.arrayContaining(['-no-snapshot-save'])
|
|
392
|
+
);
|
|
393
|
+
expect(startDetachedProcess.mock.calls[0]?.[1]).not.toContain(
|
|
394
|
+
'-no-snapshot-load'
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
it('aborts while waiting for an emulator to boot', async () => {
|
|
399
|
+
vi.useFakeTimers();
|
|
400
|
+
vi.spyOn(tools, 'spawn').mockResolvedValue({
|
|
401
|
+
stdout: 'List of devices attached\n\n',
|
|
402
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
403
|
+
const controller = new AbortController();
|
|
404
|
+
const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
|
|
405
|
+
|
|
406
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
407
|
+
controller.abort(createAbortError());
|
|
408
|
+
|
|
409
|
+
await expect(waitPromise).rejects.toBeInstanceOf(DOMException);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('aborts while waiting for boot completion', async () => {
|
|
413
|
+
vi.useFakeTimers();
|
|
414
|
+
const spawnSpy = vi.spyOn(tools, 'spawn');
|
|
415
|
+
spawnSpy
|
|
416
|
+
.mockResolvedValueOnce({
|
|
417
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
418
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
419
|
+
.mockResolvedValueOnce({
|
|
420
|
+
stdout: 'Pixel_8_API_35\n',
|
|
421
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
422
|
+
.mockResolvedValueOnce({
|
|
423
|
+
stdout: '0\n',
|
|
424
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
425
|
+
const controller = new AbortController();
|
|
426
|
+
const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
|
|
427
|
+
|
|
428
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
429
|
+
controller.abort(createAbortError());
|
|
430
|
+
|
|
431
|
+
await expect(waitPromise).rejects.toBeInstanceOf(DOMException);
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('treats transient adb shell failures as not-yet-booted', async () => {
|
|
435
|
+
vi.useFakeTimers();
|
|
436
|
+
const spawnSpy = vi.spyOn(tools, 'spawn');
|
|
437
|
+
const transientShellError = Object.assign(new Error('adb shell failed'), {
|
|
438
|
+
exitCode: 1,
|
|
439
|
+
});
|
|
440
|
+
Object.setPrototypeOf(transientShellError, SubprocessError.prototype);
|
|
441
|
+
|
|
442
|
+
spawnSpy
|
|
443
|
+
.mockResolvedValueOnce({
|
|
444
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
445
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
446
|
+
.mockResolvedValueOnce({
|
|
447
|
+
stdout: 'Pixel_8_API_35\n',
|
|
448
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
449
|
+
.mockRejectedValueOnce(transientShellError)
|
|
450
|
+
.mockResolvedValueOnce({
|
|
451
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
452
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
453
|
+
.mockResolvedValueOnce({
|
|
454
|
+
stdout: 'Pixel_8_API_35\n',
|
|
455
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
456
|
+
.mockResolvedValueOnce({
|
|
457
|
+
stdout: '1\n',
|
|
458
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
459
|
+
|
|
460
|
+
const waitPromise = waitForBoot(
|
|
461
|
+
'Pixel_8_API_35',
|
|
462
|
+
new AbortController().signal
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
466
|
+
|
|
467
|
+
await expect(waitPromise).resolves.toBe('emulator-5554');
|
|
468
|
+
expect(spawnSpy).toHaveBeenCalledTimes(6);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
it('waits for an emulator to disconnect from adb', async () => {
|
|
472
|
+
vi.useFakeTimers();
|
|
473
|
+
const spawnSpy = vi.spyOn(tools, 'spawn');
|
|
474
|
+
|
|
475
|
+
spawnSpy
|
|
476
|
+
.mockResolvedValueOnce({
|
|
477
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
478
|
+
} as Awaited<ReturnType<typeof tools.spawn>>)
|
|
479
|
+
.mockResolvedValueOnce({
|
|
480
|
+
stdout: 'List of devices attached\n\n',
|
|
481
|
+
} as Awaited<ReturnType<typeof tools.spawn>>);
|
|
482
|
+
|
|
483
|
+
const waitPromise = waitForEmulatorDisconnect(
|
|
484
|
+
'emulator-5554',
|
|
485
|
+
new AbortController().signal
|
|
486
|
+
);
|
|
487
|
+
|
|
488
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
489
|
+
|
|
490
|
+
await expect(waitPromise).resolves.toBeUndefined();
|
|
491
|
+
expect(spawnSpy).toHaveBeenCalledTimes(2);
|
|
492
|
+
});
|
|
89
493
|
});
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
getNormalizedAvdCacheConfig,
|
|
4
|
+
isAvdCompatible,
|
|
5
|
+
parseAvdConfig,
|
|
6
|
+
resolveAvdCachingEnabled,
|
|
7
|
+
} from '../avd-config.js';
|
|
8
|
+
import { AndroidPlatformConfigSchema } from '../config.js';
|
|
9
|
+
|
|
10
|
+
describe('AVD config helpers', () => {
|
|
11
|
+
it('parses snapshot config from Android schema', () => {
|
|
12
|
+
const config = AndroidPlatformConfigSchema.parse({
|
|
13
|
+
name: 'android',
|
|
14
|
+
bundleId: 'com.example.app',
|
|
15
|
+
device: {
|
|
16
|
+
type: 'emulator',
|
|
17
|
+
name: 'Pixel_8_API_35',
|
|
18
|
+
avd: {
|
|
19
|
+
apiLevel: 35,
|
|
20
|
+
profile: 'pixel_8',
|
|
21
|
+
diskSize: '1G',
|
|
22
|
+
heapSize: '512M',
|
|
23
|
+
snapshot: {
|
|
24
|
+
enabled: true,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(config.device.type).toBe('emulator');
|
|
31
|
+
if (config.device.type === 'emulator') {
|
|
32
|
+
expect(config.device.avd?.snapshot?.enabled).toBe(true);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('lets HARNESS_AVD_CACHING override config before interactive gating', () => {
|
|
37
|
+
expect(
|
|
38
|
+
resolveAvdCachingEnabled({
|
|
39
|
+
avd: {
|
|
40
|
+
apiLevel: 35,
|
|
41
|
+
profile: 'pixel_8',
|
|
42
|
+
diskSize: '1G',
|
|
43
|
+
heapSize: '1G',
|
|
44
|
+
snapshot: { enabled: false },
|
|
45
|
+
},
|
|
46
|
+
isInteractive: false,
|
|
47
|
+
env: {
|
|
48
|
+
HARNESS_AVD_CACHING: 'true',
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('disables caching for interactive sessions even when requested', () => {
|
|
55
|
+
expect(
|
|
56
|
+
resolveAvdCachingEnabled({
|
|
57
|
+
avd: {
|
|
58
|
+
apiLevel: 35,
|
|
59
|
+
profile: 'pixel_8',
|
|
60
|
+
diskSize: '1G',
|
|
61
|
+
heapSize: '1G',
|
|
62
|
+
snapshot: { enabled: true },
|
|
63
|
+
},
|
|
64
|
+
isInteractive: true,
|
|
65
|
+
})
|
|
66
|
+
).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('parses config.ini and matches compatible AVD metadata', () => {
|
|
70
|
+
const avdConfig = parseAvdConfig(`
|
|
71
|
+
image.sysdir.1=system-images/android-35/default/x86_64/
|
|
72
|
+
abi.type=x86_64
|
|
73
|
+
hw.device.name=pixel_8
|
|
74
|
+
disk.dataPartition.size=1G
|
|
75
|
+
vm.heapSize=512M
|
|
76
|
+
`);
|
|
77
|
+
|
|
78
|
+
expect(
|
|
79
|
+
isAvdCompatible({
|
|
80
|
+
emulator: {
|
|
81
|
+
type: 'emulator',
|
|
82
|
+
name: 'Pixel_8_API_35',
|
|
83
|
+
avd: {
|
|
84
|
+
apiLevel: 35,
|
|
85
|
+
profile: 'pixel_8',
|
|
86
|
+
diskSize: '1G',
|
|
87
|
+
heapSize: '512M',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
avdConfig,
|
|
91
|
+
hostArch: 'x86_64',
|
|
92
|
+
})
|
|
93
|
+
).toEqual({ compatible: true });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('accepts disk partition sizes rewritten to bytes', () => {
|
|
97
|
+
const avdConfig = parseAvdConfig(`
|
|
98
|
+
image.sysdir.1=system-images/android-35/default/x86_64/
|
|
99
|
+
abi.type=x86_64
|
|
100
|
+
hw.device.name=pixel_8
|
|
101
|
+
disk.dataPartition.size=6442450944
|
|
102
|
+
vm.heapSize=512M
|
|
103
|
+
`);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
isAvdCompatible({
|
|
107
|
+
emulator: {
|
|
108
|
+
type: 'emulator',
|
|
109
|
+
name: 'Pixel_8_API_35',
|
|
110
|
+
avd: {
|
|
111
|
+
apiLevel: 35,
|
|
112
|
+
profile: 'pixel_8',
|
|
113
|
+
diskSize: '1G',
|
|
114
|
+
heapSize: '512M',
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
avdConfig,
|
|
118
|
+
hostArch: 'x86_64',
|
|
119
|
+
})
|
|
120
|
+
).toEqual({ compatible: true });
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('rejects smaller disk partitions even when sizes are normalized', () => {
|
|
124
|
+
const avdConfig = parseAvdConfig(`
|
|
125
|
+
image.sysdir.1=system-images/android-35/default/x86_64/
|
|
126
|
+
abi.type=x86_64
|
|
127
|
+
hw.device.name=pixel_8
|
|
128
|
+
disk.dataPartition.size=536870912
|
|
129
|
+
vm.heapSize=512M
|
|
130
|
+
`);
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
isAvdCompatible({
|
|
134
|
+
emulator: {
|
|
135
|
+
type: 'emulator',
|
|
136
|
+
name: 'Pixel_8_API_35',
|
|
137
|
+
avd: {
|
|
138
|
+
apiLevel: 35,
|
|
139
|
+
profile: 'pixel_8',
|
|
140
|
+
diskSize: '1G',
|
|
141
|
+
heapSize: '512M',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
avdConfig,
|
|
145
|
+
hostArch: 'x86_64',
|
|
146
|
+
})
|
|
147
|
+
).toMatchObject({
|
|
148
|
+
compatible: false,
|
|
149
|
+
reason: 'Disk size mismatch: expected 1G, got 536870912.',
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('reports incompatibility when AVD metadata differs', () => {
|
|
154
|
+
const avdConfig = parseAvdConfig(`
|
|
155
|
+
image.sysdir.1=system-images/android-34/default/x86_64/
|
|
156
|
+
abi.type=x86_64
|
|
157
|
+
hw.device.name=pixel_7
|
|
158
|
+
disk.dataPartition.size=2G
|
|
159
|
+
vm.heapSize=1G
|
|
160
|
+
`);
|
|
161
|
+
|
|
162
|
+
expect(
|
|
163
|
+
isAvdCompatible({
|
|
164
|
+
emulator: {
|
|
165
|
+
type: 'emulator',
|
|
166
|
+
name: 'Pixel_8_API_35',
|
|
167
|
+
avd: {
|
|
168
|
+
apiLevel: 35,
|
|
169
|
+
profile: 'pixel_8',
|
|
170
|
+
diskSize: '1G',
|
|
171
|
+
heapSize: '512M',
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
avdConfig,
|
|
175
|
+
hostArch: 'x86_64',
|
|
176
|
+
})
|
|
177
|
+
).toMatchObject({
|
|
178
|
+
compatible: false,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('normalizes AVD cache key input with name and host arch', () => {
|
|
183
|
+
expect(
|
|
184
|
+
getNormalizedAvdCacheConfig({
|
|
185
|
+
emulator: {
|
|
186
|
+
type: 'emulator',
|
|
187
|
+
name: 'Pixel_8_API_35',
|
|
188
|
+
avd: {
|
|
189
|
+
apiLevel: 35,
|
|
190
|
+
profile: ' Pixel_8 ',
|
|
191
|
+
diskSize: '1G',
|
|
192
|
+
heapSize: '512M',
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
hostArch: 'arm64-v8a',
|
|
196
|
+
})
|
|
197
|
+
).toEqual({
|
|
198
|
+
name: 'Pixel_8_API_35',
|
|
199
|
+
apiLevel: 35,
|
|
200
|
+
arch: 'arm64-v8a',
|
|
201
|
+
profile: 'pixel_8',
|
|
202
|
+
diskSize: '1g',
|
|
203
|
+
heapSize: '512m',
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|