Skip to content

Commit

Permalink
Merge pull request wix#1989 from wix/break-emulator
Browse files Browse the repository at this point in the history
Break emulator class into more contained sub-concern classes
  • Loading branch information
d4vidi authored Apr 1, 2020
2 parents abcb6a3 + bd44731 commit 2126032
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 96 deletions.
2 changes: 1 addition & 1 deletion detox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
"MissingDetox.js",
"EmulatorTelnet.js",
"EmulatorExec.js",
"Emulator.js",
"EmulatorLauncher.js",
"DeviceDriverBase.js",
"GREYConfiguration.js",
"src/utils/environment.js",
Expand Down
44 changes: 17 additions & 27 deletions detox/src/devices/drivers/android/EmulatorDriver.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ const fs = require('fs');
const path = require('path');
const ini = require('ini');
const AndroidDriver = require('./AndroidDriver');
const EmulatorLookupHelper = require('./EmulatorLookupHelper');
const FreeEmulatorFinder = require('./emulator/FreeEmulatorFinder');
const AVDValidator = require('./emulator/AVDValidator');
const EmulatorLauncher = require('./emulator/EmulatorLauncher');
const { EmulatorExec } = require('./tools/EmulatorExec');
const EmulatorTelnet = require('./tools/EmulatorTelnet');
const EmulatorVersionResolver = require('./emulator/EmulatorVersionResolver');
const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
const DeviceRegistry = require('../../DeviceRegistry');
const Emulator = require('./tools/Emulator');
const EmulatorTelnet = require('./tools/EmulatorTelnet');
const EmulatorVersion = require('./EmulatorVersion');
const environment = require('../../../utils/environment');
const retry = require('../../../utils/retry');
const log = require('../../../utils/logger').child({ __filename });
Expand All @@ -25,13 +27,17 @@ class EmulatorDriver extends AndroidDriver {
constructor(config) {
super(config);

this.emulator = new Emulator();
this.deviceRegistry = new DeviceRegistry({
lockfilePath: environment.getDeviceLockFilePathAndroid(),
});

const emulatorExec = new EmulatorExec();
this._emuVersionResolver = new EmulatorVersionResolver(emulatorExec);
this._emuLauncher = new EmulatorLauncher(emulatorExec);
this._avdValidator = new AVDValidator(emulatorExec);

this.pendingBoots = {};
this._name = 'Unspecified Emulator';
this._emulatorVersion = new EmulatorVersion(this.emulator);
}

get name() {
Expand All @@ -41,7 +47,7 @@ class EmulatorDriver extends AndroidDriver {
async acquireFreeDevice(deviceQuery) {
const avdName = _.isPlainObject(deviceQuery) ? deviceQuery.avdName : deviceQuery;

await this._validateAvd(avdName);
await this._avdValidator.validate(avdName);
await this._fixEmulatorConfigIniSkinNameIfNeeded(avdName);

const adbName = await this._allocateDevice(avdName);
Expand All @@ -61,38 +67,22 @@ class EmulatorDriver extends AndroidDriver {
}

/*async*/ binaryVersion() {
return this._emulatorVersion.resolve();
return this._emuVersionResolver.resolve();
}

async _boot(avdName, adbName) {
const coldBoot = !!this.pendingBoots[adbName];

if (coldBoot) {
const port = this.pendingBoots[adbName];
await this.emulator.boot(avdName, {port});
await this._emuLauncher.launch(avdName, { port });
delete this.pendingBoots[adbName];
}

await this._waitForBootToComplete(adbName);
await this.emitter.emit('bootDevice', { coldBoot, deviceId: adbName, type: adbName });
}

async _validateAvd(avdName) {
const avds = await this.emulator.listAvds();
if (!avds) {
const avdmanagerPath = path.join(environment.getAndroidSDKPath(), 'tools', 'bin', 'avdmanager');

throw new Error(`Could not find any configured Android Emulator.
Try creating a device first, example: ${avdmanagerPath} create avd --force --name Pixel_2_API_26 --abi x86 --package 'system-images;android-26;google_apis_playstore;x86' --device "pixel"
or go to https://developer.android.com/studio/run/managing-avds.html for details on how to create an Emulator.`);
}

if (_.indexOf(avds, avdName) === -1) {
throw new Error(`Can not boot Android Emulator with the name: '${avdName}',
make sure you choose one of the available emulators: ${avds.toString()}`);
}
}

async _waitForBootToComplete(deviceId) {
await retry({ retries: 240, interval: 2500 }, async () => {
const isBootComplete = await this.adb.isBootComplete(deviceId);
Expand Down Expand Up @@ -145,8 +135,8 @@ class EmulatorDriver extends AndroidDriver {
}

async _doAllocateDevice(avdName) {
const emulatorLookupHelper = new EmulatorLookupHelper(this.adb, this.deviceRegistry, avdName);
const freeEmulatorAdbName = await emulatorLookupHelper.findFreeDevice();
const freeEmulatorFinder = new FreeEmulatorFinder(this.adb, this.deviceRegistry, avdName);
const freeEmulatorAdbName = await freeEmulatorFinder.findFreeDevice();
return freeEmulatorAdbName || this._createDevice();
}

Expand Down
28 changes: 28 additions & 0 deletions detox/src/devices/drivers/android/emulator/AVDValidator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
const _ = require('lodash');
const path = require('path');
const AVDsResolver = require('./AVDsResolver');
const environment = require('../../../../utils/environment');

class AVDValidator {
constructor(emulatorExec) {
this._avdsResolver = new AVDsResolver(emulatorExec);
}

async validate(avdName) {
const avds = await this._avdsResolver.resolve(avdName);
if (!avds) {
const avdmanagerPath = path.join(environment.getAndroidSDKPath(), 'tools', 'bin', 'avdmanager');

throw new Error(`Could not find any configured Android Emulator.\n
Try creating a device first, example: ${avdmanagerPath} create avd --force --name Pixel_API_28 --abi x86_64 --package "system-images;android-28;default;x86_64" --device "pixel"
or go to https://developer.android.com/studio/run/managing-avds.html for details on how to create an Emulator.`);
}

if (_.indexOf(avds, avdName) === -1) {
throw new Error(`Can not boot Android Emulator with the name: '${avdName}',
make sure you choose one of the available emulators: ${avds.toString()}`);
}
}
}

module.exports = AVDValidator;
53 changes: 53 additions & 0 deletions detox/src/devices/drivers/android/emulator/AVDValidator.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
describe('AVD validator', () => {
let mockAvdsResolver;

class MockAVDsResolver {
constructor(...args) {
mockAvdsResolver.ctor(...args);
this.resolve = mockAvdsResolver.resolve;
}
}

const emulatorExec = {};
let uut;
beforeEach(() => {
mockAvdsResolver = {
ctor: jest.fn(),
resolve: jest.fn().mockResolvedValue(['mock-avd-name']),
};
jest.mock('./AVDsResolver', () => MockAVDsResolver);

const AVDValidator = require('./AVDValidator');
uut = new AVDValidator(emulatorExec);
});

it('should use an AVDs resolver', async () => {
await uut.validate('mock-avd-name');

expect(mockAvdsResolver.ctor).toHaveBeenCalledWith(emulatorExec);
expect(mockAvdsResolver.resolve).toHaveBeenCalledWith('mock-avd-name');
});

it('should return safely if AVD exists', async () => {
mockAvdsResolver.resolve.mockResolvedValue(['mock-avd-name']);
await uut.validate('mock-avd-name');
});

it('should throw if no AVDs found', async () => {
mockAvdsResolver.resolve.mockResolvedValue(undefined);

try {
await uut.validate();
fail('expected to throw');
} catch (err) {}
});

it('should throw if specific AVD not found', async () => {
mockAvdsResolver.resolve.mockResolvedValue(['other-avd', 'yet-another']);

try {
await uut.validate();
fail('expected to throw');
} catch (err) {}
});
});
15 changes: 15 additions & 0 deletions detox/src/devices/drivers/android/emulator/AVDsResolver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const { ListAVDsCommand } = require('../tools/EmulatorExec');

class AVDsResolver {
constructor(emulatorExec) {
this._emulatorExec = emulatorExec;
}

async resolve() {
const output = await this._emulatorExec.exec(new ListAVDsCommand());
const avds = output.trim().split('\n');
return avds;
}
}

module.exports = AVDsResolver;
28 changes: 28 additions & 0 deletions detox/src/devices/drivers/android/emulator/AVDsResolver.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
describe('AVDs resolver', () => {
let MockListAVDsCommand;
let emulatorExec;
let uut;
beforeEach(() => {
MockListAVDsCommand = jest.genMockFromModule('../tools/EmulatorExec').ListAVDsCommand;
jest.mock('../tools/EmulatorExec', () => ({
ListAVDsCommand: MockListAVDsCommand,
}));

emulatorExec = {
exec: jest.fn().mockResolvedValue(''),
};

const AVDsResolver = require('./AVDsResolver');
uut = new AVDsResolver(emulatorExec);
});

it('should exec command', async () => {
await uut.resolve();
expect(emulatorExec.exec).toHaveBeenCalledWith(expect.any(MockListAVDsCommand));
});

it('should parse emulators list given as text', async () => {
emulatorExec.exec.mockResolvedValue('avd-device1\navd-device2\n');
expect(await uut.resolve()).toEqual(['avd-device1', 'avd-device2']);
});
});
Original file line number Diff line number Diff line change
@@ -1,33 +1,18 @@
const _ = require('lodash');
const fs = require('fs');
const Tail = require('tail').Tail;
const { LaunchCommand } = require('../tools/EmulatorExec');
const unitLogger = require('../../../../utils/logger').child({ __filename });
const retry = require('../../../../utils/retry');
const {
EmulatorExec,
ListAVDsCommand,
QueryVersionCommand,
LaunchCommand,
} = require('./EmulatorExec');

const isUnknownEmulatorError = (err) => (err.message || '').includes('failed with code null');

class Emulator {
constructor() {
this.emulatorExec = new EmulatorExec();
class EmulatorLauncher {
constructor(emulatorExec) {
this._emulatorExec = emulatorExec;
}

async listAvds() {
const output = await this.emulatorExec.exec(new ListAVDsCommand());
const avds = output.trim().split('\n');
return avds;
}

async queryVersion() {
return await this.emulatorExec.exec(new QueryVersionCommand());
}

async boot(emulatorName, options = {port: undefined}) {
async launch(emulatorName, options = {port: undefined}) {
const launchCommand = new LaunchCommand(emulatorName, options);

return await retry({
Expand Down Expand Up @@ -70,8 +55,8 @@ class Emulator {
}

let log = unitLogger.child({ fn: 'boot' });
log.debug({ event: 'SPAWN_CMD' }, this.emulatorExec.toString(), launchCommand.toString());
const childProcessPromise = this.emulatorExec.spawn(launchCommand, stdout, stderr);
log.debug({ event: 'SPAWN_CMD' }, this._emulatorExec.toString(), launchCommand.toString());
const childProcessPromise = this._emulatorExec.spawn(launchCommand, stdout, stderr);
childProcessPromise.childProcess.unref();
log = log.child({ child_pid: childProcessPromise.childProcess.pid });

Expand All @@ -93,4 +78,4 @@ class Emulator {
}
}

module.exports = Emulator;
module.exports = EmulatorLauncher;
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
const log = require('../../../utils/logger').child({ __filename });
const {QueryVersionCommand} = require('../tools/EmulatorExec');
const log = require('../../../../utils/logger').child({ __filename });

const EMU_BIN_VERSION_DETECT_EV = 'EMU_BIN_VERSION_DETECT';

class EmulatorVersion {
constructor(emulator) {
this.emulator = emulator;
class EmulatorVersionResolver {
constructor(emulatorExec) {
this._emulatorExec = emulatorExec;
this.version = undefined;
}

Expand All @@ -16,7 +17,7 @@ class EmulatorVersion {
}

async _resolve() {
const rawOutput = await this.emulator.queryVersion() || '';
const rawOutput = await this._emulatorExec.exec(new QueryVersionCommand()) || '';
const matches = rawOutput.match(/Android emulator version ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]*)/);
if (!matches) {
log.warn({ event: EMU_BIN_VERSION_DETECT_EV, success: false }, 'Could not detect emulator binary version, got:', rawOutput);
Expand All @@ -37,4 +38,4 @@ class EmulatorVersion {
};
}
}
module.exports = EmulatorVersion;
module.exports = EmulatorVersionResolver;
Loading

0 comments on commit 2126032

Please sign in to comment.