@react-native-harness/platform-android 1.3.0 → 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.
Files changed (62) hide show
  1. package/dist/__tests__/adb-wait.test.d.ts +2 -0
  2. package/dist/__tests__/adb-wait.test.d.ts.map +1 -0
  3. package/dist/__tests__/adb-wait.test.js +69 -0
  4. package/dist/__tests__/app-session.test.d.ts +2 -0
  5. package/dist/__tests__/app-session.test.d.ts.map +1 -0
  6. package/dist/__tests__/app-session.test.js +57 -0
  7. package/dist/__tests__/avd-config.test.js +58 -0
  8. package/dist/__tests__/crash-diagnostics.test.d.ts +2 -0
  9. package/dist/__tests__/crash-diagnostics.test.d.ts.map +1 -0
  10. package/dist/__tests__/crash-diagnostics.test.js +119 -0
  11. package/dist/__tests__/crash-reporter.test.d.ts +2 -0
  12. package/dist/__tests__/crash-reporter.test.d.ts.map +1 -0
  13. package/dist/__tests__/crash-reporter.test.js +150 -0
  14. package/dist/__tests__/emulator-startup.test.js +30 -3
  15. package/dist/__tests__/instance.test.js +139 -24
  16. package/dist/adb.d.ts +7 -1
  17. package/dist/adb.d.ts.map +1 -1
  18. package/dist/adb.js +72 -23
  19. package/dist/app-session.d.ts +26 -0
  20. package/dist/app-session.d.ts.map +1 -0
  21. package/dist/app-session.js +214 -0
  22. package/dist/avd-config.d.ts +1 -0
  23. package/dist/avd-config.d.ts.map +1 -1
  24. package/dist/avd-config.js +11 -0
  25. package/dist/crash-diagnostics.d.ts +40 -0
  26. package/dist/crash-diagnostics.d.ts.map +1 -0
  27. package/dist/crash-diagnostics.js +256 -0
  28. package/dist/crash-reporter.d.ts +12 -0
  29. package/dist/crash-reporter.d.ts.map +1 -0
  30. package/dist/crash-reporter.js +150 -0
  31. package/dist/emulator-startup.d.ts +23 -0
  32. package/dist/emulator-startup.d.ts.map +1 -1
  33. package/dist/emulator-startup.js +58 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -0
  37. package/dist/instance.d.ts +1 -1
  38. package/dist/instance.d.ts.map +1 -1
  39. package/dist/instance.js +51 -72
  40. package/dist/runner.d.ts +2 -2
  41. package/dist/runner.d.ts.map +1 -1
  42. package/dist/runner.js +1 -1
  43. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  44. package/package.json +4 -4
  45. package/src/__tests__/adb-wait.test.ts +95 -0
  46. package/src/__tests__/app-session.test.ts +76 -0
  47. package/src/__tests__/avd-config.test.ts +70 -0
  48. package/src/__tests__/crash-diagnostics.test.ts +144 -0
  49. package/src/__tests__/crash-reporter.test.ts +167 -0
  50. package/src/__tests__/emulator-startup.test.ts +53 -6
  51. package/src/__tests__/instance.test.ts +226 -84
  52. package/src/adb.ts +129 -63
  53. package/src/app-session.ts +316 -0
  54. package/src/avd-config.ts +17 -0
  55. package/src/crash-diagnostics.ts +471 -0
  56. package/src/crash-reporter.ts +263 -0
  57. package/src/emulator-startup.ts +74 -0
  58. package/src/index.ts +1 -0
  59. package/src/instance.ts +84 -105
  60. package/src/runner.ts +11 -11
  61. package/test/setup.ts +1 -0
  62. package/src/app-monitor.ts +0 -577
@@ -6,6 +6,9 @@ import {
6
6
  resolveAvdCachingEnabled,
7
7
  } from '../avd-config.js';
8
8
  import { AndroidPlatformConfigSchema } from '../config.js';
9
+ import { getEmulatorCpuCores } from '../emulator-startup.js';
10
+
11
+ const emulatorCpuCores = getEmulatorCpuCores();
9
12
 
