UNPKG

detox

Version:

E2E tests and automation for mobile

355 lines (295 loc) 10.4 kB
const { URL } = require('url'); const fs = require('fs-extra'); const onSignalExit = require('signal-exit'); const temporary = require('../artifacts/utils/temporaryPath'); const { DetoxRuntimeError } = require('../errors'); const SessionState = require('../ipc/SessionState'); const { getCurrentCommand } = require('../utils/argparse'); const retry = require('../utils/retry'); const uuid = require('../utils/uuid'); const DetoxContext = require('./DetoxContext'); const symbols = require('./symbols'); // Protected symbols const { $logFinalizer, $restoreSessionState, $sessionState, $worker } = DetoxContext.protected; //#region Private symbols const _ipcServer = Symbol('ipcServer'); const _wss = Symbol('wss'); const _dirty = Symbol('dirty'); const _emergencyTeardown = Symbol('emergencyTeardown'); const _lifecycleLogger = Symbol('lifecycleLogger'); const _sessionFile = Symbol('sessionFile'); const _logFinalError = Symbol('logFinalError'); const _cookieAllocators = Symbol('cookieAllocators'); const _deviceAllocators = Symbol('deviceAllocators'); const _createDeviceAllocator = Symbol('createDeviceAllocator'); const _createDeviceAllocatorInstance = Symbol('createDeviceAllocatorInstance'); const _allocateDeviceOnce = Symbol('allocateDeviceOnce'); //#endregion class DetoxPrimaryContext extends DetoxContext { constructor() { super(); this[_dirty] = false; this[_wss] = null; this[_cookieAllocators] = {}; this[_deviceAllocators] = {}; /** Path to file where the initial session object is serialized */ this[_sessionFile] = ''; /** * @type {import('../ipc/IPCServer') | null} */ this[_ipcServer] = null; /** @type {Detox.Logger} */ this[_lifecycleLogger] = this[symbols.logger].child({ cat: 'lifecycle' }); } //#region Internal members async [symbols.reportTestResults](testResults) { if (this[_ipcServer]) { this[_ipcServer].onReportTestResults({ testResults }); } } [symbols.conductEarlyTeardown] = async (permanent = false) => { if (this[_ipcServer]) { await this[_ipcServer].onConductEarlyTeardown({ permanent }); } }; async [symbols.resolveConfig](opts = {}) { const session = this[$sessionState]; if (!session.detoxConfig) { const configuration = require('../configuration'); session.detoxConfig = await configuration.composeDetoxConfig(opts); } return session.detoxConfig; } /** * @override * @param {Partial<DetoxInternals.DetoxInitOptions>} [opts] */ async [symbols.init](opts = {}) { if (this[_dirty]) { throw new DetoxRuntimeError({ message: 'Cannot initialize primary Detox context more than once.', hint: DetoxRuntimeError.reportIssueIfJest, }); } this[_dirty] = true; onSignalExit(this[_emergencyTeardown]); const detoxConfig = await this[symbols.resolveConfig](opts); const { logger: loggerConfig, session: sessionConfig } = detoxConfig; await this[symbols.logger].setConfig(loggerConfig); this[_lifecycleLogger].trace.begin({ cwd: process.cwd(), data: this[$sessionState], }, getCurrentCommand()); const IPCServer = require('../ipc/IPCServer'); this[_ipcServer] = new IPCServer({ sessionState: this[$sessionState], logger: this[symbols.logger], callbacks: { onAllocateDevice: this[symbols.allocateDevice].bind(this), onDeallocateDevice: this[symbols.deallocateDevice].bind(this), }, }); await this[_ipcServer].init(); const DetoxServer = require('../server/DetoxServer'); if (sessionConfig.autoStart) { this[_wss] = new DetoxServer({ port: sessionConfig.server ? new URL(sessionConfig.server).port : 0, standalone: false, }); await this[_wss].open(); } if (!sessionConfig.server && this[_wss]) { // @ts-ignore sessionConfig.server = `ws://localhost:${this[_wss].port}`; } this[_sessionFile] = temporary.for.json(this[$sessionState].id); await fs.writeFile(this[_sessionFile], this[$sessionState].stringify()); process.env.DETOX_CONFIG_SNAPSHOT_PATH = this[_sessionFile]; this[_lifecycleLogger].trace(`Serialized the session state at: ${this[_sessionFile]}`); if (opts.workerId !== null) { await this[symbols.installWorker](opts); } } /** * @override * @param {Partial<DetoxInternals.DetoxInstallWorkerOptions>} [opts] */ async [symbols.installWorker](opts = {}) { const workerId = opts.workerId || 'worker'; this[$sessionState].workerId = workerId; this[_ipcServer].onRegisterWorker({ workerId }); await super[symbols.installWorker]({ ...opts, workerId }); } /** @override */ async [symbols.allocateDevice](deviceConfig) { const deviceAllocator = await this[_createDeviceAllocator](deviceConfig); const retryOptions = { backoff: 'none', retries: 5, interval: 25000, conditionFn: (e) => deviceAllocator.isRecoverableError(e), }; return await retry(retryOptions, async () => { return await this[_allocateDeviceOnce](deviceAllocator, deviceConfig); }); } async [_allocateDeviceOnce](deviceAllocator, deviceConfig) { const deviceCookie = await deviceAllocator.allocate(deviceConfig); this[_cookieAllocators][deviceCookie.id] = deviceAllocator; try { return await deviceAllocator.postAllocate(deviceCookie); } catch (e) { try { await deviceAllocator.free(deviceCookie, { shutdown: true }); } catch (e2) { this[symbols.logger].error({ cat: 'device', err: e2 }, `Failed to free ${deviceCookie.name || deviceCookie.id} after a failed allocation attempt`); } finally { delete this[_cookieAllocators][deviceCookie.id]; } throw e; } } /** @override */ async [symbols.deallocateDevice](cookie) { const deviceAllocator = this[_cookieAllocators][cookie.id]; if (!deviceAllocator) { throw new DetoxRuntimeError({ message: `Cannot deallocate device ${cookie.id} because it was not allocated by this context.`, hint: `See the actually known allocated devices below:`, debugInfo: Object.keys(this[_cookieAllocators]).map(id => `- ${id}`).join('\n'), }); } await deviceAllocator.free(cookie); delete this[_cookieAllocators][cookie.id]; } /** @override */ async [symbols.cleanup]() { try { if (this[$worker]) { await this[symbols.uninstallWorker](); } } finally { for (const key of Object.keys(this[_deviceAllocators])) { const deviceAllocator = this[_deviceAllocators][key]; delete this[_deviceAllocators][key]; try { await deviceAllocator.cleanup(); } catch (err) { this[symbols.logger].error({ cat: 'device', err }, `Failed to cleanup the device allocation driver for ${key}`); } } this[_cookieAllocators] = {}; if (this[_wss]) { await this[_wss].close(); this[_wss] = null; } if (this[_ipcServer]) { await this[_ipcServer].dispose(); this[_ipcServer] = null; } if (this[_sessionFile]) { await fs.remove(this[_sessionFile]); } if (this[_dirty]) { try { this[_lifecycleLogger].trace.end(); await this[symbols.logger].close(); await this[$logFinalizer].finalize(); } catch (err) { this[_logFinalError](err); } } } } [_emergencyTeardown] = (_code, signal) => { if (!signal) { return; } for (const key of Object.keys(this[_deviceAllocators])) { const deviceAllocator = this[_deviceAllocators][key]; delete this[_deviceAllocators][key]; try { deviceAllocator.emergencyCleanup(); } catch (err) { this[symbols.logger].error({ cat: 'device', err }, `Failed to clean up the device allocation driver for ${key} in emergency mode`); } } this[_cookieAllocators] = {}; if (this[_wss]) { this[_wss].close(); } if (this[_ipcServer]) { this[_ipcServer].dispose(); } if (this[_sessionFile]) { fs.removeSync(this[_sessionFile]); } try { this[_lifecycleLogger].trace.end({ abortSignal: signal }); this[symbols.logger].close().catch(this[_logFinalError]); this[$logFinalizer].finalizeSync(); } catch (err) { this[_logFinalError](err); } }; /** @param {Detox.DetoxDeviceConfig} deviceConfig */ [_createDeviceAllocator] = async (deviceConfig) => { const deviceType = deviceConfig.type; const deviceAllocator = this[_createDeviceAllocatorInstance](deviceConfig); try { await deviceAllocator.init(); } catch (e) { try { delete this[_deviceAllocators][deviceType]; await deviceAllocator.cleanup(); } catch (e2) { this[symbols.logger].error({ cat: 'device', err: e2 }, `Failed to cleanup the device allocation driver for ${deviceType} after a failed initialization`); } throw e; } return this[_deviceAllocators][deviceType]; }; /** * @param {Detox.DetoxDeviceConfig} deviceConfig * @returns { DeviceAllocator } */ [_createDeviceAllocatorInstance] = (deviceConfig) => { const deviceType = deviceConfig.type; if (!this[_deviceAllocators][deviceType]) { const environmentFactory = require('../environmentFactory'); const { deviceAllocatorFactory } = environmentFactory.createFactories(deviceConfig); const { detoxConfig } = this[$sessionState]; this[_deviceAllocators][deviceType] = deviceAllocatorFactory.createDeviceAllocator({ detoxConfig, detoxSession: this[$sessionState] }); } return this[_deviceAllocators][deviceType]; }; [_logFinalError] = (err) => { this[_lifecycleLogger].error(err, 'Encountered an error while merging the process logs:'); }; //#endregion //#region Protected members /** * @protected * @override * @return {SessionState} */ [$restoreSessionState]() { return new SessionState({ id: uuid.UUID(), detoxIPCServer: `primary-${process.pid}`, }); } //#endregion } module.exports = DetoxPrimaryContext;