Repository: wix/Detox
Pull request #4290 (2023-11-23 13:12:53)

refactor: dynamic device allocation
## Description

This is an internal pull request to enable driver-less initialization in the primary context unless it is really needed (when a worker allocates a device).

As an implementation choice, I allow the creation of multiple device allocators simultaneously in the primary context in a lazy fashion. To map device allocator implementation to a specific cookie, we host a small map of {cookie.id}-{deviceAllocator instance}. On the cleanup, they are all destroyed as expected, in a resilient `for-of` loop...

In theory, this pull request should allow picking a device type dynamically right from the test (e.g., via test environment pragmas), but this is not a public API anyway, and we don't recommend such adventures unless you have no other choice. Full-fledged multi-device support will come significantly later. Most importantly, this pull request doesn't stand in the way of the future architecture evolution – Detox device allocation server will be agnostic to "current test file" or "current selected configuration", it will just listen to precise device configurations requested over network and will provide devices accordingly.

d4vidi: @noomorph Could you elaborate on the use case where such a primary-context initialization is needed _right now_? This is actually future proof and so as a bottom line I like this change (seeing that it is not going to cost extra in maintenance). Nevertheless, I'd be happy to better understand the current context.
noomorph: > Could you elaborate on the use case where such a primary-context initialization is needed right now? This is actually future proof and so as a bottom line I like this change (seeing that it is not going to cost extra in maintenance). Nevertheless, I'd be happy to better understand the current context.

Yes, sure.

1. The added laziness helps to avoid initialization errors in non-friendly CI environments when we don't even need to actually use this or that allocation driver. You know that I rely on this in my Detox test bundler.

2. The parametrization is not needed right now. However, this should give some freedom for DWP – e.g. if some test suite requires a specific OS version, we'll be able to request an exact device via a custom test environment:

```js
/**
 * Web view tests
 * Due to crashes on iOS 17.x, this test suite should be run on older runtimes
 * @iosVersion 16.4
 */
 
describe('Web view tests', () => {
  // ..
});
```

Patch:
diff --git a/detox/jest.config.js b/detox/jest.config.js
index 0901108b3..6ca2c4a51 100644
--- a/detox/jest.config.js
+++ b/detox/jest.config.js
@@ -74,7 +74,7 @@ module.exports = {
     'runners/jest/testEnvironment',
     'src/DetoxWorker.js',
     'src/logger/utils/streamUtils.js',
-    'src/realms'
+    'src/realms',
   ],
   resetMocks: true,
   resetModules: true,
diff --git a/detox/src/DetoxWorker.js b/detox/src/DetoxWorker.js
index 237ef37da..7d3f69a32 100644
--- a/detox/src/DetoxWorker.js
+++ b/detox/src/DetoxWorker.js
@@ -132,7 +132,7 @@ class DetoxWorker {
     };
 
     this._artifactsManager = artifactsManagerFactory.createArtifactsManager(this._artifactsConfig, commonDeps);
