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
113 changes: 113 additions & 0 deletions src/android/utils/__tests__/run.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import type { Device } from '../adb';
import { isLikelyEmulator, findAvailableEmulatorPort } from '../run';

describe('android/utils/run', () => {
describe('isLikelyEmulator', () => {
it('detects emulator by serial', () => {
const d = {
serial: 'emulator-5554',
state: 'device',
type: 'hardware',
connection: null,
properties: {},
manufacturer: '',
model: '',
product: '',
sdkVersion: '',
} as Device;
expect(isLikelyEmulator(d)).toBe(true);
});

it('detects emulator by type', () => {
const d = {
serial: 'device-1',
state: 'device',
type: 'emulator',
connection: null,
properties: {},
manufacturer: '',
model: '',
product: '',
sdkVersion: '',
} as Device;
expect(isLikelyEmulator(d)).toBe(true);
});

it('detects emulator by properties.device', () => {
const d = {
serial: 'device-2',
state: 'device',
type: 'hardware',
connection: null,
properties: { device: 'emu64a' },
manufacturer: '',
model: '',
product: '',
sdkVersion: '',
} as Device;
expect(isLikelyEmulator(d)).toBe(true);
});

it('detects emulator by properties.product', () => {
const d = {
serial: 'device-3',
state: 'device',
type: 'hardware',
connection: null,
properties: { product: 'sdk_gphone_arm64' },
manufacturer: '',
model: '',
product: '',
sdkVersion: '',
} as Device;
expect(isLikelyEmulator(d)).toBe(true);
});

it('returns false for real hardware', () => {
const d = {
serial: '0123456789',
state: 'device',
type: 'hardware',
connection: null,
properties: { product: 'samsung' },
manufacturer: '',
model: 'SM-G',
product: '',
sdkVersion: '',
} as Device;
expect(isLikelyEmulator(d)).toBe(false);
});
});

describe('findAvailableEmulatorPort', () => {
it('picks first free even port', async () => {
const devices: Device[] = [
{ serial: 'emulator-5554', type: 'emulator', properties: {}, model: '' } as Device,
{ serial: 'emulator-5556', type: 'emulator', properties: {}, model: '' } as Device,
];

const port = await findAvailableEmulatorPort(devices, 5554, 5560);
expect(port).toBe(5558);
});

it('falls back to 5554 when none found in range', async () => {
// mark the only port in the range as already used
const devices: Device[] = [
{
serial: 'emulator-6000',
state: 'device',
type: 'emulator',
connection: null,
properties: {},
manufacturer: '',
model: '',
product: '',
sdkVersion: '',
} as Device,
];

const port = await findAvailableEmulatorPort(devices, 6000, 6000);
expect(port).toBe(5554);
});
});
});
59 changes: 57 additions & 2 deletions src/android/utils/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,59 @@

const modulePrefix = 'native-run:android:utils:run';

export async function findAvailableEmulatorPort(devices: readonly Device[], start = 5554, end = 5584): Promise<number> {
const debug = Debug(`${modulePrefix}:${findAvailableEmulatorPort.name}`);
const usedPorts = new Set<number>();

for (const d of devices) {
const m = d.serial.match(/^emulator-(\d+)$/);
if (m) {
usedPorts.add(Number(m[1]));
}
}

for (let port = start; port <= end; port += 2) {
if (!usedPorts.has(port)) {
debug('Available emulator port found: %d', port);
return port;
}
}

debug('No available emulator ports found in range %d-%d; defaulting to 5554', start, end);
return 5554;
}

export function isLikelyEmulator(device: Device): boolean {
const serialEmu = /^emulator-(\d+)$/;

if (serialEmu.test(device.serial)) {
return true;
}

if (device.type === 'emulator') {
return true;
}

const props = device.properties || {};
const deviceProp = (props['device'] || '').toLowerCase();
const productProp = (props['product'] || '').toLowerCase();
const model = (device.model || '').toLowerCase();

if (deviceProp.startsWith('emu') || deviceProp.includes('generic')) {
return true;
}

if (productProp.includes('sdk_gphone') || productProp.includes('google_sdk')) {
return true;
}

if (model.includes('android_sdk') || model.includes('sdk_gphone')) {
return true;
}

return false;
}

export async function selectDeviceByTarget(
sdk: SDK,
devices: readonly Device[],
Expand All @@ -34,7 +87,7 @@
return device;
}

const emulatorDevices = devices.filter((d) => d.type === 'emulator');
const emulatorDevices = devices.filter(isLikelyEmulator);

const pairAVD = async (emulator: Device): Promise<[Device, AVD | undefined]> => {
let avd: AVD | undefined;
Expand Down Expand Up @@ -65,7 +118,9 @@

if (avd) {
debug('AVD found by ID: %s', avd.id);
const device = await runEmulator(sdk, avd, 5554); // TODO: 5554 will not always be available at this point
const port = await findAvailableEmulatorPort(devices);
debug('Using emulator port: %d', port);
const device = await runEmulator(sdk, avd, port);
debug('Emulator ready, running avd: %s on %s', avd.id, device.serial);

return device;
Expand All @@ -81,7 +136,7 @@
}
}

export async function selectVirtualDevice(sdk: SDK, devices: readonly Device[], avds: readonly AVD[]): Promise<Device> {

Check warning on line 139 in src/android/utils/run.ts

View workflow job for this annotation

GitHub Actions / Build and Test (Node 20.x)

'avds' is defined but never used

Check warning on line 139 in src/android/utils/run.ts

View workflow job for this annotation

GitHub Actions / Build and Test (Node 16.x)

'avds' is defined but never used

Check warning on line 139 in src/android/utils/run.ts

View workflow job for this annotation

GitHub Actions / Build and Test (Node 18.x)

'avds' is defined but never used
const debug = Debug(`${modulePrefix}:${selectVirtualDevice.name}`);
const emulators = devices.filter((d) => d.type === 'emulator');

Expand Down
Loading