10
13
  describe('AVD config helpers', () => {
11
14
  it('parses snapshot config from Android schema', () => {
@@ -73,8 +76,11 @@ abi.type=x86_64
73
76
  hw.device.name=pixel_8
74
77
  disk.dataPartition.size=1G
75
78
  vm.heapSize=512M
79
+ hw.cpu.ncore=${emulatorCpuCores}
76
80
  `);
77
81
 
82
+ expect(avdConfig.hwCpuNcore).toBe(String(emulatorCpuCores));
83
+
78
84
  expect(
79
85
  isAvdCompatible({
80
86
  emulator: {
@@ -93,6 +99,69 @@ vm.heapSize=512M
93
99
  ).toEqual({ compatible: true });
94
100
  });
95
101
 
102
+ it('rejects AVDs missing hw.cpu.ncore', () => {
103
+ const avdConfig = parseAvdConfig(`
104
+ image.sysdir.1=system-images/android-35/default/x86_64/
105
+ abi.type=x86_64
106
+ hw.device.name=pixel_8
107
+ disk.dataPartition.size=1G
108
+ vm.heapSize=512M
109
+ `);
110
+
111
+ expect(
112
+ isAvdCompatible({
113
+ emulator: {
114
+ type: 'emulator',
115
+ name: 'Pixel_8_API_35',
116
+ avd: {
117
+ apiLevel: 35,
118
+ profile: 'pixel_8',
119
+ diskSize: '1G',
120
+ heapSize: '512M',
121
+ },
122
+ },
123
+ avdConfig,
124
+ hostArch: 'x86_64',
125
+ })
126
+ ).toMatchObject({
127
+ compatible: false,
128
+ reason: `CPU core count mismatch: expected ${emulatorCpuCores}, got missing.`,
129
+ });
130
+ });
131
+
132
+ it('rejects AVDs with a different hw.cpu.ncore', () => {
133
+ const avdConfig = parseAvdConfig(`
134
+ image.sysdir.1=system-images/android-35/default/x86_64/
135
+ abi.type=x86_64
136
+ hw.device.name=pixel_8
137
+ disk.dataPartition.size=1G
138
+ vm.heapSize=512M
139
+ hw.cpu.ncore=${emulatorCpuCores === 4 ? 3 : 4}
140
+ `);
141
+
142
+ expect(
143
+ isAvdCompatible({
144
+ emulator: {
145
+ type: 'emulator',
146
+ name: 'Pixel_8_API_35',
147
+ avd: {
148
+ apiLevel: 35,
149
+ profile: 'pixel_8',
150
+ diskSize: '1G',
151
+ heapSize: '512M',
152
+ },
153
+ },
154
+ avdConfig,
155
+ hostArch: 'x86_64',
156
+ })
157
+ ).toMatchObject({
158
+ compatible: false,
159
+ reason: `CPU core count mismatch: expected ${emulatorCpuCores}, got ${
160
+ emulatorCpuCores === 4 ? 3 : 4
161
+ }.`,
162
+ });
163
+ });
164
+
96
165
  it('accepts disk partition sizes rewritten to bytes', () => {
97
166
  const avdConfig = parseAvdConfig(`
98
167
  image.sysdir.1=system-images/android-35/default/x86_64/
@@ -100,6 +169,7 @@ abi.type=x86_64
100
169
  hw.device.name=pixel_8
101
170
  disk.dataPartition.size=6442450944
102
171
  vm.heapSize=512M
172
+ hw.cpu.ncore=${emulatorCpuCores}
103
173
  `);
104
174
 
105
175
  expect(
@@ -0,0 +1,144 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ collectDropboxArtifacts,
4
+ filterExitInfo,
5
+ getBestDropboxArtifact,
6
+ parseDropboxOutput,
7
+ } from '../crash-diagnostics.js';
8
+
9
+ const javaCrashDropbox = `
10
+ Drop box contents: 1 entries
11
+ Max entries: 1000
12
+ ========================================
13
+ 2026-03-12 10:30:45.123 data_app_crash (text, 500 bytes)
14
+ Process: com.harnessplayground
15
+ PID: 7777
16
+ Package: com.harnessplayground v1 (1.0)
17
+ java.lang.RuntimeException: boom
18
+ \tat com.harnessplayground.MainActivity.onCreate(MainActivity.kt:38)
19
+ `;
20
+
21
+ const nativeCrashDropbox = `
22
+ ========================================
23
+ 2026-03-12 10:31:10.456 data_app_native_crash (text, 800 bytes)
24
+ Process: com.harnessplayground
25
+ PID: 8888
26
+ signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xdeadbeef
27
+ Abort message: 'JNI DETECTED ERROR IN APPLICATION'
28
+ backtrace:
29
+ #00 pc 00001234 /data/app/lib/libapp.so
30
+ `;
31
+
32
+ const deviceJavaCrashDropbox = `
33
+ Drop box contents: 8 entries
34
+ Searching for: data_app_crash
35
+
36
+ ========================================
37
+ 2026-05-29 10:22:34 data_app_crash (text, 1328 bytes)
38
+ SystemUptimeMs: 79467172
39
+ Process: com.harnessplayground
40
+ PID: 586
41
+ Package: com.harnessplayground v1 (1.0)
42
+ java.lang.RuntimeException: Intentional asynchronous Kotlin crash
43
+ \tat com.harnessplayground.PlaygroundCrashModule.crashFromKotlinAsync(PlaygroundCrashModule.kt:44)
44
+ `;
45
+
46
+ describe('collectDropboxArtifacts', () => {
47
+ it('matches harness playground crashes from merged per-tag dropbox output', async () => {
48
+ const artifacts = await collectDropboxArtifacts({
49
+ bundleId: 'com.harnessplayground',
50
+ getDropboxOutput: async () => deviceJavaCrashDropbox,
51
+ occurredAt: Date.now(),
52
+ pid: 586,
53
+ });
54
+
55
+ expect(artifacts).toHaveLength(1);
56
+ expect(artifacts[0]).toMatchObject({
57
+ artifactType: 'dropbox-crash',
58
+ pid: 586,
59
+ processName: 'com.harnessplayground',
60
+ });
61
+ });
62
+
63
+ it('ignores Dropbox entries older than the session start time', async () => {
64
+ const artifacts = await collectDropboxArtifacts({
65
+ bundleId: 'com.harnessplayground',
66
+ getDropboxOutput: async () => deviceJavaCrashDropbox,
67
+ minOccurredAt: Date.parse('2026-05-29T10:22:35.000Z'),
68
+ occurredAt: Date.parse('2026-05-29T10:22:36.000Z'),
69
+ });
70
+
71
+ expect(artifacts).toHaveLength(0);
72
+ });
73
+ });
74
+
75
+ describe('parseDropboxOutput', () => {
76
+ it('parses java and native dropbox entries', () => {
77
+ const entries = parseDropboxOutput(`${javaCrashDropbox}\n${nativeCrashDropbox}`);
78
+
79
+ expect(entries).toHaveLength(2);
80
+ expect(entries[0]).toMatchObject({
81
+ tag: 'data_app_crash',
82
+ pid: 7777,
83
+ });
84
+ expect(entries[1]).toMatchObject({
85
+ tag: 'data_app_native_crash',
86
+ pid: 8888,
87
+ });
88
+ expect(entries[1]?.content).toContain('SIGSEGV');
89
+ });
90
+ });
91
+
92
+ describe('getBestDropboxArtifact', () => {
93
+ it('prefers the highest-scored artifact', () => {
94
+ const entries = parseDropboxOutput(`${javaCrashDropbox}\n${nativeCrashDropbox}`);
95
+ const artifacts = entries.map((entry, index) => ({
96
+ artifactType:
97
+ entry.tag === 'data_app_native_crash'
98
+ ? ('dropbox-native-crash' as const)
99
+ : ('dropbox-crash' as const),
100
+ dropboxTag: entry.tag,
101
+ occurredAt: Date.now(),
102
+ score: index === 0 ? 100 : 300,
103
+ summary: entry.content,
104
+ pid: entry.pid,
105
+ processName: 'com.harnessplayground',
106
+ }));
107
+
108
+ expect(getBestDropboxArtifact(artifacts)?.dropboxTag).toBe(
109
+ 'data_app_native_crash'
110
+ );
111
+ });
112
+ });
113
+
114
+ describe('filterExitInfo', () => {
115
+ it('returns matching exit-info records for the target package and pid', () => {
116
+ const output = `
117
+ ApplicationExitInfo #0:
118
+ package=com.harnessplayground pid=7777 realUid=10123
119
+ reason=4 (APP CRASH(NATIVE))
120
+ timestamp=2026-03-12 10:31:10.456
121
+ ApplicationExitInfo #1:
122
+ package=com.other.app pid=9999 realUid=10199
123
+ reason=10 (USER REQUESTED)
124
+ `;
125
+
126
+ const filtered = filterExitInfo({
127
+ output,
128
+ bundleId: 'com.harnessplayground',
129
+ pid: 7777,
130
+ });
131
+
132
+ expect(filtered).toContain('com.harnessplayground pid=7777');
133
+ expect(filtered).not.toContain('com.other.app');
134
+ });
135
+
136
+ it('returns null when no exit-info records exist', () => {
137
+ expect(
138
+ filterExitInfo({
139
+ output: 'No exit info records for com.harnessplayground',
140
+ bundleId: 'com.harnessplayground',
141
+ })
142
+ ).toBeNull();
143
+ });
144
+ });
@@ -0,0 +1,167 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+ import { createAndroidCrashReporter } from '../crash-reporter.js';
3
+
4
+ const javaCrashDropbox = `
5
+ ========================================
6
+ 2026-03-12 10:30:45.123 data_app_crash (text, 500 bytes)
7
+ Process: com.harnessplayground
8
+ PID: 7777
9
+ Package: com.harnessplayground v1 (1.0)
10
+ java.lang.RuntimeException: boom
11
+ \tat com.harnessplayground.MainActivity.onCreate(MainActivity.kt:38)
12
+ `;
13
+
14
+ const nativeCrashDropbox = `
15
+ ========================================
16
+ 2026-03-12 10:31:10.456 data_app_native_crash (text, 800 bytes)
17
+ Process: com.harnessplayground
18
+ PID: 8888
19
+ signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0xdeadbeef
20
+ Abort message: 'JNI DETECTED ERROR IN APPLICATION'
21
+ backtrace:
22
+ #00 pc 00001234 /data/app/lib/libapp.so
23
+ `;
24
+
25
+ describe('createAndroidCrashReporter', () => {
26
+ afterEach(() => {
27
+ vi.useRealTimers();
28
+ });
29
+
30
+ it('persists the current logcat crash block with the test file path', async () => {
31
+ vi.useFakeTimers();
32
+ const persistArtifact = vi.fn(
33
+ () => '/tmp/.harness/crash-reports/logcat.txt'
34
+ );
35
+ const reporter = createAndroidCrashReporter({
36
+ bundleId: 'com.harnessplayground',
37
+ crashArtifactWriter: {
38
+ runTimestamp: '2026-03-12T11-35-08-000Z',
39
+ persistArtifact,
40
+ },
41
+ getLogs: () => [
42
+ { line: 'ordinary crash-buffer line', occurredAt: 1_000 },
43
+ { line: '--------- beginning of crash', occurredAt: 1_100 },
44
+ {
45
+ line: 'Process: com.harnessplayground, PID: 7777',
46
+ occurredAt: 1_100,
47
+ },
48
+ { line: 'FATAL EXCEPTION: main', occurredAt: 1_100 },
49
+ ],
50
+ });
51
+
52
+ const detailsPromise = reporter.getCrashDetails({
53
+ occurredAt: 1_100,
54
+ pid: 7777,
55
+ testFilePath: '/test/native-crash.test.ts',
56
+ });
57
+ await vi.advanceTimersByTimeAsync(100);
58
+ const details = await detailsPromise;
59
+
60
+ expect(details).toMatchObject({
61
+ artifactType: 'logcat',
62
+ artifactPath: '/tmp/.harness/crash-reports/logcat.txt',
63
+ pid: 7777,
64
+ processName: 'com.harnessplayground',
65
+ });
66
+ expect(persistArtifact).toHaveBeenCalledWith({
67
+ artifactKind: 'logcat',
68
+ testFilePath: '/test/native-crash.test.ts',
69
+ source: {
70
+ kind: 'text',
71
+ fileName: 'logcat.txt',
72
+ text: [
73
+ '--------- beginning of crash',
74
+ 'Process: com.harnessplayground, PID: 7777',
75
+ 'FATAL EXCEPTION: main',
76
+ '',
77
+ ].join('\n'),
78
+ },
79
+ });
80
+ });
81
+
82
+ it('adds dropbox artifacts as enrichment when logcat evidence is available', async () => {
83
+ vi.useFakeTimers();
84
+ const persistArtifact = vi
85
+ .fn()
86
+ .mockImplementation(({ artifactKind }: { artifactKind: string }) => {
87
+ return `/tmp/.harness/crash-reports/${artifactKind}.txt`;
88
+ });
89
+ const reporter = createAndroidCrashReporter({
90
+ bundleId: 'com.harnessplayground',
91
+ crashArtifactWriter: {
92
+ runTimestamp: '2026-03-12T11-35-08-000Z',
93
+ persistArtifact,
94
+ },
95
+ getLogs: () => [
96
+ { line: '--------- beginning of crash', occurredAt: 1_100 },
97
+ {
98
+ line: 'Process: com.harnessplayground, PID: 7777',
99
+ occurredAt: 1_100,
100
+ },
101
+ { line: 'FATAL EXCEPTION: main', occurredAt: 1_100 },
102
+ ],
103
+ getDropboxOutput: async () => javaCrashDropbox,
104
+ getExitInfo: async () =>
105
+ 'ApplicationExitInfo #0:\n package=com.harnessplayground pid=7777 reason=4 (APP CRASH(NATIVE))',
106
+ });
107
+
108
+ const detailsPromise = reporter.getCrashDetails({
109
+ occurredAt: 1_100,
110
+ pid: 7777,
111
+ testFilePath: '/test/native-crash.test.ts',
112
+ });
113
+ await vi.advanceTimersByTimeAsync(100);
114
+ const details = await detailsPromise;
115
+
116
+ expect(details).toMatchObject({
117
+ artifactType: 'logcat',
118
+ artifactPath: '/tmp/.harness/crash-reports/logcat.txt',
119
+ });
120
+ expect(details?.enrichmentArtifacts).toEqual(
121
+ expect.arrayContaining([
122
+ {
123
+ artifactType: 'dropbox-crash',
124
+ artifactPath: '/tmp/.harness/crash-reports/dropbox-crash.txt',
125
+ },
126
+ {
127
+ artifactType: 'exit-info',
128
+ artifactPath: '/tmp/.harness/crash-reports/exit-info.txt',
129
+ },
130
+ ])
131
+ );
132
+ });
133
+
134
+ it('falls back to native dropbox evidence when logcat is empty', async () => {
135
+ vi.useFakeTimers();
136
+ const persistArtifact = vi
137
+ .fn()
138
+ .mockImplementation(({ artifactKind }: { artifactKind: string }) => {
139
+ return `/tmp/.harness/crash-reports/${artifactKind}.txt`;
140
+ });
141
+ const reporter = createAndroidCrashReporter({
142
+ bundleId: 'com.harnessplayground',
143
+ crashArtifactWriter: {
144
+ runTimestamp: '2026-03-12T11-35-08-000Z',
145
+ persistArtifact,
146
+ },
147
+ getLogs: () => [],
148
+ getDropboxOutput: async () => nativeCrashDropbox,
149
+ });
150
+
151
+ const detailsPromise = reporter.getCrashDetails({
152
+ occurredAt: 1_100,
153
+ pid: 8888,
154
+ testFilePath: '/test/native-crash.test.ts',
155
+ });
156
+ await vi.advanceTimersByTimeAsync(100);
157
+ const details = await detailsPromise;
158
+
159
+ expect(details).toMatchObject({
160
+ artifactType: 'dropbox-native-crash',
161
+ artifactPath: '/tmp/.harness/crash-reports/dropbox-native-crash.txt',
162
+ pid: 8888,
163
+ signal: 'SIGSEGV',
164
+ processName: 'com.harnessplayground',
165
+ });
166
+ });
167
+ });
@@ -1,5 +1,19 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { getEmulatorStartupArgs } from '../emulator-startup.js';
2
+ import {
3
+ getEmulatorCpuCores,
4
+ getEmulatorStartupArgs,
5
+ isAccelerationUsable,
6
+ } from '../emulator-startup.js';
7
+ import { getDeviceCoreBudget } from '@react-native-harness/tools';
8
+ import os from 'node:os';
9
+
10
+ describe('getEmulatorCpuCores', () => {
11
+ it('uses the shared device-core budget for the current host', () => {
12
+ expect(getEmulatorCpuCores()).toBe(
13
+ getDeviceCoreBudget(os.availableParallelism())
14
+ );
15
+ });
16
+ });
3
17
 
4
18
  describe('emulator startup modes', () => {
5
19
  it('builds default boot args', () => {
@@ -8,17 +22,24 @@ describe('emulator startup modes', () => {
8
22
  '@Pixel_8_API_35',
9
23
  '-no-snapshot-load',
10
24
  '-no-snapshot-save',
25
+ '-no-metrics',
11
26
  ])
12
27
  );
13
- expect(getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')).not.toEqual(
14
- expect.arrayContaining(['-camera-back', 'none'])
15
- );
28
+ expect(
29
+ getEmulatorStartupArgs('Pixel_8_API_35', 'default-boot')
30
+ ).not.toEqual(expect.arrayContaining(['-camera-back', 'none']));
16
31
  });
17
32
 
18
33
  it('builds clean snapshot generation args', () => {
19
34
  expect(
20
35
  getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')
21
- ).toEqual(expect.arrayContaining(['@Pixel_8_API_35', '-no-snapshot-load']));
36
+ ).toEqual(
37
+ expect.arrayContaining([
38
+ '@Pixel_8_API_35',
39
+ '-no-snapshot-load',
40
+ '-no-metrics',
41
+ ])
42
+ );
22
43
  expect(
23
44
  getEmulatorStartupArgs('Pixel_8_API_35', 'clean-snapshot-generation')
24
45
  ).not.toContain('-no-snapshot-save');
@@ -26,10 +47,36 @@ describe('emulator startup modes', () => {
26
47
 
27
48
  it('builds snapshot reuse args', () => {
28
49
  expect(getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')).toEqual(
29
- expect.arrayContaining(['@Pixel_8_API_35', '-no-snapshot-save'])
50
+ expect.arrayContaining([
51
+ '@Pixel_8_API_35',
52
+ '-no-snapshot-save',
53
+ '-no-metrics',
54
+ ])
30
55
  );
31
56
  expect(
32
57
  getEmulatorStartupArgs('Pixel_8_API_35', 'snapshot-reuse')
33
58
  ).not.toContain('-no-snapshot-load');
34
59
  });
35
60
  });
61
+
62
+ describe('isAccelerationUsable', () => {
63
+ it('returns true when the emulator reports acceleration is usable', () => {
64
+ expect(
65
+ isAccelerationUsable(
66
+ 'accel:\n0\nKVM (version 12) is installed and usable.\naccel\n'
67
+ )
68
+ ).toBe(true);
69
+ });
70
+
71
+ it('returns false when acceleration is disabled', () => {
72
+ expect(
73
+ isAccelerationUsable(
74
+ 'accel:\n0\nKVM is not installed on this machine.\naccel\n'
75
+ )
76
+ ).toBe(false);
77
+ });
78
+
79
+ it('returns false for empty output', () => {
80
+ expect(isAccelerationUsable('')).toBe(false);
81
+ });
82
+ });