Skip to content

Commit 1881792

Browse files
authored
feat: make libimobiledevice optional when native crash detection is off (#94)
Mobile runners now fully disable native crash monitoring when `detectNativeCrashes` is set to `false`, including iOS simulators and Android emulators and physical devices. This keeps crash-monitor setup aligned with the runtime setting while preserving the existing default behavior of enabling native crash detection when the option is omitted.
1 parent 96021a6 commit 1881792

File tree

5 files changed

+210
-17
lines changed

5 files changed

+210
-17
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
__default__: patch
3+
---
4+
5+
Mobile runners now fully disable native crash monitoring when `detectNativeCrashes` is set to `false`, including iOS simulators and Android emulators and physical devices. This keeps crash-monitor setup aligned with the runtime setting while preserving the existing default behavior of enabling native crash detection when the option is omitted.

packages/platform-android/src/__tests__/instance.test.ts

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ import { HarnessAppPathError, HarnessEmulatorConfigError } from '../errors.js';
1818
const harnessConfig = {
1919
metroPort: DEFAULT_METRO_PORT,
2020
} as HarnessConfig;
21+
const harnessConfigWithoutNativeCrashDetection = {
22+
metroPort: DEFAULT_METRO_PORT,
23+
detectNativeCrashes: false,
24+
} as HarnessConfig;
2125
const init = {
2226
signal: new AbortController().signal,
2327
};
@@ -504,7 +508,53 @@ describe('Android platform instance', () => {
504508
).rejects.toBeInstanceOf(HarnessEmulatorConfigError);
505509
});
506510

507-
it('keeps physical device behavior unchanged', async () => {
511+
it('returns a noop emulator app monitor when native crash detection is disabled', async () => {
512+
vi.spyOn(
513+
await import('../environment.js'),
514+
'ensureAndroidEmulatorEnvironment'
515+
).mockResolvedValue('/tmp/android-sdk');
516+
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
517+
vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
518+
vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
519+
vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
520+
vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
521+
vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
522+
vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
523+
vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
524+
undefined
525+
);
526+
527+
const instance = await getAndroidEmulatorPlatformInstance(
528+
{
529+
name: 'android',
530+
device: {
531+
type: 'emulator',
532+
name: 'Pixel_8_API_35',
533+
avd: {
534+
apiLevel: 35,
535+
profile: 'pixel_8',
536+
diskSize: '1G',
537+
heapSize: '1G',
538+
},
539+
},
540+
bundleId: 'com.harnessplayground',
541+
activityName: '.MainActivity',
542+
},
543+
harnessConfigWithoutNativeCrashDetection,
544+
init
545+
);
546+
547+
const listener = vi.fn();
548+
const appMonitor = instance.createAppMonitor();
549+
550+
await expect(appMonitor.start()).resolves.toBeUndefined();
551+
await expect(appMonitor.stop()).resolves.toBeUndefined();
552+
await expect(appMonitor.dispose()).resolves.toBeUndefined();
553+
expect(appMonitor.addListener(listener)).toBeUndefined();
554+
expect(appMonitor.removeListener(listener)).toBeUndefined();
555+
});
556+
557+
it('returns a noop physical device app monitor when native crash detection is disabled', async () => {
508558
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
509559
vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
510560
manufacturer: 'motorola',
@@ -530,8 +580,31 @@ describe('Android platform instance', () => {
530580
bundleId: 'com.harnessplayground',
531581
activityName: '.MainActivity',
532582
},
533-
harnessConfig
583+
harnessConfigWithoutNativeCrashDetection
534584
)
535585
).resolves.toBeDefined();
586+
587+
const instance = await getAndroidPhysicalDevicePlatformInstance(
588+
{
589+
name: 'android-device',
590+
device: {
591+
type: 'physical',
592+
manufacturer: 'motorola',
593+
model: 'moto g72',
594+
},
595+
bundleId: 'com.harnessplayground',
596+
activityName: '.MainActivity',
597+
},
598+
harnessConfigWithoutNativeCrashDetection
599+
);
600+
601+
const listener = vi.fn();
602+
const appMonitor = instance.createAppMonitor();
603+
604+
await expect(appMonitor.start()).resolves.toBeUndefined();
605+
await expect(appMonitor.stop()).resolves.toBeUndefined();
606+
await expect(appMonitor.dispose()).resolves.toBeUndefined();
607+
expect(appMonitor.addListener(listener)).toBeUndefined();
608+
expect(appMonitor.removeListener(listener)).toBeUndefined();
536609
});
537610
});

packages/platform-android/src/instance.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,18 @@ import {
3232
} from './environment.js';
3333
import { isInteractive } from '@react-native-harness/tools';
3434
import fs from 'node:fs';
35+
import type { AppMonitor } from '@react-native-harness/platforms';
3536

3637
const androidInstanceLogger = logger.child('android-instance');
3738

