Skip to content
Open
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
1 change: 0 additions & 1 deletion .buildkite/jobs/pipeline.android_rn_82.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
env:
REACT_NATIVE_VERSION: 0.83.0
RCT_NEW_ARCH_ENABLED: 1
TEST_GENYCLOUD_SANITY: true
DETOX_DISABLE_POD_INSTALL: true
DETOX_DISABLE_POSTINSTALL: true
artifact_paths:
Expand Down
2 changes: 2 additions & 0 deletions .buildkite/jobs/pipeline.android_rn_83.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
env:
REACT_NATIVE_VERSION: 0.83.0
RCT_NEW_ARCH_ENABLED: 1
TEST_GENYCLOUD_SANITY: true # Only set 'true' in jobs with the latest supported RN
TEST_SINGLE_ADB_SERVER_SANITY: true # Only set 'true' in jobs with the latest supported RN
DETOX_DISABLE_POD_INSTALL: true
DETOX_DISABLE_POSTINSTALL: true
artifact_paths:
Expand Down
7 changes: 7 additions & 0 deletions detox/detox.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,13 @@ declare global {
* @default true
*/
readonly?: boolean;
/**
* When `true`, each emulator uses its own separate ADB server on a separate port. This is the
* opposite of Android's default, which is to share a single ADB server on the default port (5037). The default
* has been found to be unstable and is therefore not recommended.
* @default true
*/
useSeparateAdbServers?: boolean;
}

interface DetoxGenymotionCloudDriverConfig extends DetoxSharedAndroidDriverConfig {
Expand Down
1 change: 1 addition & 0 deletions detox/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ module.exports = {
'src/utils/pressAnyKey.js',
'src/utils/repl.js',
'src/utils/shellUtils.js',
'src/utils/netUtils.js',
'runners/jest/reporters',
'runners/jest/testEnvironment',
'src/DetoxWorker.js',
Expand Down
2 changes: 2 additions & 0 deletions detox/src/DetoxWorker.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ class DetoxWorker {
});
}

// @ts-ignore
yield this.device.init();
// @ts-ignore
yield this.device.installUtilBinaries();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,31 @@
/**
* @typedef {import('../../../common/drivers/android/tools/DeviceHandle')} DeviceHandle
* @typedef {import('../../../common/drivers/android/tools/EmulatorHandle')} EmulatorHandle
*/

const log = require('../../../../utils/logger').child({ cat: 'device' });

const DEVICE_LOOKUP = { event: 'DEVICE_LOOKUP' };

