Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .nx/version-plans/disable-native-crash-monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
__default__: patch
---

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.
77 changes: 75 additions & 2 deletions packages/platform-android/src/__tests__/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { HarnessAppPathError, HarnessEmulatorConfigError } from '../errors.js';
const harnessConfig = {
metroPort: DEFAULT_METRO_PORT,
} as HarnessConfig;
const harnessConfigWithoutNativeCrashDetection = {
metroPort: DEFAULT_METRO_PORT,
detectNativeCrashes: false,
} as HarnessConfig;
const init = {
signal: new AbortController().signal,
};
Expand Down Expand Up @@ -504,7 +508,53 @@ describe('Android platform instance', () => {
).rejects.toBeInstanceOf(HarnessEmulatorConfigError);
});

it('keeps physical device behavior unchanged', async () => {
it('returns a noop emulator app monitor when native crash detection is disabled', async () => {
vi.spyOn(
await import('../environment.js'),
'ensureAndroidEmulatorEnvironment'
).mockResolvedValue('/tmp/android-sdk');
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['emulator-5554']);
vi.spyOn(adb, 'getEmulatorName').mockResolvedValue('Pixel_8_API_35');
vi.spyOn(adb, 'waitForBoot').mockResolvedValue('emulator-5554');
vi.spyOn(adb, 'isAppInstalled').mockResolvedValue(true);
vi.spyOn(adb, 'reversePort').mockResolvedValue(undefined);
vi.spyOn(adb, 'setHideErrorDialogs').mockResolvedValue(undefined);
vi.spyOn(adb, 'getAppUid').mockResolvedValue(10234);
vi.spyOn(sharedPrefs, 'applyHarnessDebugHttpHost').mockResolvedValue(
undefined
);

const instance = await getAndroidEmulatorPlatformInstance(
{
name: 'android',
device: {
type: 'emulator',
name: 'Pixel_8_API_35',
avd: {
apiLevel: 35,
profile: 'pixel_8',
diskSize: '1G',
heapSize: '1G',
},
},
bundleId: 'com.harnessplayground',
activityName: '.MainActivity',
},
harnessConfigWithoutNativeCrashDetection,
init
);

const listener = vi.fn();
const appMonitor = instance.createAppMonitor();

await expect(appMonitor.start()).resolves.toBeUndefined();
await expect(appMonitor.stop()).resolves.toBeUndefined();
await expect(appMonitor.dispose()).resolves.toBeUndefined();
expect(appMonitor.addListener(listener)).toBeUndefined();
expect(appMonitor.removeListener(listener)).toBeUndefined();
});

it('returns a noop physical device app monitor when native crash detection is disabled', async () => {
vi.spyOn(adb, 'getDeviceIds').mockResolvedValue(['012345']);
vi.spyOn(adb, 'getDeviceInfo').mockResolvedValue({
manufacturer: 'motorola',
Expand All @@ -530,8 +580,31 @@ describe('Android platform instance', () => {
bundleId: 'com.harnessplayground',
activityName: '.MainActivity',
},
harnessConfig
harnessConfigWithoutNativeCrashDetection
)
).resolves.toBeDefined();

const instance = await getAndroidPhysicalDevicePlatformInstance(
{
name: 'android-device',
device: {
type: 'physical',
manufacturer: 'motorola',
model: 'moto g72',
},
bundleId: 'com.harnessplayground',
activityName: '.MainActivity',
},
harnessConfigWithoutNativeCrashDetection
);

const listener = vi.fn();
const appMonitor = instance.createAppMonitor();

await expect(appMonitor.start()).resolves.toBeUndefined();
await expect(appMonitor.stop()).resolves.toBeUndefined();
await expect(appMonitor.dispose()).resolves.toBeUndefined();
expect(appMonitor.addListener(listener)).toBeUndefined();
expect(appMonitor.removeListener(listener)).toBeUndefined();
});
});
33 changes: 27 additions & 6 deletions packages/platform-android/src/instance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,18 @@ import {
} from './environment.js';
import { isInteractive } from '@react-native-harness/tools';
import fs from 'node:fs';
import type { AppMonitor } from '@react-native-harness/platforms';

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