39+
const createNoopAppMonitor = (): AppMonitor => ({
40+
start: async () => undefined,
41+
stop: async () => undefined,
42+
dispose: async () => undefined,
43+
addListener: () => undefined,
44+
removeListener: () => undefined,
45+
});
46+
3847
const getHarnessAppPath = (): string => {
3948
const appPath = process.env.HARNESS_APP_PATH;
4049

@@ -168,6 +177,7 @@ export const getAndroidEmulatorPlatformInstance = async (
168177
init: HarnessPlatformInitOptions
169178
): Promise<HarnessPlatformRunner> => {
170179
assertAndroidDeviceEmulator(config.device);
180+
const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
171181
const emulatorConfig = config.device;
172182
const emulatorName = emulatorConfig.name;
173183
const avdConfig = emulatorConfig.avd;
@@ -284,13 +294,18 @@ export const getAndroidEmulatorPlatformInstance = async (
284294
isAppRunning: async () => {
285295
return await adb.isAppRunning(adbId, config.bundleId);
286296
},
287-
createAppMonitor: (options?: CreateAppMonitorOptions) =>
288-
createAndroidAppMonitor({
297+
createAppMonitor: (options?: CreateAppMonitorOptions) => {
298+
if (!detectNativeCrashes) {
299+
return createNoopAppMonitor();
300+
}
301+
302+
return createAndroidAppMonitor({
289303
adbId,
290304
bundleId: config.bundleId,
291305
appUid,
292306
crashArtifactWriter: options?.crashArtifactWriter,
293-
}),
307+
});
308+
},
294309
};
295310
};
296311

@@ -299,6 +314,7 @@ export const getAndroidPhysicalDevicePlatformInstance = async (
299314
harnessConfig: HarnessConfig
300315
): Promise<HarnessPlatformRunner> => {
301316
assertAndroidDevicePhysical(config.device);
317+
const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
302318

303319
const adbId = await getAdbId(config.device);
304320

@@ -348,12 +364,17 @@ export const getAndroidPhysicalDevicePlatformInstance = async (
348364
isAppRunning: async () => {
349365
return await adb.isAppRunning(adbId, config.bundleId);
350366
},
351-
createAppMonitor: (options?: CreateAppMonitorOptions) =>
352-
createAndroidAppMonitor({
367+
createAppMonitor: (options?: CreateAppMonitorOptions) => {
368+
if (!detectNativeCrashes) {
369+
return createNoopAppMonitor();
370+
}
371+
372+
return createAndroidAppMonitor({
353373
adbId,
354374
bundleId: config.bundleId,
355375
appUid,
356376
crashArtifactWriter: options?.crashArtifactWriter,
357-
}),
377+
});
378+
},
358379
};
359380
};

packages/platform-ios/src/__tests__/instance.test.ts

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ const init = {
2222
signal: new AbortController().signal,
2323
};
2424

25+
const harnessConfigWithoutNativeCrashDetection = {
26+
metroPort: DEFAULT_METRO_PORT,
27+
detectNativeCrashes: false,
28+
} as HarnessConfig;
29+
2530
describe('iOS platform instance dependency validation', () => {
2631
beforeEach(() => {
2732
vi.restoreAllMocks();
@@ -55,7 +60,7 @@ describe('iOS platform instance dependency validation', () => {
5560
expect(assertInstalled).not.toHaveBeenCalled();
5661
});
5762

58-
it('validates libimobiledevice before creating a physical device instance', async () => {
63+
it('validates libimobiledevice before creating a physical device instance when native crash detection is enabled', async () => {
5964
const assertInstalled = vi
6065
.spyOn(libimobiledevice, 'assertLibimobiledeviceInstalled')
6166
.mockRejectedValue(new Error('missing'));
@@ -102,7 +107,7 @@ describe('iOS platform instance dependency validation', () => {
102107
expect(getSimulatorId).toHaveBeenCalled();
103108
});
104109

105-
it('does not try to discover the physical device when the dependency is missing', async () => {
110+
it('does not try to discover the physical device when the dependency is missing and native crash detection is enabled', async () => {
106111
vi.spyOn(
107112
libimobiledevice,
108113
'assertLibimobiledeviceInstalled'
@@ -121,6 +126,71 @@ describe('iOS platform instance dependency validation', () => {
121126
expect(getDeviceId).not.toHaveBeenCalled();
122127
});
123128

129+
it('skips libimobiledevice validation when native crash detection is disabled', async () => {
130+
const assertInstalled = vi
131+
.spyOn(libimobiledevice, 'assertLibimobiledeviceInstalled')
132+
.mockRejectedValue(new Error('missing'));
133+
vi.spyOn(devicectl, 'getDevice').mockResolvedValue({
134+
identifier: 'physical-device-id',
135+
deviceProperties: {
136+
name: 'My iPhone',
137+
osVersionNumber: '18.0',
138+
},
139+
hardwareProperties: {
140+
marketingName: 'iPhone',
141+
productType: 'iPhone17,1',
142+
udid: '00008140-001600222422201C',
143+
},
144+
});
145+
vi.spyOn(devicectl, 'isAppInstalled').mockResolvedValue(true);
146+
147+
const config = {
148+
name: 'ios-device',
149+
device: { type: 'physical' as const, name: 'My iPhone' },
150+
bundleId: 'com.harnessplayground',
151+
};
152+
153+
await expect(
154+
getApplePhysicalDevicePlatformInstance(
155+
config,
156+
harnessConfigWithoutNativeCrashDetection
157+
)
158+
).resolves.toBeDefined();
159+
expect(assertInstalled).not.toHaveBeenCalled();
160+
});
161+
162+
it('returns a noop simulator app monitor when native crash detection is disabled', async () => {
163+
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
164+
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true);
165+
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
166+
vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue(
167+
undefined
168+
);
169+
170+
const instance = await getAppleSimulatorPlatformInstance(
171+
{
172+
name: 'ios',
173+
device: {
174+
type: 'simulator',
175+
name: 'iPhone 16 Pro',
176+
systemVersion: '18.0',
177+
},
178+
bundleId: 'com.harnessplayground',
179+
},
180+
harnessConfigWithoutNativeCrashDetection,
181+
init
182+
);
183+
184+
const listener = vi.fn();
185+
const appMonitor = instance.createAppMonitor();
186+
187+
await expect(appMonitor.start()).resolves.toBeUndefined();
188+
await expect(appMonitor.stop()).resolves.toBeUndefined();
189+
await expect(appMonitor.dispose()).resolves.toBeUndefined();
190+
expect(appMonitor.addListener(listener)).toBeUndefined();
191+
expect(appMonitor.removeListener(listener)).toBeUndefined();
192+
});
193+
124194
it('reuses a booted simulator and does not shut it down on dispose', async () => {
125195
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
126196
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');

packages/platform-ios/src/instance.ts

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
AppMonitor,
23
AppNotInstalledError,
34
CreateAppMonitorOptions,
45
DeviceNotFoundError,
@@ -42,12 +43,21 @@ const getHarnessAppPath = (): string => {
4243
return appPath;
4344
};
4445

46+
const createNoopAppMonitor = (): AppMonitor => ({
47+
start: async () => undefined,
48+
stop: async () => undefined,
49+
dispose: async () => undefined,
50+
addListener: () => undefined,
51+
removeListener: () => undefined,
52+
});
53+
4554
export const getAppleSimulatorPlatformInstance = async (
4655
config: ApplePlatformConfig,
4756
harnessConfig: HarnessConfig,
4857
init: HarnessPlatformInitOptions
4958
): Promise<HarnessPlatformRunner> => {
5059
assertAppleDeviceSimulator(config.device);
60+
const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
5161

5262
const udid = await simctl.getSimulatorId(
5363
config.device.name,
@@ -144,12 +154,17 @@ export const getAppleSimulatorPlatformInstance = async (
144154
isAppRunning: async () => {
145155
return await simctl.isAppRunning(udid, config.bundleId);
146156
},
147-
createAppMonitor: (options?: CreateAppMonitorOptions) =>
148-
createIosSimulatorAppMonitor({
157+
createAppMonitor: (options?: CreateAppMonitorOptions) => {
158+
if (!detectNativeCrashes) {
159+
return createNoopAppMonitor();
160+
}
161+
162+
return createIosSimulatorAppMonitor({
149163
udid,
150164
bundleId: config.bundleId,
151165
crashArtifactWriter: options?.crashArtifactWriter,
152-
}),
166+
});
167+
},
153168
};
154169
};
155170

@@ -158,7 +173,11 @@ export const getApplePhysicalDevicePlatformInstance = async (
158173
harnessConfig: HarnessConfig
159174
): Promise<HarnessPlatformRunner> => {
160175
assertAppleDevicePhysical(config.device);
161-
await assertLibimobiledeviceInstalled();
176+
const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
177+
178+
if (detectNativeCrashes) {
179+
await assertLibimobiledeviceInstalled();
180+
}
162181

163182
if (harnessConfig.metroPort !== DEFAULT_METRO_PORT) {
164183
throw new Error(
@@ -211,12 +230,17 @@ export const getApplePhysicalDevicePlatformInstance = async (
211230
isAppRunning: async () => {
212231
return await devicectl.isAppRunning(deviceId, config.bundleId);
213232
},
214-
createAppMonitor: (options?: CreateAppMonitorOptions) =>
215-
createIosDeviceAppMonitor({
233+
createAppMonitor: (options?: CreateAppMonitorOptions) => {
234+
if (!detectNativeCrashes) {
235+
return createNoopAppMonitor();
236+
}
237+
238+
return createIosDeviceAppMonitor({
216239
deviceId,
217240
libimobiledeviceUdid: hardwareUdid,
218241
bundleId: config.bundleId,
219242
crashArtifactWriter: options?.crashArtifactWriter,
220-
}),
243+
});
244+
},
221245
};
222246
};

0 commit comments

Comments
 (0)