class FreeDeviceFinder {
constructor(adb, deviceRegistry) {
this.adb = adb;
/**
* @param {import('../../DeviceRegistry')} deviceRegistry
*/
constructor(deviceRegistry) {
this.deviceRegistry = deviceRegistry;
}

async findFreeDevice(deviceQuery) {
const { devices } = await this.adb.devices();
/**
* @param {DeviceHandle[]} candidates
* @param {string} deviceQuery
* @returns {Promise<import('../../../common/drivers/android/tools/EmulatorHandle') | null>}
*/
async findFreeDevice(candidates, deviceQuery) {
const takenDevices = this.deviceRegistry.getTakenDevicesSync();
for (const candidate of devices) {
for (const candidate of candidates) {
if (await this._isDeviceFreeAndMatching(takenDevices, candidate, deviceQuery)) {
return candidate.adbName;
// @ts-ignore
return candidate;
}
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ const FreeDeviceFinder = require('./FreeDeviceFinder');
const { deviceOffline, emulator5556, ip5557, localhost5555 } = require('./__mocks__/handles');

describe('FreeDeviceFinder', () => {
const mockAdb = { devices: jest.fn() };

/** @type {DeviceList} */
let fakeDeviceList;
let mockDeviceRegistry;
Expand All @@ -17,42 +15,34 @@ describe('FreeDeviceFinder', () => {
mockDeviceRegistry = new DeviceRegistry();
mockDeviceRegistry.getTakenDevicesSync.mockImplementation(() => fakeDeviceList);

uut = new FreeDeviceFinder(mockAdb, mockDeviceRegistry);
uut = new FreeDeviceFinder(mockDeviceRegistry);
});

it('should return the only device when it matches, is online and not already taken by other workers', async () => {
mockAdbDevices([emulator5556]);

const result = await uut.findFreeDevice(emulator5556.adbName);
expect(result).toEqual(emulator5556.adbName);
const result = await uut.findFreeDevice([emulator5556], emulator5556.adbName);
expect(result).toEqual(emulator5556);
});

it('should return null when there are no devices', async () => {
mockAdbDevices([]);

const result = await uut.findFreeDevice(emulator5556.adbName);
const result = await uut.findFreeDevice([], emulator5556.adbName);
expect(result).toEqual(null);
});

it('should return null when device is already taken by other workers', async () => {
mockAdbDevices([emulator5556]);
mockAllDevicesTaken();

expect(await uut.findFreeDevice(emulator5556.adbName)).toEqual(null);
expect(await uut.findFreeDevice([emulator5556], emulator5556.adbName)).toEqual(null);
});

it('should return null when device is offline', async () => {
mockAdbDevices([deviceOffline]);
expect(await uut.findFreeDevice(deviceOffline.adbName)).toEqual(null);
expect(await uut.findFreeDevice([deviceOffline], deviceOffline.adbName)).toEqual(null);
});

it('should return first device that matches a regular expression', async () => {
mockAdbDevices([emulator5556, localhost5555, ip5557]);
const localhost = '^localhost:\\d+$';
expect(await uut.findFreeDevice(localhost)).toBe(localhost5555.adbName);
expect(await uut.findFreeDevice([emulator5556, localhost5555, ip5557], localhost)).toBe(localhost5555);
});

const mockAdbDevices = (devices) => mockAdb.devices.mockResolvedValue({ devices });
const mockAllDevicesTaken = () => {
fakeDeviceList.add(emulator5556.adbName, { busy: true });
fakeDeviceList.add(localhost5555.adbName, { busy: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ class AttachedAndroidAllocDriver extends AndroidAllocDriver {
*/
async allocate(deviceConfig) {
const adbNamePattern = deviceConfig.device.adbName;
const adbName = await this._deviceRegistry.registerDevice(() => this._freeDeviceFinder.findFreeDevice(adbNamePattern));
const adbName = await this._deviceRegistry.registerDevice(async () =>
(await this._freeDeviceFinder.findFreeDevice(adbNamePattern)).adbName);

return { id: adbName, adbName };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
/**
* @typedef {import('../../../../common/drivers/android/cookies').AndroidDeviceCookie} AndroidDeviceCookie
* @typedef {import('../../../../common/drivers/android/cookies').EmulatorDeviceCookie} EmulatorDeviceCookie
* @typedef {import('../../../../common/drivers/android/tools/DeviceHandle')} DeviceHandle
*/

const _ = require('lodash');

const log = require('../../../../../utils/logger').child({ cat: 'device,device-allocation' });
const { isPortTaken } = require('../../../../../utils/netUtils');
const adbPortRegistry = require('../../../../common/drivers/android/AdbPortRegistry');
const AndroidAllocDriver = require('../AndroidAllocDriver');

const { patchAvdSkinConfig } = require('./patchAvdSkinConfig');
Expand Down Expand Up @@ -48,29 +52,47 @@ class EmulatorAllocDriver extends AndroidAllocDriver {

/**
* @param deviceConfig
* @returns {Promise<AndroidDeviceCookie>}
* @returns {Promise<EmulatorDeviceCookie>}
*/
async allocate(deviceConfig) {
const avdName = deviceConfig.device.avdName;
const useSeparateAdbServers = deviceConfig.useSeparateAdbServers === true;

await this._avdValidator.validate(avdName, deviceConfig.headless);
await this._fixAvdConfigIniSkinNameIfNeeded(avdName, deviceConfig.headless);

const adbName = await this._deviceRegistry.registerDevice(async () => {
let adbName = await this._freeDeviceFinder.findFreeDevice(avdName);
if (!adbName) {
let adbServerPort;
let adbName;

await this._deviceRegistry.registerDevice(async () => {
const candidates = await this._getAllDevices(useSeparateAdbServers);
const device = await this._freeDeviceFinder.findFreeDevice(candidates, avdName);

if (device) {
adbName = device.adbName;
adbServerPort = device.adbServerPort;
adbPortRegistry.register(adbName, adbServerPort);
} else {
const port = await this._freePortFinder.findFreePort();
adbName = `emulator-${port}`;

await this._emulatorLauncher.launch({
bootArgs: deviceConfig.bootArgs,
gpuMode: deviceConfig.gpuMode,
headless: deviceConfig.headless,
readonly: deviceConfig.readonly,
avdName,
adbName,
port,
});
adbName = `emulator-${port}`;
adbServerPort = this._getFreeAdbServerPort(candidates, useSeparateAdbServers);
adbPortRegistry.register(adbName, adbServerPort);

try {
await this._emulatorLauncher.launch({
bootArgs: deviceConfig.bootArgs,
gpuMode: deviceConfig.gpuMode,
headless: deviceConfig.headless,
readonly: deviceConfig.readonly,
avdName,
adbName,
port,
adbServerPort,
});
} catch (e) {
adbPortRegistry.unregister(adbName);
}
}

return adbName;
Expand All @@ -80,6 +102,7 @@ class EmulatorAllocDriver extends AndroidAllocDriver {
id: adbName,
adbName,
name: `${adbName} (${avdName})`,
adbServerPort,
};
}

Expand Down Expand Up @@ -111,14 +134,16 @@ class EmulatorAllocDriver extends AndroidAllocDriver {
if (options.shutdown) {
await this._doShutdown(adbName);
await this._deviceRegistry.unregisterDevice(adbName);

adbPortRegistry.unregister(adbName);
} else {
await this._deviceRegistry.releaseDevice(adbName);
}
}

async cleanup() {
if (this._shouldShutdown) {
const { devices } = await this._adb.devices();
const devices = await this._getAllDevices(true);
const actualEmulators = devices.map((device) => device.adbName);
const sessionDevices = await this._deviceRegistry.readSessionDevices();
const emulatorsToShutdown = _.intersection(sessionDevices.getIds(), actualEmulators);
Expand Down Expand Up @@ -146,6 +171,41 @@ class EmulatorAllocDriver extends AndroidAllocDriver {
const binaryVersion = _.get(rawBinaryVersion, 'major');
return await patchAvdSkinConfig(avdName, binaryVersion);
}

/**
* @param {boolean} useSeparateAdbServers
* @returns {Promise<DeviceHandle[]>}
* @private
*/
async _getAllDevices(useSeparateAdbServers) {
const adbServers = await this._getRunningAdbServers(useSeparateAdbServers);
return (await this._adb.devices({}, adbServers)).devices;
}

/**
* @param {boolean} useSeparateAdbServers
* @returns {Promise<number[]>}
* @private
*/
async _getRunningAdbServers(useSeparateAdbServers = true) {
const ports = [this._adb.defaultServerPort];

if (useSeparateAdbServers) {
for (let port = this._adb.defaultServerPort + 1; await isPortTaken(port); port++) {
Copy link
Collaborator

@noomorph noomorph Jan 12, 2026

Choose a reason for hiding this comment

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

What's the guarantee that the port is taken by an ADB server actually?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Can't we just enumerate ports from adbPortRegistry?

ports.push(port);
}
}
return ports;
}

_getFreeAdbServerPort(currentDevices, useSeparateAdbServers) {
if (!useSeparateAdbServers) {
return this._adb.defaultServerPort;
}

const maxPortDevice = _.maxBy(currentDevices, 'adbServerPort');
return _.get(maxPortDevice, 'adbServerPort', this._adb.defaultServerPort) + 1;
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do you check that it is actually free?

}
}

module.exports = EmulatorAllocDriver;
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class EmulatorLauncher {
* @param {object} options
* @param {string} options.avdName
* @param {string} options.adbName
* @param {number} options.adbServerPort
* @param {number} options.port
* @param {string | undefined} options.bootArgs
* @param {string | undefined} options.gpuMode
Expand All @@ -29,7 +30,7 @@ class EmulatorLauncher {
retries: 2,
interval: 100,
conditionFn: isUnknownEmulatorError,
}, () => launchEmulatorProcess(this._emulatorExec, this._adb, launchCommand));
}, () => launchEmulatorProcess(this._emulatorExec, this._adb, launchCommand, options.adbServerPort));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('Emulator launcher', () => {
expect(launchEmulatorProcess).toHaveBeenCalledWith(emulatorExec, adb, expect.objectContaining({
avdName,
adbName,
}));
}), undefined);
});

it('should launch using a specific emulator port, if provided', async () => {
Expand All @@ -49,6 +49,13 @@ describe('Emulator launcher', () => {
expect(command.port).toEqual(port);
});

it('should pass adbServerPort to launchEmulatorProcess', async () => {
const adbServerPort = 5038;
await uut.launch({ avdName, adbName, adbServerPort });

expect(launchEmulatorProcess).toHaveBeenCalledWith(emulatorExec, adb, expect.anything(), adbServerPort);
});

it('should retry emulator process launching with custom args', async () => {
const expectedRetryOptions = {
retries: 2,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class EmulatorVersionResolver {
}

const version = this._parseVersionString(matches[1]);
log.debug({ success: true }, 'Detected emulator binary version', version);
log.debug({ success: true }, `Detected emulator binary version ${version}`);
return version;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,6 @@ describe('Emulator binary version', () => {

it('should log the version', async () => {
await uut.resolve();
expect(log.debug).toHaveBeenCalledWith({ success: true }, expect.any(String), expect.objectContaining(expectedVersion));
expect(log.debug).toHaveBeenCalledWith({ success: true }, `Detected emulator binary version ${expectedVersionRaw}`);
});
});
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
const FreeDeviceFinder = require('../FreeDeviceFinder');

class FreeEmulatorFinder extends FreeDeviceFinder {
/**
* @param {import('../../../DeviceRegistry')} deviceRegistry
*/
constructor(deviceRegistry) {
super(deviceRegistry);
}

/**
* @override
*/
Expand Down
Loading
Loading