const createNoopAppMonitor = (): AppMonitor => ({
start: async () => undefined,
stop: async () => undefined,
dispose: async () => undefined,
addListener: () => undefined,
removeListener: () => undefined,
});

const getHarnessAppPath = (): string => {
const appPath = process.env.HARNESS_APP_PATH;

Expand Down Expand Up @@ -168,6 +177,7 @@ export const getAndroidEmulatorPlatformInstance = async (
init: HarnessPlatformInitOptions
): Promise<HarnessPlatformRunner> => {
assertAndroidDeviceEmulator(config.device);
const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;
const emulatorConfig = config.device;
const emulatorName = emulatorConfig.name;
const avdConfig = emulatorConfig.avd;
Expand Down Expand Up @@ -284,13 +294,18 @@ export const getAndroidEmulatorPlatformInstance = async (
isAppRunning: async () => {
return await adb.isAppRunning(adbId, config.bundleId);
},
createAppMonitor: (options?: CreateAppMonitorOptions) =>
createAndroidAppMonitor({
createAppMonitor: (options?: CreateAppMonitorOptions) => {
if (!detectNativeCrashes) {
return createNoopAppMonitor();
}

return createAndroidAppMonitor({
adbId,
bundleId: config.bundleId,
appUid,
crashArtifactWriter: options?.crashArtifactWriter,
}),
});
},
};
};

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

const adbId = await getAdbId(config.device);

Expand Down Expand Up @@ -348,12 +364,17 @@ export const getAndroidPhysicalDevicePlatformInstance = async (
isAppRunning: async () => {
return await adb.isAppRunning(adbId, config.bundleId);
},
createAppMonitor: (options?: CreateAppMonitorOptions) =>
createAndroidAppMonitor({
createAppMonitor: (options?: CreateAppMonitorOptions) => {
if (!detectNativeCrashes) {
return createNoopAppMonitor();
}

return createAndroidAppMonitor({
adbId,
bundleId: config.bundleId,
appUid,
crashArtifactWriter: options?.crashArtifactWriter,
}),
});
},
};
};
74 changes: 72 additions & 2 deletions packages/platform-ios/src/__tests__/instance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ const init = {
signal: new AbortController().signal,
};

const harnessConfigWithoutNativeCrashDetection = {
metroPort: DEFAULT_METRO_PORT,
detectNativeCrashes: false,
} as HarnessConfig;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
} as HarnessConfig;
} satisfies HarnessConfig;


