@react-native-harness/platform-android 1.2.0-rc.1 → 1.3.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-id.test.d.ts +2 -0
- package/dist/__tests__/adb-id.test.d.ts.map +1 -0
- package/dist/__tests__/adb-id.test.js +41 -0
- package/dist/__tests__/adb.test.js +56 -13
- package/dist/__tests__/app-monitor.test.js +196 -141
- package/dist/__tests__/instance.test.js +3 -72
- package/dist/adb.d.ts +2 -0
- package/dist/adb.d.ts.map +1 -1
- package/dist/adb.js +5 -1
- package/dist/app-monitor.d.ts.map +1 -1
- package/dist/app-monitor.js +2 -5
- package/dist/device-state.d.ts +6 -0
- package/dist/device-state.d.ts.map +1 -0
- package/dist/device-state.js +142 -0
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/eslint.config.mjs +1 -0
- package/package.json +5 -4
- package/src/__tests__/adb-id.test.ts +51 -0
- package/src/__tests__/instance.test.ts +9 -105
- package/src/adb.ts +15 -1
- package/src/app-monitor.ts +3 -8
- package/test/setup.ts +68 -0
- package/vite.config.ts +19 -0
- package/src/__tests__/adb.test.ts +0 -700
- package/src/__tests__/app-monitor.test.ts +0 -273
- package/src/__tests__/environment.test.ts +0 -212
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adb-id.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adb-id.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { getAdbId, isAdbIdEmulator } from '../adb-id.js';
|
|
3
|
+
import * as adb from '../adb.js';
|
|
4
|
+
describe('adb id resolution', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
vi.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
it('identifies emulator adb ids', () => {
|
|
9
|
+
expect(isAdbIdEmulator('emulator-5554')).toBe(true);
|
|
10
|
+
expect(isAdbIdEmulator('device-serial-001')).toBe(false);
|
|
11
|
+
});
|
|
12
|
+
it('skips non-emulator ids when resolving an emulator device', async () => {
|
|
13
|
+
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([
|
|
14
|
+
'device-serial-001',
|
|
15
|
+
'emulator-5554',
|
|
16
|
+
]);
|
|
17
|
+
vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Test_AVD_API_35');
|
|
18
|
+
await expect(getAdbId({
|
|
19
|
+
type: 'emulator',
|
|
20
|
+
name: 'Test_AVD_API_35',
|
|
21
|
+
})).resolves.toBe('emulator-5554');
|
|
22
|
+
});
|
|
23
|
+
it('resolves matching physical device by manufacturer and model', async () => {
|
|
24
|
+
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([
|
|
25
|
+
'device-serial-001',
|
|
26
|
+
'emulator-5554',
|
|
27
|
+
]);
|
|
28
|
+
vi.spyOn(adb, 'getDeviceInfo').mockImplementation(async (adbId) => {
|
|
29
|
+
const results = {
|
|
30
|
+
'device-serial-001': { manufacturer: 'Acme', model: 'Model A1' },
|
|
31
|
+
'emulator-5554': { manufacturer: 'Emulator', model: 'Emulator Model' },
|
|
32
|
+
};
|
|
33
|
+
return results[adbId] || null;
|
|
34
|
+
});
|
|
35
|
+
await expect(getAdbId({
|
|
36
|
+
type: 'physical',
|
|
37
|
+
manufacturer: 'acme',
|
|
38
|
+
model: 'model a1',
|
|
39
|
+
})).resolves.toBe('device-serial-001');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
2
5
|
import { PassThrough } from 'node:stream';
|
|
3
6
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
7
|
import { SubprocessError } from '@react-native-harness/tools';
|
|
5
|
-
|
|
8
|
+
vi.unmock('../adb.js');
|
|
9
|
+
import { createAvd, deleteAvd, emulatorProcess, getAppUid, getLogcatTimestamp, getStartAppArgs, hasAvd, installApp, startLogcat, startEmulator, uninstallApp, waitForBoot, waitForEmulatorDisconnect, } from '../adb.js';
|
|
6
10
|
import * as tools from '@react-native-harness/tools';
|
|
7
11
|
import * as environment from '../environment.js';
|
|
8
12
|
import * as avdConfig from '../avd-config.js';
|
|
9
13
|
const createAbortError = () => new DOMException('The operation was aborted', 'AbortError');
|
|
14
|
+
let sdkRoot;
|
|
10
15
|
const createMockChildProcess = () => {
|
|
11
16
|
const process = new EventEmitter();
|
|
12
17
|
process.stdout = new PassThrough();
|
|
@@ -16,6 +21,14 @@ const createMockChildProcess = () => {
|
|
|
16
21
|
};
|
|
17
22
|
beforeEach(() => {
|
|
18
23
|
vi.restoreAllMocks();
|
|
24
|
+
vi.unstubAllEnvs();
|
|
25
|
+
sdkRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'android-sdk-'));
|
|
26
|
+
fs.mkdirSync(path.join(sdkRoot, 'emulator'), { recursive: true });
|
|
27
|
+
fs.writeFileSync(path.join(sdkRoot, 'emulator', 'emulator'), '');
|
|
28
|
+
fs.mkdirSync(path.join(sdkRoot, 'platform-tools'), { recursive: true });
|
|
29
|
+
fs.writeFileSync(path.join(sdkRoot, 'platform-tools', 'adb'), '');
|
|
30
|
+
vi.stubEnv('ANDROID_HOME', sdkRoot);
|
|
31
|
+
vi.spyOn(environment, 'ensureAndroidEmulatorAvailable').mockResolvedValue(sdkRoot);
|
|
19
32
|
});
|
|
20
33
|
describe('getStartAppArgs', () => {
|
|
21
34
|
it('maps supported extras to adb am start flags', () => {
|
|
@@ -81,8 +94,31 @@ describe('getStartAppArgs', () => {
|
|
|
81
94
|
"+'%m-%d %H:%M:%S.000'",
|
|
82
95
|
]);
|
|
83
96
|
});
|
|
97
|
+
it('starts logcat using the resolved adb binary path', async () => {
|
|
98
|
+
const subprocess = {};
|
|
99
|
+
const spawnSpy = vi.spyOn(tools, 'spawn').mockReturnValue(subprocess);
|
|
100
|
+
expect(startLogcat('emulator-5554', [
|
|
101
|
+
'logcat',
|
|
102
|
+
'-v',
|
|
103
|
+
'threadtime',
|
|
104
|
+
'-b',
|
|
105
|
+
'crash',
|
|
106
|
+
])).toBe(subprocess);
|
|
107
|
+
expect(spawnSpy).toHaveBeenCalledWith(expect.stringMatching(/adb$/), [
|
|
108
|
+
'-s',
|
|
109
|
+
'emulator-5554',
|
|
110
|
+
'logcat',
|
|
111
|
+
'-v',
|
|
112
|
+
'threadtime',
|
|
113
|
+
'-b',
|
|
114
|
+
'crash',
|
|
115
|
+
], {
|
|
116
|
+
stdout: 'pipe',
|
|
117
|
+
stderr: 'pipe',
|
|
118
|
+
});
|
|
119
|
+
});
|
|
84
120
|
it('checks whether an AVD exists', async () => {
|
|
85
|
-
vi.spyOn(tools, 'spawn').
|
|
121
|
+
vi.spyOn(tools, 'spawn').mockResolvedValue({
|
|
86
122
|
stdout: 'Pixel_6_API_33\nPixel_8_API_35\n',
|
|
87
123
|
});
|
|
88
124
|
await expect(hasAvd('Pixel_8_API_35')).resolves.toBe(true);
|
|
@@ -365,17 +401,24 @@ describe('getStartAppArgs', () => {
|
|
|
365
401
|
});
|
|
366
402
|
it('aborts while waiting for boot completion', async () => {
|
|
367
403
|
vi.useFakeTimers();
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
404
|
+
vi.spyOn(tools, 'spawn').mockImplementation((async (_file, args) => {
|
|
405
|
+
if (args?.[0] === 'devices') {
|
|
406
|
+
return {
|
|
407
|
+
stdout: 'List of devices attached\nemulator-5554\tdevice\n',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
if (args?.includes('emu') && args?.includes('avd') && args?.includes('name')) {
|
|
411
|
+
return {
|
|
412
|
+
stdout: 'Pixel_8_API_35\n',
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (args?.includes('getprop')) {
|
|
416
|
+
return {
|
|
417
|
+
stdout: '0\n',
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
throw new Error(`Unexpected adb call: ${args?.join(' ')}`);
|
|
421
|
+
}));
|
|
379
422
|
const controller = new AbortController();
|
|
380
423
|
const waitPromise = waitForBoot('Pixel_8_API_35', controller.signal);
|
|
381
424
|
await vi.advanceTimersByTimeAsync(1000);
|
|
@@ -1,17 +1,7 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { NativeCrashError } from '@react-native-harness/platforms';
|
|
5
3
|
import { createAndroidAppMonitor, createAndroidLogEvent } from '../app-monitor.js';
|
|
6
|
-
import * as
|
|
7
|
-
import { createCrashArtifactWriter } from '@react-native-harness/tools';
|
|
8
|
-
const createMockSubprocess = () => ({
|
|
9
|
-
nodeChildProcess: Promise.resolve({
|
|
10
|
-
kill: vi.fn(),
|
|
11
|
-
}),
|
|
12
|
-
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
13
|
-
[Symbol.asyncIterator]: async function* () { },
|
|
14
|
-
});
|
|
4
|
+
import * as adb from '../adb.js';
|
|
15
5
|
const createStreamingSubprocess = (chunks) => ({
|
|
16
6
|
nodeChildProcess: Promise.resolve({
|
|
17
7
|
kill: vi.fn(),
|
|
@@ -25,178 +15,243 @@ const createStreamingSubprocess = (chunks) => ({
|
|
|
25
15
|
}
|
|
26
16
|
},
|
|
27
17
|
});
|
|
28
|
-
const artifactRoot = fs.mkdtempSync(path.join(tmpdir(), 'rn-harness-android-monitor-artifacts-'));
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
fs.rmSync(artifactRoot, { recursive: true, force: true });
|
|
31
|
-
fs.mkdirSync(artifactRoot, { recursive: true });
|
|
32
|
-
});
|
|
33
18
|
describe('createAndroidLogEvent', () => {
|
|
34
|
-
it('
|
|
35
|
-
const event = createAndroidLogEvent('
|
|
19
|
+
it('detects confirmation-grade am_crash events', () => {
|
|
20
|
+
const event = createAndroidLogEvent('05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,boom]', 'com.harnessplayground');
|
|
36
21
|
expect(event).toMatchObject({
|
|
37
|
-
type: '
|
|
38
|
-
source: 'logs',
|
|
22
|
+
type: 'crash_confirmed',
|
|
39
23
|
crashDetails: {
|
|
24
|
+
platform: 'android',
|
|
25
|
+
kind: 'java-exception',
|
|
26
|
+
confidence: 'high',
|
|
40
27
|
source: 'logs',
|
|
41
|
-
signal: 'SIGSEGV',
|
|
42
|
-
summary: '03-12 11:35:08.000 1234 1234 F libc : Fatal signal 11 (SIGSEGV), code 1 (SEGV_MAPERR) in tid 1234 (com.harnessplayground), pid 1234 (com.harnessplayground)',
|
|
43
28
|
},
|
|
44
29
|
});
|
|
45
30
|
});
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
processName: 'com.harnessplayground',
|
|
53
|
-
pid: 1234,
|
|
54
|
-
},
|
|
55
|
-
});
|
|
31
|
+
});
|
|
32
|
+
describe('createAndroidAppMonitor', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.restoreAllMocks();
|
|
35
|
+
vi.useFakeTimers();
|
|
36
|
+
vi.spyOn(adb, 'getLogcatTimestamp').mockResolvedValue('05-20 12:00:00.000');
|
|
56
37
|
});
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
return createMockSubprocess();
|
|
66
|
-
}));
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
vi.useRealTimers();
|
|
40
|
+
});
|
|
41
|
+
it('starts logcat with crash, main, system, and events buffers', async () => {
|
|
42
|
+
const startLogcat = vi
|
|
43
|
+
.spyOn(adb, 'startLogcat')
|
|
44
|
+
.mockReturnValue(createStreamingSubprocess([]));
|
|
67
45
|
const monitor = createAndroidAppMonitor({
|
|
68
46
|
adbId: 'emulator-5554',
|
|
69
47
|
bundleId: 'com.harnessplayground',
|
|
70
48
|
appUid: 10234,
|
|
49
|
+
isAppRunning: async () => true,
|
|
71
50
|
});
|
|
72
51
|
await monitor.start();
|
|
73
52
|
await monitor.stop();
|
|
74
|
-
expect(
|
|
75
|
-
'-s',
|
|
76
|
-
'emulator-5554',
|
|
53
|
+
expect(startLogcat).toHaveBeenCalledWith('emulator-5554', [
|
|
77
54
|
'logcat',
|
|
78
|
-
'-v',
|
|
79
|
-
'threadtime',
|
|
80
55
|
'-b',
|
|
81
56
|
'crash',
|
|
82
|
-
'
|
|
57
|
+
'-b',
|
|
58
|
+
'main',
|
|
59
|
+
'-b',
|
|
60
|
+
'system',
|
|
61
|
+
'-b',
|
|
62
|
+
'events',
|
|
63
|
+
'-v',
|
|
64
|
+
'threadtime',
|
|
83
65
|
'-T',
|
|
84
|
-
'
|
|
85
|
-
]
|
|
86
|
-
stdout: 'pipe',
|
|
87
|
-
stderr: 'pipe',
|
|
88
|
-
});
|
|
66
|
+
'05-20 12:00:00.000',
|
|
67
|
+
]);
|
|
89
68
|
});
|
|
90
|
-
it('
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
69
|
+
it('reports app lifecycle and crash events through the reporter', async () => {
|
|
70
|
+
vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
|
|
71
|
+
{
|
|
72
|
+
line: '05-20 12:00:00.000 1234 1234 I ActivityManager: Start proc 1234:com.harnessplayground/u0a123 for activity',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
line: '05-20 12:00:00.100 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
line: '05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,boom]',
|
|
79
|
+
},
|
|
80
|
+
]));
|
|
81
|
+
const eventReporter = vi.fn();
|
|
82
|
+
const monitor = createAndroidAppMonitor({
|
|
83
|
+
adbId: 'emulator-5554',
|
|
84
|
+
bundleId: 'com.harnessplayground',
|
|
85
|
+
appUid: 10234,
|
|
86
|
+
isAppRunning: async () => true,
|
|
87
|
+
eventReporter,
|
|
88
|
+
});
|
|
89
|
+
const crashWatch = monitor.watch('example.test.ts', 'startup');
|
|
90
|
+
crashWatch.promise.catch(() => undefined);
|
|
91
|
+
monitor.launchRequested({
|
|
92
|
+
type: 'launch_requested',
|
|
93
|
+
launchId: 'launch-1',
|
|
94
|
+
at: Date.now(),
|
|
95
|
+
reason: 'start',
|
|
96
|
+
});
|
|
97
|
+
await monitor.start();
|
|
98
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
99
|
+
expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
|
|
100
|
+
type: 'app:started',
|
|
101
|
+
appPlatform: 'android',
|
|
102
|
+
targetIdentifier: 'emulator-5554',
|
|
103
|
+
launchId: 'launch-1',
|
|
104
|
+
}));
|
|
105
|
+
expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
|
|
106
|
+
type: 'app:crash-suspected',
|
|
107
|
+
appPlatform: 'android',
|
|
108
|
+
processName: 'com.harnessplayground',
|
|
112
109
|
}));
|
|
110
|
+
expect(eventReporter).toHaveBeenCalledWith(expect.objectContaining({
|
|
111
|
+
type: 'app:crash-confirmed',
|
|
112
|
+
appPlatform: 'android',
|
|
113
|
+
processName: 'com.harnessplayground',
|
|
114
|
+
}));
|
|
115
|
+
await monitor.stop();
|
|
116
|
+
});
|
|
117
|
+
it('rejects the watch for Java crashes when am_crash arrives even if the process still looks alive', async () => {
|
|
118
|
+
vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
|
|
119
|
+
{
|
|
120
|
+
line: '05-20 12:00:00.000 1234 1234 I ActivityManager: Start proc 1234:com.harnessplayground/u0a123 for activity',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
line: '05-20 12:00:00.100 1234 1234 E AndroidRuntime: FATAL EXCEPTION: main',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
line: '05-20 12:00:00.102 1234 1234 E AndroidRuntime: java.lang.IllegalStateException: Intentional pre-RN startup crash',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
line: '05-20 12:00:00.103 1234 1234 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
line: '05-20 12:00:00.200 1000 1000 I am_crash: [0,1234,com.harnessplayground,123,java.lang.IllegalStateException,Intentional pre-RN startup crash]',
|
|
136
|
+
},
|
|
137
|
+
]));
|
|
113
138
|
const monitor = createAndroidAppMonitor({
|
|
114
139
|
adbId: 'emulator-5554',
|
|
115
140
|
bundleId: 'com.harnessplayground',
|
|
116
141
|
appUid: 10234,
|
|
142
|
+
isAppRunning: async () => true,
|
|
117
143
|
});
|
|
144
|
+
const crashWatch = monitor.watch('example.test.ts', 'startup');
|
|
145
|
+
crashWatch.promise.catch(() => undefined);
|
|
118
146
|
await monitor.start();
|
|
119
|
-
await
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
147
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
148
|
+
await expect(crashWatch.promise).rejects.toMatchObject({
|
|
149
|
+
testFilePath: 'example.test.ts',
|
|
150
|
+
details: {
|
|
151
|
+
phase: 'startup',
|
|
152
|
+
platform: 'android',
|
|
153
|
+
kind: 'java-exception',
|
|
154
|
+
confidence: 'high',
|
|
155
|
+
processName: 'com.harnessplayground',
|
|
156
|
+
pid: 1234,
|
|
157
|
+
exceptionType: 'java.lang.IllegalStateException: Intentional pre-RN startup crash',
|
|
158
|
+
},
|
|
123
159
|
});
|
|
160
|
+
await expect(crashWatch.promise).rejects.toBeInstanceOf(NativeCrashError);
|
|
124
161
|
await monitor.stop();
|
|
125
|
-
expect(details?.rawLines).toEqual([
|
|
126
|
-
'--------- beginning of crash',
|
|
127
|
-
'03-12 10:44:40.420 13861 13861 E AndroidRuntime: Process: com.harnessplayground, PID: 13861',
|
|
128
|
-
'03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
129
|
-
'03-12 10:44:40.422 13861 13861 E AndroidRuntime: at com.harnessplayground.MainActivity.onCreate(MainActivity.kt:42)',
|
|
130
|
-
]);
|
|
131
162
|
});
|
|
132
|
-
it('
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
line: '03-12 10:44:40.421 13861 13861 E AndroidRuntime: java.lang.RuntimeException: boom',
|
|
147
|
-
delayMs: 20,
|
|
148
|
-
},
|
|
149
|
-
]);
|
|
150
|
-
}));
|
|
163
|
+
it('rejects the watch when a suspected crash is followed by process exit', async () => {
|
|
164
|
+
vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
|
|
165
|
+
{
|
|
166
|
+
line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
line: '05-20 12:00:00.102 1234 1234 E AndroidRuntime: java.lang.IllegalStateException: Intentional delayed startup crash',
|
|
170
|
+
},
|
|
171
|
+
]));
|
|
172
|
+
const isAppRunning = vi
|
|
173
|
+
.fn()
|
|
174
|
+
.mockResolvedValueOnce(true)
|
|
175
|
+
.mockResolvedValueOnce(false)
|
|
176
|
+
.mockResolvedValue(false);
|
|
151
177
|
const monitor = createAndroidAppMonitor({
|
|
152
178
|
adbId: 'emulator-5554',
|
|
153
179
|
bundleId: 'com.harnessplayground',
|
|
154
180
|
appUid: 10234,
|
|
155
|
-
|
|
156
|
-
runnerName: 'android',
|
|
157
|
-
platformId: 'android',
|
|
158
|
-
rootDir: path.join(artifactRoot, '.harness', 'crash-reports'),
|
|
159
|
-
runTimestamp: '2026-03-12T11-35-08-000Z',
|
|
160
|
-
}),
|
|
181
|
+
isAppRunning,
|
|
161
182
|
});
|
|
183
|
+
const crashWatch = monitor.watch('example.test.ts', 'execution');
|
|
184
|
+
crashWatch.promise.catch(() => undefined);
|
|
162
185
|
await monitor.start();
|
|
163
|
-
await
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
186
|
+
await vi.advanceTimersByTimeAsync(350);
|
|
187
|
+
await expect(crashWatch.promise).rejects.toMatchObject({
|
|
188
|
+
details: {
|
|
189
|
+
phase: 'execution',
|
|
190
|
+
platform: 'android',
|
|
191
|
+
kind: 'java-exception',
|
|
192
|
+
processName: 'com.harnessplayground',
|
|
193
|
+
pid: 1234,
|
|
194
|
+
},
|
|
167
195
|
});
|
|
168
196
|
await monitor.stop();
|
|
169
|
-
expect(details?.artifactPath).toContain('/.harness/crash-reports/');
|
|
170
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
171
|
-
expect(fs.readFileSync(details.artifactPath, 'utf8')).toContain('RuntimeException: boom');
|
|
172
197
|
});
|
|
173
|
-
it('
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
calledArgs.includes('date')).length === 1) {
|
|
181
|
-
throw timestampError;
|
|
182
|
-
}
|
|
183
|
-
return {
|
|
184
|
-
stdout: '03-12 11:35:08.000\n',
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
return createMockSubprocess();
|
|
188
|
-
}));
|
|
198
|
+
it('does not reject the watch when the process exits without a recent crash signal', async () => {
|
|
199
|
+
vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([]));
|
|
200
|
+
const isAppRunning = vi
|
|
201
|
+
.fn()
|
|
202
|
+
.mockResolvedValueOnce(true)
|
|
203
|
+
.mockResolvedValueOnce(false)
|
|
204
|
+
.mockResolvedValue(false);
|
|
189
205
|
const monitor = createAndroidAppMonitor({
|
|
190
206
|
adbId: 'emulator-5554',
|
|
191
207
|
bundleId: 'com.harnessplayground',
|
|
192
208
|
appUid: 10234,
|
|
209
|
+
isAppRunning,
|
|
210
|
+
});
|
|
211
|
+
const crashWatch = monitor.watch('example.test.ts', 'execution');
|
|
212
|
+
let settled = false;
|
|
213
|
+
const watchHandled = crashWatch.promise.catch(() => {
|
|
214
|
+
settled = true;
|
|
215
|
+
});
|
|
216
|
+
await monitor.start();
|
|
217
|
+
await vi.advanceTimersByTimeAsync(600);
|
|
218
|
+
expect(settled).toBe(false);
|
|
219
|
+
crashWatch.cancel();
|
|
220
|
+
await monitor.stop();
|
|
221
|
+
await watchHandled;
|
|
222
|
+
});
|
|
223
|
+
it('suppresses process-exit confirmation during a controlled stop window', async () => {
|
|
224
|
+
vi.spyOn(adb, 'startLogcat').mockReturnValue(createStreamingSubprocess([
|
|
225
|
+
{
|
|
226
|
+
line: '05-20 12:00:00.101 1234 1234 E AndroidRuntime: Process: com.harnessplayground, PID: 1234',
|
|
227
|
+
},
|
|
228
|
+
]));
|
|
229
|
+
const isAppRunning = vi
|
|
230
|
+
.fn()
|
|
231
|
+
.mockResolvedValueOnce(true)
|
|
232
|
+
.mockResolvedValueOnce(false)
|
|
233
|
+
.mockResolvedValue(false);
|
|
234
|
+
const monitor = createAndroidAppMonitor({
|
|
235
|
+
adbId: 'emulator-5554',
|
|
236
|
+
bundleId: 'com.harnessplayground',
|
|
237
|
+
appUid: 10234,
|
|
238
|
+
isAppRunning,
|
|
239
|
+
});
|
|
240
|
+
const crashWatch = monitor.watch('example.test.ts', 'execution');
|
|
241
|
+
let settled = false;
|
|
242
|
+
const watchHandled = crashWatch.promise.catch(() => {
|
|
243
|
+
settled = true;
|
|
244
|
+
});
|
|
245
|
+
await monitor.start();
|
|
246
|
+
monitor.stopRequested({
|
|
247
|
+
type: 'stop_requested',
|
|
248
|
+
at: Date.now(),
|
|
249
|
+
reason: 'manual',
|
|
193
250
|
});
|
|
194
|
-
await
|
|
195
|
-
|
|
251
|
+
await vi.advanceTimersByTimeAsync(400);
|
|
252
|
+
expect(settled).toBe(false);
|
|
253
|
+
crashWatch.cancel();
|
|
196
254
|
await monitor.stop();
|
|
197
|
-
|
|
198
|
-
Array.isArray(args) &&
|
|
199
|
-
args.includes('logcat') &&
|
|
200
|
-
args.includes('--uid=10234'))).toBe(true);
|
|
255
|
+
await watchHandled;
|
|
201
256
|
});
|
|
202
257
|
});
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { DEFAULT_METRO_PORT, } from '@react-native-harness/config';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
import path from 'node:path';
|
|
6
3
|
import { getAndroidEmulatorPlatformInstance, getAndroidPhysicalDevicePlatformInstance, } from '../instance.js';
|
|
7
4
|
import * as adb from '../adb.js';
|
|
8
5
|
import * as avdConfig from '../avd-config.js';
|
|
@@ -19,9 +16,10 @@ const init = {
|
|
|
19
16
|
signal: new AbortController().signal,
|
|
20
17
|
};
|
|
21
18
|
describe('Android platform instance', () => {
|
|
22
|
-
beforeEach(() => {
|
|
19
|
+
beforeEach(async () => {
|
|
23
20
|
vi.restoreAllMocks();
|
|
24
21
|
vi.unstubAllEnvs();
|
|
22
|
+
vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorAvailable').mockResolvedValue('/tmp/android-sdk');
|
|
25
23
|
});
|
|
26
24
|
it('reuses a running emulator and does not shut it down on dispose', async () => {
|
|
27
25
|
const ensureAndroidEmulatorEnvironment = vi
|
|
@@ -137,6 +135,7 @@ describe('Android platform instance', () => {
|
|
|
137
135
|
});
|
|
138
136
|
it('reuses a compatible cached AVD snapshot when caching is enabled', async () => {
|
|
139
137
|
vi.stubEnv('HARNESS_AVD_CACHING', 'true');
|
|
138
|
+
vi.spyOn(await import('../environment.js'), 'getHostAndroidSystemImageArch').mockReturnValue('arm64-v8a');
|
|
140
139
|
vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
|
|
141
140
|
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue([]);
|
|
142
141
|
vi.spyOn(adb, 'hasAvd').mockResolvedValue(true);
|
|
@@ -261,74 +260,6 @@ describe('Android platform instance', () => {
|
|
|
261
260
|
expect(waitForEmulatorDisconnect).toHaveBeenCalledWith('emulator-5554', init.signal);
|
|
262
261
|
expect(startEmulator).toHaveBeenNthCalledWith(2, 'Pixel_8_API_35', 'snapshot-reuse');
|
|
263
262
|
});
|
|
264
|
-
it('installs the app from HARNESS_APP_PATH when missing', async () => {
|
|
265
|
-
const appPath = path.join(os.tmpdir(), 'HarnessPlayground.apk');
|
|
266
|
-
fs.writeFileSync(appPath, 'apk');
|
|
267
|
-
vi.stubEnv('HARNESS_APP_PATH', appPath);
|
|
268
|
-
vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
|
|
269
|
-
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
|
|
270
|
-
vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
|
|
271
|
-
vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
|
|
272
|
-
vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(false);
|
|
273
|
-
const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined);
|
|
274
|
-
vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
|
|
275
|
-
vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
|
|
276
|
-
vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
|
|
277
|
-
vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
|
|
278
|
-
await expect(getAndroidEmulatorPlatformInstance({
|
|
279
|
-
name: 'android',
|
|
280
|
-
device: {
|
|
281
|
-
type: 'emulator',
|
|
282
|
-
name: 'Pixel_8_API_35',
|
|
283
|
-
avd: {
|
|
284
|
-
apiLevel: 35,
|
|
285
|
-
profile: 'pixel_8',
|
|
286
|
-
diskSize: '1G',
|
|
287
|
-
heapSize: '1G',
|
|
288
|
-
},
|
|
289
|
-
},
|
|
290
|
-
bundleId: 'com.harnessplayground',
|
|
291
|
-
activityName: '.MainActivity',
|
|
292
|
-
}, harnessConfig, init)).resolves.toBeDefined();
|
|
293
|
-
expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
|
|
294
|
-
fs.rmSync(appPath, { force: true });
|
|
295
|
-
});
|
|
296
|
-
it('reinstalls the app from HARNESS_APP_PATH when already installed', async () => {
|
|
297
|
-
const appPath = path.join(os.tmpdir(), 'HarnessPlayground-installed.apk');
|
|
298
|
-
fs.writeFileSync(appPath, 'apk');
|
|
299
|
-
vi.stubEnv('HARNESS_APP_PATH', appPath);
|
|
300
|
-
vi.spyOn(await import('../environment.js'), 'ensureAndroidEmulatorEnvironment').mockResolvedValue('/tmp/android-sdk');
|
|
301
|
-
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
|
|
302
|
-
vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
|
|
303
|
-
vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
|
|
304
|
-
vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
|
|
305
|
-
const uninstallApp = vi
|
|
306
|
-
.spyOn(adb, 'uninstallApp')
|
|
307
|
-
.mockResolvedValue(undefined);
|
|
308
|
-
const installApp = vi.spyOn(adb, 'installApp').mockResolvedValue(undefined);
|
|
309
|
-
vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
|
|
310
|
-
vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
|
|
311
|
-
vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
|
|
312
|
-
vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(undefined);
|
|
313
|
-
await expect(getAndroidEmulatorPlatformInstance({
|
|
314
|
-
name: 'android',
|
|
315
|
-
device: {
|
|
316
|
-
type: 'emulator',
|
|
317
|
-
name: 'Pixel_8_API_35',
|
|
318
|
-
avd: {
|
|
319
|
-
apiLevel: 35,
|
|
320
|
-
profile: 'pixel_8',
|
|
321
|
-
diskSize: '1G',
|
|
322
|
-
heapSize: '1G',
|
|
323
|
-
},
|
|
324
|
-
},
|
|
325
|
-
bundleId: 'com.harnessplayground',
|
|
326
|
-
activityName: '.MainActivity',
|
|
327
|
-
}, harnessConfig, init)).resolves.toBeDefined();
|
|
328
|
-
expect(uninstallApp).toHaveBeenCalledWith('emulator-5554', 'com.harnessplayground');
|
|
329
|
-
expect(installApp).toHaveBeenCalledWith('emulator-5554', appPath);
|
|
330
|
-
fs.rmSync(appPath, { force: true });
|
|
331
|
-
});
|
|
332
263
|
it('throws a HarnessAppPathError when HARNESS_APP_PATH is missing', async () => {
|
|
333
264
|
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
|
|
334
265
|
vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
|