detox
Version:
E2E tests and automation for mobile
172 lines (148 loc) • 5.57 kB
JavaScript
/**
* @typedef {import('../../AllocationDriverBase').AllocationDriverBase} AllocationDriverBase
* @typedef {import('../../../../common/drivers/android/cookies').GenycloudEmulatorCookie} GenycloudEmulatorCookie
*/
const Timer = require('../../../../../utils/Timer');
const log = require('../../../../../utils/logger').child({ cat: 'device' });
const GenyRegistry = require('./GenyRegistry');
const events = {
GENYCLOUD_INIT: { event: 'GENYCLOUD_INIT' },
GENYCLOUD_TEARDOWN: { event: 'GENYCLOUD_TEARDOWN' },
};
/**
* @implements {AllocationDriverBase}
*/
class GenyAllocDriver {
/**
* @param {object} options
* @param {import('../../../../common/drivers/android/exec/ADB')} options.adb
* @param {DetoxInternals.SessionState} options.detoxSession
* @param {import('./GenyRegistry')} options.genyRegistry
* @param {import('./GenyInstanceLauncher')} options.instanceLauncher
* @param {import('./GenyRecipeQuerying')} options.recipeQuerying
*/
constructor({
adb,
detoxSession,
genyRegistry = new GenyRegistry(),
instanceLauncher,
recipeQuerying,
}) {
this._adb = adb;
this._detoxSessionId = detoxSession.id;
this._genyRegistry = genyRegistry;
this._instanceLauncher = instanceLauncher;
this._recipeQuerying = recipeQuerying;
this._instanceCounter = 0;
}
async init() {
try {
await this._adb.startDaemon();
} catch (error) {
log.warn({ ...events.GENYCLOUD_INIT, error }, 'ADB server start failed; error ignored');
}
}
/**
* @param deviceConfig { Object }
* @return {Promise<GenycloudEmulatorCookie>}
*/
async allocate(deviceConfig) {
const deviceQuery = deviceConfig.device;
const recipe = await this._recipeQuerying.getRecipeFromQuery(deviceQuery);
let instance = this._genyRegistry.findFreeInstance(recipe);
if (!instance) {
const instanceName = `Detox.${this._detoxSessionId}.${this._instanceCounter++}`;
instance = await this._instanceLauncher.launch(recipe, instanceName);
this._genyRegistry.addInstance(instance, recipe);
}
return {
id: instance.uuid,
adbName: instance.adbName,
name: instance.name,
instance,
};
}
/**
* @param {GenycloudEmulatorCookie} cookie
*/
async postAllocate(cookie) {
const instance = await this._instanceLauncher.connect(cookie.instance);
this._genyRegistry.updateInstance(instance);
if (this._genyRegistry.pollNewInstance(instance.uuid)) {
const { adbName } = instance;
await Timer.run(20000, 'waiting for device to respond', async () => {
await this._adb.disableAndroidAnimations(adbName);
await this._adb.setWiFiToggle(adbName, true);
await this._adb.apiLevel(adbName);
});
}
return {
...cookie,
adbName: instance.adbName,
};
}
/**
* @param cookie {Omit<GenycloudEmulatorCookie, 'instance'>}
* @param options {Partial<import('../../AllocationDriverBase').DeallocOptions>}
* @return {Promise<void>}
*/
async free(cookie, options = {}) {
try {
if (!options.shutdown) {
await Timer.run(10000, 'waiting for device to respond', async () => {
await this._adb.shell(cookie.adbName, 'echo ok');
});
}
} catch {
options.shutdown = true;
}
// Known issue: cookie won't have a proper 'instance' field due to (de)serialization
if (options.shutdown) {
this._genyRegistry.removeInstance(cookie.id);
await this._instanceLauncher.shutdown(cookie.id);
} else {
this._genyRegistry.markAsFree(cookie.id);
}
}
async cleanup() {
log.info(events.GENYCLOUD_TEARDOWN, 'Initiating Genymotion SaaS instances teardown...');
const killPromises = this._genyRegistry.getInstances().map((instance) => {
this._genyRegistry.markAsBusy(instance.uuid);
const onSuccess = () => this._genyRegistry.removeInstance(instance.uuid);
const onError = (error) => ({ ...instance, error });
return this._instanceLauncher.shutdown(instance.uuid).then(onSuccess, onError);
});
const deletionLeaks = (await Promise.all(killPromises)).filter(Boolean);
this._reportGlobalCleanupSummary(deletionLeaks);
}
/**
* The current error we could recover from in the context of Genymotion Cloud is when the device is not found.
* The error message will contain the following text adb: device 'localhost:xxxxx' not found
* @param error
* @returns {boolean}
*/
isRecoverableError(error) {
const errorStr = JSON.stringify(error);
return errorStr.indexOf('adb: device \'localhost:') !== -1;
}
emergencyCleanup() {
const instances = this._genyRegistry.getInstances();
this._reportGlobalCleanupSummary(instances);
}
_reportGlobalCleanupSummary(deletionLeaks) {
if (deletionLeaks.length) {
log.warn(events.GENYCLOUD_TEARDOWN, 'WARNING! Detected a Genymotion SaaS instance leakage, for the following instances:');
deletionLeaks.forEach(({ uuid, name, error }) => {
log.warn(events.GENYCLOUD_TEARDOWN, [
`Instance ${name} (${uuid})${error ? `: ${error}` : ''}`,
` Kill it by visiting https://cloud.geny.io/instance/${uuid}, or by running:`,
` gmsaas instances stop ${uuid}`,
].join('\n'));
});
log.info(events.GENYCLOUD_TEARDOWN, 'Instances teardown completed with warnings');
} else {
log.info(events.GENYCLOUD_TEARDOWN, 'Instances teardown completed successfully');
}
}
}
module.exports = GenyAllocDriver;