Skip to content

Commit 3dae97a

Browse files
committed
wip: Allocation for emulator reuse
1 parent 053b7b9 commit 3dae97a

File tree

12 files changed

+154
-83
lines changed

12 files changed

+154
-83
lines changed

detox/src/devices/allocation/drivers/android/FreeDeviceFinder.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ class FreeDeviceFinder {
1212
this.deviceRegistry = deviceRegistry;
1313
}
1414

15+
/**
16+
* @returns {Promise<import('../../../common/drivers/android/tools/DeviceHandle')>}
17+
*/
1518
async findFreeDevice(deviceQuery) {
1619
const { devices } = await this.adb.devices();
1720
const takenDevices = this.deviceRegistry.getTakenDevicesSync();
1821
for (const candidate of devices) {
1922
if (await this._isDeviceFreeAndMatching(takenDevices, candidate, deviceQuery)) {
20-
return candidate.adbName;
23+
return candidate;
2124
}
2225
}
2326
return null;

detox/src/devices/allocation/drivers/android/attached/AttachedAndroidAllocDriver.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ class AttachedAndroidAllocDriver extends AndroidAllocDriver {
2727
*/
2828
async allocate(deviceConfig) {
2929
const adbNamePattern = deviceConfig.device.adbName;
30-
const adbName = await this._deviceRegistry.registerDevice(() => this._freeDeviceFinder.findFreeDevice(adbNamePattern));
30+
const adbName = await this._deviceRegistry.registerDevice(async () =>
31+
(await this._freeDeviceFinder.findFreeDevice(adbNamePattern)).adbName);
3132

3233
return { id: adbName, adbName };
3334
}

detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,18 @@ class EmulatorAllocDriver extends AndroidAllocDriver {
6363
let adbName;
6464

6565
await this._deviceRegistry.registerDevice(async () => {
66-
adbName = await this._freeDeviceFinder.findFreeDevice(avdName);
66+
const device = await this._freeDeviceFinder.findFreeDevice(avdName);
6767

68-
if (adbName) {
69-
adbServerPort = adbPortRegistry.getPort(adbName);
68+
if (device) {
69+
adbName = device.adbName;
70+
adbServerPort = device.adbServerPort;
71+
adbPortRegistry.register(adbName, adbServerPort);
7072
} else {
7173
const port = await this._freePortFinder.findFreePort();
7274

7375
adbName = `emulator-${port}`;
7476
adbServerPort = this._nextAdbServerPort++;
77+
adbPortRegistry.register(adbName, adbServerPort);
7578

7679
await this._emulatorLauncher.launch({
7780
bootArgs: deviceConfig.bootArgs,
Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const net = require('net');
1+
const { isPortTaken } = require('../../../../../utils/portUtils');
22

33
class FreePortFinder {
44
constructor({ min = 10000, max = 20000 } = {}) {
@@ -14,24 +14,10 @@ class FreePortFinder {
1414
const max = this._max;
1515
port = Math.random() * (max - min) + min;
1616
port = port & 0xFFFFFFFE; // Should always be even
17-
} while (await this.isPortTaken(port));
17+
} while (await isPortTaken(port));
1818

1919
return port;
2020
}
21-
22-
async isPortTaken(port) {
23-
return new Promise((resolve, reject) => {
24-
const tester = net.createServer()
25-
.once('error', /** @param {*} err */ (err) => {
26-
/* istanbul ignore next */
27-
return err && err.code === 'EADDRINUSE' ? resolve(true) : reject(err);
28-
})
29-
.once('listening', () => {
30-
tester.once('close', () => resolve(false)).close();
31-
})
32-
.listen(port);
33-
});
34-
}
3521
}
3622

3723
module.exports = FreePortFinder;
Lines changed: 7 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,21 @@
1-
const net = require('net');
2-
3-
const FreePortFinder = require('./FreePortFinder');
4-
51
describe('FreePortFinder', () => {
62
let finder;
7-
let server;
3+
let isPortTaken;
84

95
beforeEach(() => {
10-
finder = new FreePortFinder();
11-
});
6+
jest.mock('../../../../../utils/portUtils');
7+
isPortTaken = require('../../../../../utils/portUtils').isPortTaken;
8+
isPortTaken.mockResolvedValue(false);
129

13-
afterEach(done => {
14-
if (server) {
15-
server.close(done);
16-
} else {
17-
done();
18-
}
10+
const FreePortFinder = require('./FreePortFinder');
11+
finder = new FreePortFinder();
1912
});
2013

2114
test('should find a free port', async () => {
2215
const port = await finder.findFreePort();
2316
expect(port).toBeGreaterThanOrEqual(10000);
2417
expect(port).toBeLessThanOrEqual(20000);
2518
expect(port % 2).toBe(0);
26-
await expect(finder.isPortTaken(port)).resolves.toBe(false);
27-
});
28-
29-
test('should identify a taken port', async () => {
30-
server = net.createServer();
31-
const portTaken = await new Promise(resolve => {
32-
server.listen(0, () => { // 0 means random available port
33-
resolve(server.address().port);
34-
});
35-
});
36-
37-
await expect(finder.isPortTaken(portTaken)).resolves.toBe(true);
19+
expect(isPortTaken).toHaveBeenCalled();
3820
});
3921
});

detox/src/devices/common/drivers/android/AdbPortRegistry.js

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,6 @@ class AdbPortRegistry {
1818
getPort(adbName) {
1919
return this._registry.get(adbName);
2020
}
21-
22-
/**
23-
* @returns { number[] }
24-
*/
25-
getAllPorts() {
26-
return this._registry.values().toArray();
27-
}
2821
}
2922

3023
module.exports = new AdbPortRegistry();

detox/src/devices/common/drivers/android/exec/ADB.js

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const { escape } = require('../../../../../utils/pipeCommands');
88
const adbPortRegistry = require('../AdbPortRegistry');
99
const DeviceHandle = require('../tools/DeviceHandle');
1010
const EmulatorHandle = require('../tools/EmulatorHandle');
11+
const { isPortTaken } = require('../../../../../utils/portUtils');
1112

1213
const DEFAULT_EXEC_OPTIONS = {
1314
retries: 1,
@@ -36,28 +37,29 @@ class ADB {
3637
}
3738

3839
async devices(options) {
39-
let devices = [];
40+
const devicesByPort = {};
4041
const stdouts = [];
41-
const ports = [ADB_SERVER_BASE_PORT, ...adbPortRegistry.getAllPorts()];
42-
43-
for (const port of ports) {
42+
for (let port = ADB_SERVER_BASE_PORT; await isPortTaken(port); port++) {
4443
const { stdout } = await this.adbCmdWithPort('', port, 'devices', { verbosity: 'high', ...options });
45-
devices.push(
46-
_.chain(stdout)
47-
.trim()
48-
.split('\n')
49-
.slice(1)
50-
.map(s => _.trim(s))
51-
.value()
52-
);
44+
devicesByPort[port] = _.chain(stdout)
45+
.trim()
46+
.split('\n')
47+
.slice(1)
48+
.map(s => _.trim(s))
49+
.value();
5350
stdouts.push(stdout);
5451
}
5552

53+
const devices =
54+
_.flatMap(Object.keys(devicesByPort), port =>
55+
_.map(devicesByPort[port],
56+
s => s.startsWith('emulator-')
57+
? new EmulatorHandle(s, Number(port))
58+
: new DeviceHandle(s, Number(port))
59+
));
60+
5661
return {
57-
devices: _.flatten(devices)
58-
.map(s => s.startsWith('emulator-')
59-
? new EmulatorHandle(s)
60-
: new DeviceHandle(s)),
62+
devices,
6163
stdout: stdouts.join('\n'),
6264
};
6365
}

detox/src/devices/common/drivers/android/exec/ADB.test.js

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
describe('ADB', () => {
33
const deviceId = 'mockEmulator';
44
const adbBinPath = `/Android/sdk-mock/platform-tools/adb`;
5+
const baseAdbServerPort = 5037;
56

67
let ADB;
78
let adb;
@@ -11,6 +12,7 @@ describe('ADB', () => {
1112
let spawnAndLog;
1213
let spawnWithRetriesAndLogs;
1314
let adbPortRegistry;
15+
let isPortTaken;
1416

1517
beforeEach(() => {
1618
jest.mock('../../../../../utils/logger');
@@ -38,14 +40,16 @@ describe('ADB', () => {
3840

3941
jest.mock('../AdbPortRegistry');
4042
adbPortRegistry = require('../AdbPortRegistry');
41-
adbPortRegistry.getAllPorts.mockReturnValue([]);
43+
44+
jest.mock('../../../../../utils/portUtils');
45+
isPortTaken = require('../../../../../utils/portUtils').isPortTaken;
46+
isPortTaken.mockResolvedValueOnce(true);
4247

4348
ADB = require('./ADB');
4449
adb = new ADB();
4550
});
4651

4752
describe('devices', () => {
48-
4953
const mockDeviceEntry = 'MOCK_SERIAL\tdevice';
5054
const wifiDeviceEntry = '192.168.60.101:6666\tdevice';
5155
const emulatorEntry = 'emulator-5554\tdevice';
@@ -78,10 +82,10 @@ describe('ADB', () => {
7882
EmulatorHandle.mock.instances[0],
7983
EmulatorHandle.mock.instances[1],
8084
]);
81-
expect(DeviceHandle).toHaveBeenCalledWith(mockDevices[0]);
82-
expect(DeviceHandle).toHaveBeenCalledWith(mockDevices[1]);
83-
expect(EmulatorHandle).toHaveBeenCalledWith(mockDevices[2]);
84-
expect(EmulatorHandle).toHaveBeenCalledWith(mockDevices[3]);
85+
expect(DeviceHandle).toHaveBeenCalledWith(mockDevices[0], baseAdbServerPort);
86+
expect(DeviceHandle).toHaveBeenCalledWith(mockDevices[1], baseAdbServerPort);
87+
expect(EmulatorHandle).toHaveBeenCalledWith(mockDevices[2], baseAdbServerPort);
88+
expect(EmulatorHandle).toHaveBeenCalledWith(mockDevices[3], baseAdbServerPort);
8589
});
8690

8791
it(`should return an empty list if no devices are available`, async () => {
@@ -103,18 +107,17 @@ describe('ADB', () => {
103107
expect(execWithRetriesAndLogs).toHaveBeenCalledWith(expect.any(String), options);
104108
});
105109

106-
it('should process devices correctly when adbPortRegistry.getAllPorts() returns a port array only on the 2nd call', async () => {
107-
const regPort = 5038;
108-
adbPortRegistry.getAllPorts.mockReturnValue([regPort]);
110+
it('should process devices correctly when emulators are on another adb-server', async () => {
111+
isPortTaken.mockImplementation((port) => (port === baseAdbServerPort || port === baseAdbServerPort + 1));
109112
execWithRetriesAndLogs
110113
.mockReturnValueOnce({ stdout: adbDevicesStdout([emulatorEntry]) }) // Result for the standard port (5037)
111114
.mockReturnValueOnce({ stdout: adbDevicesStdout([emulator5556Entry]) }); // Result for the port from the adb-ports registry
112115

113116
const { devices } = await adb.devices();
114117

115118
expect(devices).toHaveLength(2);
116-
expect(EmulatorHandle).toHaveBeenCalledWith(emulatorEntry);
117-
expect(EmulatorHandle).toHaveBeenCalledWith(emulator5556Entry);
119+
expect(EmulatorHandle).toHaveBeenCalledWith(emulatorEntry, baseAdbServerPort);
120+
expect(EmulatorHandle).toHaveBeenCalledWith(emulator5556Entry, baseAdbServerPort + 1);
118121
});
119122
});
120123

detox/src/devices/common/drivers/android/tools/DeviceHandle.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
class DeviceHandle {
2-
constructor(deviceString) {
2+
constructor(deviceString, adbServerPort) {
33
const [adbName, status] = deviceString.split('\t');
44
this.type = this._inferDeviceType(adbName);
55
this.adbName = adbName;
66
this.status = status;
7+
this.adbServerPort = adbServerPort;
78
}
89

910
_inferDeviceType(adbName) {

detox/src/devices/common/drivers/android/tools/EmulatorHandle.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ const DeviceHandle = require('./DeviceHandle');
22
const EmulatorTelnet = require('./EmulatorTelnet');
33

44
class EmulatorHandle extends DeviceHandle {
5-
constructor(deviceString) {
6-
super(deviceString);
5+
constructor(deviceString, adbServerPort) {
6+
super(deviceString, adbServerPort);
77
this._telnet = new EmulatorTelnet();
88

99
this.port = this.adbName.split('-')[1];

0 commit comments

Comments
 (0)