describe('iOS platform instance dependency validation', () => {
beforeEach(() => {
vi.restoreAllMocks();
Expand Down Expand Up @@ -55,7 +60,7 @@ describe('iOS platform instance dependency validation', () => {
expect(assertInstalled).not.toHaveBeenCalled();
});

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

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

it('skips libimobiledevice validation when native crash detection is disabled', async () => {
const assertInstalled = vi
.spyOn(libimobiledevice, 'assertLibimobiledeviceInstalled')
.mockRejectedValue(new Error('missing'));
vi.spyOn(devicectl, 'getDevice').mockResolvedValue({
identifier: 'physical-device-id',
deviceProperties: {
name: 'My iPhone',
osVersionNumber: '18.0',
},
hardwareProperties: {
marketingName: 'iPhone',
productType: 'iPhone17,1',
udid: '00008140-001600222422201C',
},
});
vi.spyOn(devicectl, 'isAppInstalled').mockResolvedValue(true);

const config = {
name: 'ios-device',
device: { type: 'physical' as const, name: 'My iPhone' },
bundleId: 'com.harnessplayground',
};

await expect(
getApplePhysicalDevicePlatformInstance(
config,
harnessConfigWithoutNativeCrashDetection
)
).resolves.toBeDefined();
expect(assertInstalled).not.toHaveBeenCalled();
});

it('returns a noop simulator app monitor when native crash detection is disabled', async () => {
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
vi.spyOn(simctl, 'isAppInstalled').mockResolvedValue(true);
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
vi.spyOn(simctl, 'applyHarnessJsLocationOverride').mockResolvedValue(
undefined
);

const instance = await getAppleSimulatorPlatformInstance(
{
name: 'ios',
device: {
type: 'simulator',
name: 'iPhone 16 Pro',
systemVersion: '18.0',
},
bundleId: 'com.harnessplayground',
},
harnessConfigWithoutNativeCrashDetection,
init
);

const listener = vi.fn();
const appMonitor = instance.createAppMonitor();

await expect(appMonitor.start()).resolves.toBeUndefined();
await expect(appMonitor.stop()).resolves.toBeUndefined();
await expect(appMonitor.dispose()).resolves.toBeUndefined();
expect(appMonitor.addListener(listener)).toBeUndefined();
expect(appMonitor.removeListener(listener)).toBeUndefined();
});

it('reuses a booted simulator and does not shut it down on dispose', async () => {
vi.spyOn(simctl, 'getSimulatorId').mockResolvedValue('sim-udid');
vi.spyOn(simctl, 'getSimulatorStatus').mockResolvedValue('Booted');
Expand Down
38 changes: 31 additions & 7 deletions packages/platform-ios/src/instance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
AppMonitor,
AppNotInstalledError,
CreateAppMonitorOptions,
DeviceNotFoundError,
Expand Down Expand Up @@ -42,12 +43,21 @@ const getHarnessAppPath = (): string => {
return appPath;
};

const createNoopAppMonitor = (): AppMonitor => ({
start: async () => undefined,
stop: async () => undefined,
dispose: async () => undefined,
addListener: () => undefined,
removeListener: () => undefined,
});

export const getAppleSimulatorPlatformInstance = async (
config: ApplePlatformConfig,
harnessConfig: HarnessConfig,
init: HarnessPlatformInitOptions
): Promise<HarnessPlatformRunner> => {
assertAppleDeviceSimulator(config.device);
const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;

const udid = await simctl.getSimulatorId(
config.device.name,
Expand Down Expand Up @@ -144,12 +154,17 @@ export const getAppleSimulatorPlatformInstance = async (
isAppRunning: async () => {
return await simctl.isAppRunning(udid, config.bundleId);
},
createAppMonitor: (options?: CreateAppMonitorOptions) =>
createIosSimulatorAppMonitor({
createAppMonitor: (options?: CreateAppMonitorOptions) => {
if (!detectNativeCrashes) {
return createNoopAppMonitor();
}

return createIosSimulatorAppMonitor({
udid,
bundleId: config.bundleId,
crashArtifactWriter: options?.crashArtifactWriter,
}),
});
},
};
};

Expand All @@ -158,7 +173,11 @@ export const getApplePhysicalDevicePlatformInstance = async (
harnessConfig: HarnessConfig
): Promise<HarnessPlatformRunner> => {
assertAppleDevicePhysical(config.device);
await assertLibimobiledeviceInstalled();
const detectNativeCrashes = harnessConfig.detectNativeCrashes ?? true;

if (detectNativeCrashes) {
await assertLibimobiledeviceInstalled();
}

if (harnessConfig.metroPort !== DEFAULT_METRO_PORT) {
throw new Error(
Expand Down Expand Up @@ -211,12 +230,17 @@ export const getApplePhysicalDevicePlatformInstance = async (
isAppRunning: async () => {
return await devicectl.isAppRunning(deviceId, config.bundleId);
},
createAppMonitor: (options?: CreateAppMonitorOptions) =>
createIosDeviceAppMonitor({
createAppMonitor: (options?: CreateAppMonitorOptions) => {
if (!detectNativeCrashes) {
return createNoopAppMonitor();
}

return createIosDeviceAppMonitor({
deviceId,
libimobiledeviceUdid: hardwareUdid,
bundleId: config.bundleId,
crashArtifactWriter: options?.crashArtifactWriter,
}),
});
},
};
};
Loading