UNPKG

detox

Version:

E2E tests and automation for mobile

172 lines (148 loc) 5.57 kB
/** * @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;