-    this._deviceCookie = yield this._context[symbols.allocateDevice]();
+    this._deviceCookie = yield this._context[symbols.allocateDevice](this._deviceConfig);
 
     this.device = runtimeDeviceFactory.createRuntimeDevice(
       this._deviceCookie,
diff --git a/detox/src/ipc/IPCClient.js b/detox/src/ipc/IPCClient.js
index ca5554361..bcc538432 100644
--- a/detox/src/ipc/IPCClient.js
+++ b/detox/src/ipc/IPCClient.js
@@ -60,8 +60,8 @@ class IPCClient {
     this._sessionState.patch(sessionState);
   }
 
-  async allocateDevice() {
-    const { deviceCookie, error } = deserializeObjectWithError(await this._emit('allocateDevice', {}));
+  async allocateDevice(deviceConfig) {
+    const { deviceCookie, error } = deserializeObjectWithError(await this._emit('allocateDevice', { deviceConfig }));
     if (error) {
       throw error;
     }
diff --git a/detox/src/ipc/IPCServer.js b/detox/src/ipc/IPCServer.js
index 51f4e2f10..eea96f233 100644
--- a/detox/src/ipc/IPCServer.js
+++ b/detox/src/ipc/IPCServer.js
@@ -9,7 +9,7 @@ class IPCServer {
    * @param {import('./SessionState')} options.sessionState
    * @param {Detox.Logger} options.logger
    * @param {object} options.callbacks
-   * @param {() => Promise<any>} options.callbacks.onAllocateDevice
+   * @param {(deviceConfig: DetoxInternals.RuntimeConfig['device']) => Promise<any>} options.callbacks.onAllocateDevice
    * @param {(cookie: any) => Promise<void>} options.callbacks.onDeallocateDevice
    */
   constructor({ sessionState, logger, callbacks }) {
@@ -104,11 +104,11 @@ class IPCServer {
     this._ipc.server.broadcast('sessionStateUpdate', newState);
   }
 
-  async onAllocateDevice(_payload, socket) {
+  async onAllocateDevice({ deviceConfig }, socket) {
     let deviceCookie;
 
     try {
-      deviceCookie = await this._callbacks.onAllocateDevice();
+      deviceCookie = await this._callbacks.onAllocateDevice(deviceConfig);
       this._ipc.server.emit(socket, 'allocateDeviceDone', { deviceCookie });
     } catch (error) {
       this._ipc.server.emit(socket, 'allocateDeviceDone', serializeObjectWithError({ error }));
diff --git a/detox/src/realms/DetoxContext.js b/detox/src/realms/DetoxContext.js
index 67c6d6ea4..93d45f882 100644
--- a/detox/src/realms/DetoxContext.js
+++ b/detox/src/realms/DetoxContext.js
@@ -152,10 +152,10 @@ class DetoxContext {
   }
 
   /** @abstract */
-  async [symbols.allocateDevice]() {}
+  async [symbols.allocateDevice](_deviceConfig) {}
 
   /** @abstract */
-  async [symbols.deallocateDevice]() {}
+  async [symbols.deallocateDevice](_deviceCookie) {}
 
   async [symbols.uninstallWorker]() {
     try {
diff --git a/detox/src/realms/DetoxPrimaryContext.js b/detox/src/realms/DetoxPrimaryContext.js
index feb061fdb..5db099591 100644
--- a/detox/src/realms/DetoxPrimaryContext.js
+++ b/detox/src/realms/DetoxPrimaryContext.js
@@ -23,7 +23,9 @@ const _emergencyTeardown = Symbol('emergencyTeardown');
 const _lifecycleLogger = Symbol('lifecycleLogger');
 const _sessionFile = Symbol('sessionFile');
 const _logFinalError = Symbol('logFinalError');
-const _deviceAllocator = Symbol('deviceAllocator');
+const _cookieAllocators = Symbol('cookieAllocators');
+const _deviceAllocators = Symbol('deviceAllocators');
+const _createDeviceAllocator = Symbol('createDeviceAllocator');
 //#endregion
 
 class DetoxPrimaryContext extends DetoxContext {
@@ -32,7 +34,8 @@ class DetoxPrimaryContext extends DetoxContext {
 
     this[_dirty] = false;
     this[_wss] = null;
-    this[_deviceAllocator] = null;
+    this[_cookieAllocators] = {};
+    this[_deviceAllocators] = {};
 
     /** Path to file where the initial session object is serialized */
     this[_sessionFile] = '';
@@ -85,7 +88,6 @@ class DetoxPrimaryContext extends DetoxContext {
     const detoxConfig = await this[symbols.resolveConfig](opts);
 
     const {
-      device: deviceConfig,
       logger: loggerConfig,
       session: sessionConfig
     } = detoxConfig;
@@ -109,16 +111,6 @@ class DetoxPrimaryContext extends DetoxContext {
 
     await this[_ipcServer].init();
 
-    const environmentFactory = require('../environmentFactory');
-
-    const { deviceAllocatorFactory } = environmentFactory.createFactories(deviceConfig);
-    this[_deviceAllocator] = deviceAllocatorFactory.createDeviceAllocator({
-      detoxConfig,
-      detoxSession: this[$sessionState],
-    });
-
-    await this[_deviceAllocator].init();
-
     // TODO: Detox-server creation ought to be delegated to a generator/factory.
     const DetoxServer = require('../server/DetoxServer');
     if (sessionConfig.autoStart) {
@@ -162,17 +154,20 @@ class DetoxPrimaryContext extends DetoxContext {
   }
 
   /** @override */
-  async [symbols.allocateDevice]() {
-    const { device } = this[$sessionState].detoxConfig;
-    const deviceCookie = await this[_deviceAllocator].allocate(device);
+  async [symbols.allocateDevice](deviceConfig) {
+    const deviceAllocator = await this[_createDeviceAllocator](deviceConfig);
+    const deviceCookie = await deviceAllocator.allocate(deviceConfig);
+    this[_cookieAllocators][deviceCookie.id] = deviceAllocator;
 
     try {
-      return await this[_deviceAllocator].postAllocate(deviceCookie);
+      return await deviceAllocator.postAllocate(deviceCookie);
     } catch (e) {
       try {
-        await this[_deviceAllocator].free(deviceCookie, { shutdown: true });
+        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`);
+        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;
@@ -181,7 +176,17 @@ class DetoxPrimaryContext extends DetoxContext {
 
   /** @override */
   async [symbols.deallocateDevice](cookie) {
-    await this[_deviceAllocator].free(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 */
@@ -191,11 +196,18 @@ class DetoxPrimaryContext extends DetoxContext {
         await this[symbols.uninstallWorker]();
       }
     } finally {
-      if (this[_deviceAllocator]) {
-        await this[_deviceAllocator].cleanup();
-        this[_deviceAllocator] = null;
+      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;
@@ -227,11 +239,18 @@ class DetoxPrimaryContext extends DetoxContext {
       return;
     }
 
-    if (this[_deviceAllocator]) {
-      this[_deviceAllocator].emergencyCleanup();
-      this[_deviceAllocator] = null;
+    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();
     }
@@ -253,6 +272,35 @@ class DetoxPrimaryContext extends DetoxContext {
     }
   };
 
+  /** @param {Detox.DetoxDeviceConfig} deviceConfig */
+  [_createDeviceAllocator] = async (deviceConfig) => {
+    const deviceType = deviceConfig.type;
+    if (!this[_deviceAllocators][deviceType]) {
+      const environmentFactory = require('../environmentFactory');
+      const { deviceAllocatorFactory } = environmentFactory.createFactories(deviceConfig);
+      const { detoxConfig } = this[$sessionState];
+      const deviceAllocator = deviceAllocatorFactory.createDeviceAllocator({
+        detoxConfig,
+        detoxSession: this[$sessionState],
+      });
+
+      try {
+        await deviceAllocator.init();
+        this[_deviceAllocators][deviceType] = deviceAllocator;
+      } catch (e) {
+        try {
+          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];
+  };
+
   [_logFinalError] = (err) => {
     this[_lifecycleLogger].error(err, 'Encountered an error while merging the process logs:');
   };
diff --git a/detox/src/realms/DetoxSecondaryContext.js b/detox/src/realms/DetoxSecondaryContext.js
index 108daf29c..e5795f1ef 100644
--- a/detox/src/realms/DetoxSecondaryContext.js
+++ b/detox/src/realms/DetoxSecondaryContext.js
@@ -63,9 +63,9 @@ class DetoxSecondaryContext extends DetoxContext {
   }
 
   /** @override */
-  async [symbols.allocateDevice]() {
+  async [symbols.allocateDevice](deviceConfig) {
     if (this[_ipcClient]) {
-      const deviceCookie = await this[_ipcClient].allocateDevice();
+      const deviceCookie = await this[_ipcClient].allocateDevice(deviceConfig);
       return deviceCookie;
     } else {
       throw new DetoxInternalError('Detected an attempt to allocate a device using a non-initialized context.');


Test patch:
diff --git a/detox/src/DetoxWorker.test.js b/detox/src/DetoxWorker.test.js
index 4fe220fac..9946e7d1a 100644
--- a/detox/src/DetoxWorker.test.js
+++ b/detox/src/DetoxWorker.test.js
@@ -133,7 +133,9 @@ describe('DetoxWorker', () => {
         expect(envValidator.validate).toHaveBeenCalled());
 
       it('should allocate a device', () => {
-        expect(detoxContext[symbols.allocateDevice]).toHaveBeenCalledWith();
+        expect(detoxContext[symbols.allocateDevice]).toHaveBeenCalledWith(expect.objectContaining({
+          type: 'fake.device',
+        }));
       });
 
       it('should create a runtime-device based on the allocation result (cookie)', () =>
diff --git a/detox/src/ipc/ipc.test.js b/detox/src/ipc/ipc.test.js
index 29f70758b..6e33cd97f 100644
--- a/detox/src/ipc/ipc.test.js
+++ b/detox/src/ipc/ipc.test.js
@@ -362,18 +362,22 @@ describe('IPC', () => {
   });
 
   describe('integration', () => {
+    const deviceConfig = { type: 'stub.device' };
+
     beforeEach(() => ipcServer.init());
     beforeEach(() => ipcClient1.init());
 
     describe('onAllocateDevice', () => {
       it('should allocate a device and return its cookie', async () => {
         callbacks.onAllocateDevice.mockResolvedValue({ id: 'device-1' });
-        await expect(ipcClient1.allocateDevice()).resolves.toEqual({ id: 'device-1' });
+        await expect(ipcClient1.allocateDevice(deviceConfig)).resolves.toEqual({ id: 'device-1' });
+        expect(callbacks.onAllocateDevice).toHaveBeenCalledWith(deviceConfig);
       });
 
       it('should return an error if allocation fails', async () => {
         callbacks.onAllocateDevice.mockRejectedValue(new Error('foo'));
-        await expect(ipcClient1.allocateDevice()).rejects.toThrow('foo');
+        await expect(ipcClient1.allocateDevice(deviceConfig)).rejects.toThrow('foo');
+        expect(callbacks.onAllocateDevice).toHaveBeenCalledWith(deviceConfig);
       });
     });
 
diff --git a/detox/src/realms/DetoxPrimaryContext.test.js b/detox/src/realms/DetoxPrimaryContext.test.js
index de374c853..3f373f09b 100644
--- a/detox/src/realms/DetoxPrimaryContext.test.js
+++ b/detox/src/realms/DetoxPrimaryContext.test.js
@@ -53,15 +53,22 @@ describe('DetoxPrimaryContext', () => {
   let DetoxWorker;
   //#endregion
 
+  /** @type {import('./DetoxPrimaryContext')} */
+  let context;
   /** @type {import('./DetoxInternalsFacade')} */
   let facade;
+  /** @type {import('./symbols')} */
+  let symbols;
 
   const detoxServer = () => latestInstanceOf(DetoxServer);
   const ipcServer = () => latestInstanceOf(IPCServer);
   const detoxWorker = () => latestInstanceOf(DetoxWorker);
+  // @ts-ignore
+  const log = () => logger.DetoxLogger.instances[0];
   const logFinalizer = () => latestInstanceOf(logger.DetoxLogFinalizer);
   const getSignalHandler = () => lastCallTo(signalExit)[FIRST_ARGUMENT];
   const facadeInit = () => facade.init({ workerId: null });
+  const facadeInitWithWorker = async () => facade.init({ workerId: WORKER_ID });
 
   backupProcessEnv();
 
@@ -72,8 +79,9 @@ describe('DetoxPrimaryContext', () => {
     const DetoxPrimaryContext = require('./DetoxPrimaryContext');
     const DetoxInternalsFacade = require('./DetoxInternalsFacade');
 
-    const context = new DetoxPrimaryContext();
+    context = new DetoxPrimaryContext();
     facade = new DetoxInternalsFacade(context);
+    symbols = require('./symbols');
   });
 
   describe('when not initialized', () => {
@@ -94,12 +102,12 @@ describe('DetoxPrimaryContext', () => {
     });
   });
 
-  describe('when initializing', () => {
+  describe('when initialized', () => {
+    beforeEach(facadeInit);
+
     it('should create an IPC server with a valid session state', async () => {
       const expectedIPCServerName = `primary-${process.pid}`;
 
-      await facadeInit();
-
       expect(IPCServer).toHaveBeenCalledWith(expect.objectContaining({
         sessionState: expect.objectContaining({
           id: expect.stringMatching(UUID_REGEXP),
@@ -109,56 +117,10 @@ describe('DetoxPrimaryContext', () => {
     });
 
     it('should init the IPC server', async () => {
-      await facadeInit();
       expect(ipcServer().init).toHaveBeenCalled();
     });
 
-    it('should init the device allocation driver', async () => {
-      await facadeInit();
-      expect(deviceAllocator.init).toHaveBeenCalled();
-    });
-
-    describe('given detox-server auto-start enabled via config', () => {
-      beforeEach(() => detoxConfigDriver.givenDetoxServerAutostart());
-
-      it('should create the Detox server', async () => {
-        const expectedServerArgs = {
-          port: 0,
-          standalone: false,
-        };
-
-        await facadeInit();
-        expect(DetoxServer).toHaveBeenCalledWith(expectedServerArgs);
-      });
-
-      it('should create the Detox server based on a specified port', async () => {
-        const port = '666';
-        detoxConfigDriver.givenDetoxServerPort(port);
-
-        const expectedServerArgs = {
-          port,
-          standalone: false,
-        };
-        await facadeInit();
-        expect(DetoxServer).toHaveBeenCalledWith(expectedServerArgs);
-      });
-
-      it('should start the server', async () => {
-        await facadeInit();
-        expect(detoxServer().open).toHaveBeenCalled();
-      });
-    });
-
-    describe('given detox-server auto-start disabled via config', () => {
-      it('should not create a server', async () => {
-        await facadeInit();
-        expect(DetoxServer).not.toHaveBeenCalled();
-      });
-    });
-
     it('should save the session state onto the context-shared file', async () => {
-      await facadeInit();
-
       expect(fs.writeFile).toHaveBeenCalledWith(
         expect.stringMatching(TEMP_FILE_REGEXP),
         expect.any(String),
@@ -171,150 +133,182 @@ describe('DetoxPrimaryContext', () => {
     });
 
     it('should export context-shared file via DETOX_CONFIG_SNAPSHOT_PATH', async () => {
-      await facadeInit();
-
       expect(process.env.DETOX_CONFIG_SNAPSHOT_PATH).toBeDefined();
       expect(process.env.DETOX_CONFIG_SNAPSHOT_PATH).toMatch(TEMP_FILE_REGEXP);
     });
 
-    it('should install a worker if called without options', async () => {
-      await facade.init();
-      expect(facade.session).toEqual(expect.objectContaining({ workerId: 'worker' }));
-      expect(detoxWorker().init).toHaveBeenCalled();
+    it('should reject further initializations', async () => {
+      await expect(() => facadeInit()).rejects.toThrowErrorMatchingSnapshot();
     });
 
-    it('should install a worker if worker ID has been specified', async () => {
-      await facade.init({ workerId: WORKER_ID });
-      expect(facade.session).toEqual(expect.objectContaining({ workerId: WORKER_ID }));
-      expect(detoxWorker().init).toHaveBeenCalled();
+    it('should change status to "active"', async () => {
+      expect(facade.getStatus()).toBe('active');
     });
 
-    it('should register the worker at the IPC server\'s', async () => {
-      await facade.init({ workerId: WORKER_ID });
-      expect(ipcServer().onRegisterWorker).toHaveBeenCalledWith({ workerId: WORKER_ID });
-    });
+    describe('when a device is being allocated', () => {
+      let cookie;
 
-    describe('given an initialization failure', () => {
-      it('should report status as "init"', async () => {
-        IPCServer.prototype.init = jest.fn().mockRejectedValue(new Error('init failed'));
+      beforeEach(async () => {
+        cookie = await allocateSomeDevice();
+      });
 
-        await expect(() => facadeInit()).rejects.toThrow();
-        expect(facade.getStatus()).toBe('init');
+      it('should return a cookie', async () => {
+        expect(cookie).toEqual({ id: 'a-device-id' });
       });
-    });
-  });
 
-  describe('when initialized', () => {
-    it('should reject further initializations', async () => {
-      await facadeInit();
-      await expect(() => facadeInit()).rejects.toThrowErrorMatchingSnapshot();
-    });
+      it('should call the device allocator', async () => {
+        expect(deviceAllocator.init).toHaveBeenCalled();
+        expect(deviceAllocator.allocate).toHaveBeenCalled();
+        expect(deviceAllocator.postAllocate).toHaveBeenCalled();
+      });
 
-    it('should change status to "active"', async () => {
-      await facadeInit();
-      expect(facade.getStatus()).toBe('active');
-    });
+      it('can be deallocated', async () => {
+        await expect(deallocateDevice(cookie)).resolves.toBeUndefined();
+      });
 
-    describe('then cleaned-up', () => {
-      it('should uninstall an assigned worker', async () => {
-        await facade.init({ workerId: WORKER_ID });
-        await facade.cleanup();
+      it('should throw on attempt to deallocate a cookie that does not belong to this context', async () => {
+        await expect(deallocateDevice({ id: 'some-other-device' })).rejects.toThrowErrorMatchingSnapshot();
+      });
 
-        expect(detoxWorker().cleanup).toHaveBeenCalled();
+      it('cannot be deallocated twice', async () => {
+        await deallocateDevice(cookie);
+        await expect(deallocateDevice(cookie)).rejects.toThrowError(/Cannot deallocate device/);
       });
 
-      it('should clean up the allocation driver', async () => {
-        await facadeInit();
-        await facade.cleanup();
+      describe('and then the context has been cleaned up', () => {
+        beforeEach(async () => {
+          await facade.cleanup();
+        });
 
-        expect(deviceAllocator.cleanup).toHaveBeenCalled();
+        it('should clean up the allocation driver', async () => {
+          expect(deviceAllocator.cleanup).toHaveBeenCalled();
+        });
+
+        it('should not be able to find that cookie anymore', async () => {
+          await expect(deallocateDevice(cookie)).rejects.toThrowError(/Cannot deallocate device/);
+        });
       });
 
-      it('should close the detox server', async () => {
-        detoxConfigDriver.givenDetoxServerAutostart();
+      describe('and then the context has been cleaned up with an allocator cleanup error', () => {
+        let error = new Error('cleanup failed');
 
-        await facadeInit();
-        await facade.cleanup();
+        beforeEach(async () => {
+          deviceAllocator.cleanup.mockRejectedValue(error);
+        });
 
-        expect(detoxServer().close).toHaveBeenCalled();
+        it('should log the error but not throw', async () => {
+          await expect(facade.cleanup()).resolves.toBeUndefined();
+          expect(log().error).toHaveBeenCalledWith({ cat: 'device', err: error }, `Failed to cleanup the device allocation driver for some.device`);
+        });
       });
 
-      it('should close the ipc server', async () => {
-        await facadeInit();
-        await facade.cleanup();
+      describe('on emergency context cleanup', () => {
+        beforeEach(async () => {
+          const signalHandler = getSignalHandler();
+          signalHandler(123, 'SIGSMT');
+        });
 
-        expect(ipcServer().dispose).toHaveBeenCalled();
+        it('should call emergencyCleanup in allocation driver', async () => {
+          expect(deviceAllocator.emergencyCleanup).toHaveBeenCalled();
+        });
       });
 
-      it('should delete the context-shared file', async () => {
-        await facadeInit();
-        await facade.cleanup();
+      describe('on emergency context cleanup with an allocator cleanup error', () => {
+        let error = new Error('cleanup failed');
 
-        expect(fs.remove).toHaveBeenCalledWith(expect.stringMatching(TEMP_FILE_REGEXP));
+        beforeEach(async () => {
+          deviceAllocator.emergencyCleanup.mockImplementation(() => { throw error; });
+        });
+
+        it('should log the error but not throw', async () => {
+          const signalHandler = getSignalHandler();
+          expect(() => signalHandler(123, 'SIGSMT')).not.toThrow();
+          expect(log().error).toHaveBeenCalledWith({ cat: 'device', err: error }, `Failed to clean up the device allocation driver for some.device in emergency mode`);
+        });
       });
+    });
 
-      it('should finalize the logger', async () => {
-        await facadeInit();
-        await facade.cleanup();
-        expect(logFinalizer().finalize).toHaveBeenCalled();
+    describe('when a device is being allocated using a faulty driver', () => {
+      beforeEach(() => {
+        deviceAllocator.init.mockRejectedValue(new Error('init failed'));
       });
 
-      it('should change intermediate status to "cleanup"', async () => {
-        expect.assertions(1);
-        await facadeInit();
+      it('should destroy the allocation driver immediately', async () => {
+        await expect(allocateSomeDevice()).rejects.toThrow(/init failed/);
+        expect(deviceAllocator.cleanup).toHaveBeenCalled();
+      });
 
-        ipcServer().dispose.mockImplementation(async () => {
-          expect(facade.getStatus()).toBe('cleanup');
+      describe('and the driver fails to clean up', () => {
+        beforeEach(() => {
+          deviceAllocator.cleanup.mockRejectedValue(new Error('cleanup failed'));
         });
 
-        await facade.cleanup();
+        it('should log the error', async () => {
+          await expect(allocateSomeDevice()).rejects.toThrow(/init failed/);
+          expect(log().error).toHaveBeenCalledWith({ cat: 'device', err: new Error('cleanup failed') }, `Failed to cleanup the device allocation driver for some.device after a failed initialization`);
+        });
       });
+    });
 
-      it('should restore status to "inactive"', async () => {
-        await facadeInit();
-        await facade.cleanup();
-        expect(facade.getStatus()).toBe('inactive');
+    describe('when a faulty device is being allocated', () => {
+      beforeEach(async () => {
+        deviceAllocator.postAllocate.mockRejectedValue(new Error('postAllocate failed'));
       });
 
-      describe('given a worker clean-up error', () => {
-        const facadeInitWithWorker = async () => facade.init({ workerId: WORKER_ID });
-        const facadeCleanup = async () => expect(() => facade.cleanup()).rejects.toThrow();
+      it('should free the device after an error', async () => {
+        await expect(allocateSomeDevice()).rejects.toThrow(/postAllocate failed/);
+        expect(deviceAllocator.free).toHaveBeenCalled();
+      });
 
-        beforeEach(async () => {
-          detoxConfigDriver.givenDetoxServerAutostart();
-          await facadeInitWithWorker();
+      describe('and cannot be freed properly', () => {
+        let error = new Error('free failed');
 
-          detoxWorker().cleanup.mockRejectedValue(new Error(''));
+        beforeEach(async () => {
+          deviceAllocator.free.mockRejectedValue(error);
         });
 
-        it('should clean-up nonetheless', async () => {
-          await facadeCleanup();
-          expect(detoxServer().close).toHaveBeenCalled();
-          expect(ipcServer().dispose).toHaveBeenCalled();
+        it('should throw the original allocation error', async () => {
+          await expect(allocateSomeDevice()).rejects.toThrow(/postAllocate failed/);
+          expect(log().error).toHaveBeenCalledWith({ cat: 'device', err: error }, `Failed to free a-device-id after a failed allocation attempt`);
         });
+      });
+    });
 
-        it('should restore status to "inactive"', async () => {
-          await facadeCleanup();
-          expect(facade.getStatus()).toBe('inactive');
+    describe('and cleaning up', () => {
+      it('should change intermediate status to "cleanup"', async () => {
+        expect.assertions(1);
+        ipcServer().dispose.mockImplementation(async () => {
+          expect(facade.getStatus()).toBe('cleanup');
         });
+        await facade.cleanup();
       });
     });
 
-    describe('given an exit signal', () => {
-      beforeEach(async () => {
-        detoxConfigDriver.givenDetoxServerAutostart();
+    describe('and cleaned up', () => {
+      beforeEach(async () => facade.cleanup());
 
-        await facadeInit();
+      it('should close the ipc server', async () => {
+        expect(ipcServer().dispose).toHaveBeenCalled();
+      });
 
-        const signalHandler = getSignalHandler();
-        signalHandler(123, 'SIGSMT');
+      it('should delete the context-shared file', async () => {
+        expect(fs.remove).toHaveBeenCalledWith(expect.stringMatching(TEMP_FILE_REGEXP));
+      });
+
+      it('should finalize the logger', async () => {
+        expect(logFinalizer().finalize).toHaveBeenCalled();
       });
 
-      it('should *emergency* cleanup the global lifecycle handler', () =>
-        expect(deviceAllocator.emergencyCleanup).toHaveBeenCalled());
+      it('should restore status to "inactive"', async () => {
+        expect(facade.getStatus()).toBe('inactive');
+      });
+    });
 
-      it('should close the detox server', async () =>
-        expect(detoxServer().close).toHaveBeenCalled());
+    describe('given an exit signal', () => {
+      beforeEach(async () => {
+        const signalHandler = getSignalHandler();
+        signalHandler(123, 'SIGSMT');
+      });
 
       it('should close the ipc server', async () =>
         expect(ipcServer().dispose).toHaveBeenCalled());
@@ -325,19 +319,47 @@ describe('DetoxPrimaryContext', () => {
       it('should finalize the logger', async () =>
         expect(logFinalizer().finalizeSync).toHaveBeenCalled());
     });
+  });
 
-    describe('given a broken exit signal', () => {
-      let signalHandler;
-      beforeEach(async () => {
-        detoxConfigDriver.givenDetoxServerAutostart();
-        await facadeInit();
+  describe('when initialized with no options', () => {
+    beforeEach(async () => facade.init());
+
+    it('should also install a worker', async () => {
+      expect(detoxWorker().init).toHaveBeenCalled();
+      expect(facade.session).toEqual(expect.objectContaining({ workerId: 'worker' }));
+    });
+  });
+
+  describe('when initialized with auto-start of Detox server', () => {
+    beforeEach(() => detoxConfigDriver.givenDetoxServerAutostart());
+    beforeEach(facadeInit);
+
+    it('should create the Detox server', async () => {
+      expect(DetoxServer).toHaveBeenCalledWith({
+        port: 0,
+        standalone: false,
+      });
+    });
+
+    it('should start the server', async () => {
+      expect(detoxServer().open).toHaveBeenCalled();
+    });
+
+    describe('and cleaned up', () => {
+      beforeEach(async () => facade.cleanup());
 
-        signalHandler = getSignalHandler();
+      it('should close the detox server', async () => {
+        expect(detoxServer().close).toHaveBeenCalled();
       });
+    });
 
-      it('should do nothing', () => {
+    describe('given a non-conforming exit signal', () => {
+      beforeEach(async () => {
+        const signalHandler = getSignalHandler();
         signalHandler(123, undefined);
+      });
 
+      it('should do nothing', () => {
         expect(deviceAllocator.emergencyCleanup).not.toHaveBeenCalled();
         expect(detoxServer().close).not.toHaveBeenCalled();
         expect(ipcServer().dispose).not.toHaveBeenCalled();
@@ -345,6 +367,76 @@ describe('DetoxPrimaryContext', () => {
     });
   });
 
+  describe('when initialized with Detox server on a certain port', () => {
+    const port = '666';
+
+    beforeEach(() => detoxConfigDriver.givenDetoxServerAutostart(port));
+    beforeEach(() => detoxConfigDriver.givenDetoxServerPort(port));
+    beforeEach(facadeInit);
+
+    it('should create it', async () => {
+      expect(DetoxServer).toHaveBeenCalledWith({
+        port,
+        standalone: false,
+      });
+    });
+  });
+
+  describe('when initialized without auto-start of Detox server', () => {
+    beforeEach(facadeInit);
+
+    it('should not create a server', async () => {
+      expect(DetoxServer).not.toHaveBeenCalled();
+    });
+  });
+
+  describe('when initialized not successfully', () => {
+    it('should report status as "init"', async () => {
+      IPCServer.prototype.init = jest.fn().mockRejectedValue(new Error('init failed'));
+
+      await expect(() => facadeInit()).rejects.toThrow();
+      expect(facade.getStatus()).toBe('init');
+    });
+  });
+
+  describe('when initialized with a worker', () => {
+    beforeEach(() => detoxConfigDriver.givenDetoxServerAutostart());
+    beforeEach(facadeInitWithWorker);
+
+    it('should install a worker if worker ID has been specified', async () => {
+      expect(facade.session).toEqual(expect.objectContaining({ workerId: WORKER_ID }));
+      expect(detoxWorker().init).toHaveBeenCalled();
+    });
+
+    it('should register the worker at the IPC server\'s', async () => {
+      expect(ipcServer().onRegisterWorker).toHaveBeenCalledWith({ workerId: WORKER_ID });
+    });
+
+    describe('and cleaned up', () => {
+      beforeEach(async () => facade.cleanup());
+
+      it('should uninstall an assigned worker', async () => {
+        expect(detoxWorker().cleanup).toHaveBeenCalled();
+      });
+    });
+
+    describe('and cleaned up with an error', () => {
+      beforeEach(async () => {
+        detoxWorker().cleanup.mockRejectedValue(new Error(''));
+        await expect(() => facade.cleanup()).rejects.toThrow();
+      });
+
+      it('should clean-up nonetheless', async () => {
+        expect(detoxServer().close).toHaveBeenCalled();
+        expect(ipcServer().dispose).toHaveBeenCalled();
+      });
+
+      it('should restore status to "inactive"', async () => {
+        expect(facade.getStatus()).toBe('inactive');
+      });
+    });
+  });
+
   function _initDetoxConfig() {
     detoxConfigDriver = new DetoxConfigDriver(_.cloneDeep(DETOX_CONFIG_BASE));
 
@@ -369,8 +461,8 @@ describe('DetoxPrimaryContext', () => {
 
     deviceAllocator = {
       init: jest.fn(),
-      allocate: jest.fn(),
-      postAllocate: jest.fn(),
+      allocate: jest.fn().mockResolvedValue({ id: 'a-device-id' }),
+      postAllocate: jest.fn().mockResolvedValue({ id: 'a-device-id' }),
       free: jest.fn(),
       cleanup: jest.fn(),
       emergencyCleanup: jest.fn(),
@@ -391,6 +483,14 @@ describe('DetoxPrimaryContext', () => {
     DetoxWorker = jest.requireMock('../DetoxWorker');
   }
 
+  async function allocateSomeDevice() {
+    return context[symbols.allocateDevice]({ type: 'some.device' });
+  }
+
+  async function deallocateDevice(cookie) {
+    return context[symbols.deallocateDevice](cookie);
+  }
+
   class DetoxConfigDriver {
     constructor(detoxConfig) {
       this.detoxConfig = detoxConfig;
diff --git a/detox/src/realms/__snapshots__/DetoxPrimaryContext.test.js.snap b/detox/src/realms/__snapshots__/DetoxPrimaryContext.test.js.snap
index d15431ff0..768841cdd 100644
--- a/detox/src/realms/__snapshots__/DetoxPrimaryContext.test.js.snap
+++ b/detox/src/realms/__snapshots__/DetoxPrimaryContext.test.js.snap
@@ -7,6 +7,14 @@ HINT: If you are using Detox with Jest according to the latest guide, please rep
 https://github.com/wix/Detox/issues"
 `;
 
+exports[`DetoxPrimaryContext when initialized when a device is being allocated should throw on attempt to deallocate a cookie that does not belong to this context 1`] = `
+"Cannot deallocate device some-other-device because it was not allocated by this context.
+
+HINT: See the actually known allocated devices below:
+
+- a-device-id"
+`;
+
 exports[`DetoxPrimaryContext when not initialized should throw on attempt to get a worker 1`] = `
 "Detox worker instance has not been installed in this context (DetoxPrimaryContext).