Repository: wix/Detox
Pull request #4311 (2023-12-24 19:28:59)

Fix refs and docs involving testBinaryPath
## Description

<!--
Thank you for contributing!

Step 1: Before submitting a pull request that introduces a new functionality or fixes a bug,
please open an issue where we could discuss the suggestion, problem - and potential ways to fix.
-->

- This pull request stems from this discussion: https://github.com/wix/Detox/discussions/4307

<!--
Step 2: Provide an overview of how your fix / enhancement works.
If possible, provide screenshots of the before and after states (even for simple command line options - show the terminal).
-->

In this pull request, I have fixed inline code-refs to the Android setup guide, in places where the usage of `testBinaryPath` was not clear.

<!--
Step 3: Please review the checklist below.
-->

---


> _For features/enhancements:_
 - [ ] I have added/updated the relevant references in the [documentation](https://github.com/wix/Detox/tree/master/docs) files.

> _For API changes:_
 - [ ] I have made the necessary changes in the [types index](https://github.com/wix/Detox/blob/master/detox/index.d.ts) file.


d4vidi: @noomorph could use your review here
noomorph: Okay, tomorrow

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/package.json b/detox/package.json
index 7f3c4f216..7d0612d52 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -51,7 +51,7 @@
     "eslint-plugin-node": "^11.1.0",
     "eslint-plugin-unicorn": "^47.0.0",
     "jest": "^28.1.3",
-    "jest-allure2-reporter": "2.0.0-alpha.11",
+    "jest-allure2-reporter": "^2.0.0-beta.2",
     "mockdate": "^2.0.1",
     "prettier": "^2.4.1",
     "react-native": "0.71.10",
@@ -72,6 +72,7 @@
     "funpermaproxy": "^1.1.0",
     "glob": "^8.0.3",
     "ini": "^1.3.4",
+    "jest-environment-emit": "^1.0.3",
     "json-cycle": "^1.3.0",
     "lodash": "^4.17.11",
     "multi-sort-stream": "^1.0.3",
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/devices/common/drivers/android/tools/ApkValidator.js b/detox/src/devices/common/drivers/android/tools/ApkValidator.js
index b6fcb8a3a..3fe5c659b 100644
--- a/detox/src/devices/common/drivers/android/tools/ApkValidator.js
+++ b/detox/src/devices/common/drivers/android/tools/ApkValidator.js
@@ -1,6 +1,6 @@
 const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError');
 
-const setupGuideHint = 'For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md';
+const setupGuideHint = 'For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup';
 
 class ApkValidator {
   constructor(aapt) {
diff --git a/detox/src/devices/runtime/drivers/android/AndroidDriver.js b/detox/src/devices/runtime/drivers/android/AndroidDriver.js
index 8ff4d523c..7f0f2cbc3 100644
--- a/detox/src/devices/runtime/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/runtime/drivers/android/AndroidDriver.js
@@ -280,7 +280,7 @@ class AndroidDriver extends DeviceDriverBase {
       throw new DetoxRuntimeError({
         message: `The test APK could not be found at path: '${testApkPath}'`,
         hint: 'Try running the detox build command, and make sure it was configured to execute a build command (e.g. \'./gradlew assembleAndroidTest\')' +
-          '\nFor further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md',
+          '\nFor further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup',
       });
     }
     return testApkPath;
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.');
diff --git a/docs/introduction/partials/_project-setup-apps-android.mdx b/docs/introduction/partials/_project-setup-apps-android.mdx
index e53091f22..547d041d5 100644
--- a/docs/introduction/partials/_project-setup-apps-android.mdx
+++ b/docs/introduction/partials/_project-setup-apps-android.mdx
@@ -2,7 +2,7 @@
 
 import FlavorizedCodeBlock from '@site/src/components/FlavorizedCodeBlock';
 
-Check **binaryPath** and **build** configs for `android.debug` and `android.release` mode in your Detox config:
+Check the **build** and **binaryPath** attributes for the `android.debug` and `android.release` Detox configs:
 
 ```js title=".detoxrc.js"
 module.exports = {
@@ -10,15 +10,55 @@ module.exports = {
     'android.debug': {
       type: 'android.apk',
       // highlight-start
-      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
-      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug'
+      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
+      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk'
       // highlight-end
     },
     'android.release': {
       type: 'android.apk',
       // highlight-start
+      build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
+      binaryPath: 'android/app/build/outputs/apk/release/app-release.apk'
+      // highlight-end
+    },
+    // ...
+  },
+  // ...
+};
+```
+
+If you have a typical React Native project, these values should already be sufficient and correct.
+
+#### testBinaryPath
+
+In Android automation testing, there are in fact 2 app binaries involved:
+1. The _app_ APK, containing your app's code.
+2. The _test_ APK, containing _test_ code. That includes Detox's native code, Espresso and more.
+
+In some projects, it might make sense for the test APK to be generated over a separate flow, through which is may end up
+being put it some custom,
+[non-default path](https://stackoverflow.com/questions/43670463/where-is-the-test-apk-located-in-android-project).
+One such example is an optimization where the test APK is prebuilt once and used across multiple app variations. This is
+a place where the `testBinaryPath` attribute can come to the rescue; It can be applied in order to set the custom path
+to the test APK explicitly:
+
+```js title=".detoxrc.js"
+module.exports = {
+  apps: {
+    'android.debug': {
+      type: 'android.apk',
+      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
+      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
+      // highlight-start
+      testBinaryPath: 'custom/path/to/app-debug-androidTest.apk'
+      // highlight-end
+    },
+    'android.release': {
+      type: 'android.apk',
       binaryPath: 'android/app/build/outputs/apk/release/app-release.apk',
-      build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release'
+      build: 'cd android && ./gradlew assembleRelease assembleAndroidTest -DtestBuildType=release',
+      // highlight-start
+      testBinaryPath: 'custom/path/to/app-release-androidTest.apk'
       // highlight-end
     },
     // ...
@@ -27,23 +67,26 @@ module.exports = {
 };
 ```
 
-If you have a typical React Native project, these values should already be correct.
+> Mind that in the common case, the `testBinaryPath` attributes is not explicitly required, simply because Detox knows
+how to locate it in one of the default paths automatically.
+
+#### Product flavors
 
-On the other hand, if your app has extra [`productFlavors`](https://developer.android.com/studio/build/build-variants#product-flavors)
-(let's imagine you have `driver` and `passenger` flavors of a taxi application), then you should rewrite your apps config
-for both **debug** and **release** configurations, e.g.:
+On even more advanced use cases, apps may have additional, custom [`productFlavors`](https://developer.android.com/studio/build/build-variants#product-flavors)
+(consider the case of a `driver` and `passenger` flavors of a taxi application). In this case, you should rewrite your apps config,
+for both **debug** and **release** configurations, according to those flavors, e.g.:
 
 <FlavorizedCodeBlock
   language="diff"
   header={'   apps: {\n'}
   flavors={['Driver', 'Passenger']}
 >
-  {(conf) => `\
-     '${conf.toLowerCase()}.android.debug': {
+  {(flavor) => `\
+     '${flavor.toLowerCase()}.android.debug': {
        type: 'android.apk',
 -      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
-+      binaryPath: 'android/app/build/outputs/apk/${conf.toLowerCase()}/debug/app-${conf.toLowerCase()}-debug.apk',
++      binaryPath: 'android/app/build/outputs/apk/${flavor.toLowerCase()}/debug/app-${flavor.toLowerCase()}-debug.apk',
 -      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
-+      build: 'cd android && ./gradlew assemble${conf}Debug assemble${conf}DebugAndroidTest -DtestBuildType=debug',
++      build: 'cd android && ./gradlew assemble${flavor}Debug assemble${flavor}DebugAndroidTest -DtestBuildType=debug',
      },`}
 </FlavorizedCodeBlock>
diff --git a/generation/package.json b/generation/package.json
index a082c848a..e788779bd 100644
--- a/generation/package.json
+++ b/generation/package.json
@@ -1,6 +1,6 @@
 {
   "name": "generation",
-  "version": "20.14.0",
+  "version": "20.14.2-smoke.0",
   "description": "Generate wrapper code for native dependencies",
   "main": "index.js",
   "private": true,
@@ -30,7 +30,7 @@
   },
   "devDependencies": {
     "jest": "^27.0.0",
-    "jest-allure2-reporter": "2.0.0-alpha.11",
+    "jest-allure2-reporter": "^2.0.0-beta.2",
     "lint-staged": "^6.0.0",
     "prettier": "^1.8.2"
   },


Test patch:
diff --git a/detox/runners/jest/testEnvironment/index.js b/detox/runners/jest/testEnvironment/index.js
index 9418a65ed..159b1b124 100644
--- a/detox/runners/jest/testEnvironment/index.js
+++ b/detox/runners/jest/testEnvironment/index.js
@@ -1,5 +1,6 @@
 const path = require('path');
 
+const WithEmitter = require('jest-environment-emit');
 const resolveFrom = require('resolve-from');
 const maybeNodeEnvironment = require(resolveFrom(process.cwd(), 'jest-environment-node'));
 /** @type {typeof import('@jest/environment').JestEnvironment} */
@@ -31,7 +32,7 @@ const log = detox.log.child({ cat: 'lifecycle,jest-environment' });
 /**
  * @see https://www.npmjs.com/package/jest-circus#overview
  */
-class DetoxCircusEnvironment extends NodeEnvironment {
+class DetoxCircusEnvironment extends WithEmitter(NodeEnvironment) {
   constructor(config, context) {
     super(assertJestCircus27(config), assertExistingContext(context));
 
@@ -62,6 +63,8 @@ class DetoxCircusEnvironment extends NodeEnvironment {
       SpecReporter,
       WorkerAssignReporter,
     });
+
+    this.testEvents.on('*', this._onTestEvent.bind(this));
   }
 
   /** @override */
@@ -72,19 +75,13 @@ class DetoxCircusEnvironment extends NodeEnvironment {
 
   // @ts-expect-error TS2425
   async handleTestEvent(event, state) {
+    await super.handleTestEvent(event, state);
+
     if (detox.session.unsafe_earlyTeardown) {
       if (event.name === 'test_fn_start' || event.name === 'hook_start') {
         throw new Error('Detox halted test execution due to an early teardown request');
       }
     }
-
-    this._timer.schedule(state.testTimeout != null ? state.testTimeout : this.setupTimeout);
-
-    if (SYNC_CIRCUS_EVENTS.has(event.name)) {
-      this._handleTestEventSync(event, state);
-    } else {
-      await this._handleTestEventAsync(event, state);
-    }
   }
 
   /** @override */
@@ -147,6 +144,23 @@ class DetoxCircusEnvironment extends NodeEnvironment {
     }
   }
 
+  /** @private */
+  _onTestEvent({ type, event, state }) {
+    const timeout = state && state.testTimeout != null ? state.testTimeout : this.setupTimeout;
+
+    this._timer.schedule(timeout);
+
+    if (event) {
+      if (SYNC_CIRCUS_EVENTS.has(event.name)) {
+        this._handleTestEventSync(event, state);
+      } else {
+        return this._handleTestEventAsync(event, state);
+      }
+    } else {
+      return this._handleTestEventAsync({ name: type }, null);
+    }
+  }
+
   /** @private */
   async _handleTestEventAsync(event, state = null) {
     const description = `handling ${state ? 'jest-circus' : 'jest-environment'} "${event.name}" event`;
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/devices/common/drivers/android/tools/ApkValidator.test.js b/detox/src/devices/common/drivers/android/tools/ApkValidator.test.js
index b93deece4..4f4c92d62 100644
--- a/detox/src/devices/common/drivers/android/tools/ApkValidator.test.js
+++ b/detox/src/devices/common/drivers/android/tools/ApkValidator.test.js
@@ -1,3 +1,4 @@
+
 describe('APK validation', () => {
 
   const binaryPath = 'mock-bin-path';
diff --git a/detox/src/devices/common/drivers/android/tools/__snapshots__/ApkValidator.test.js.snap b/detox/src/devices/common/drivers/android/tools/__snapshots__/ApkValidator.test.js.snap
index 215dde3df..35b07b32d 100644
--- a/detox/src/devices/common/drivers/android/tools/__snapshots__/ApkValidator.test.js.snap
+++ b/detox/src/devices/common/drivers/android/tools/__snapshots__/ApkValidator.test.js.snap
@@ -4,7 +4,7 @@ exports[`APK validation App APK validation should throw a descriptive error if a
 "App APK at path mock-bin-path was detected as the *test* APK!
 
 HINT: Your binary path was probably wrongly set in the active Detox configuration.
-For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md"
+For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup"
 `;
 
 exports[`APK validation App APK validation should throw a specific error if AAPT throws 1`] = `
@@ -12,14 +12,14 @@ exports[`APK validation App APK validation should throw a specific error if AAPT
 Error: mock error
 
 HINT: Check that the binary path in the active Detox configuration has been set to a path of an APK file.
-For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md"
+For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup"
 `;
 
 exports[`APK validation Test APK validation should throw a descriptive error if APK happens to be an app APK 1`] = `
 "Test APK at path mock-test-bin-path was detected as the *app* APK!
 
 HINT: Your test test-binary path was probably wrongly set in the active Detox configuration.
-For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md"
+For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup"
 `;
 
 exports[`APK validation Test APK validation should throw a specific error if AAPT throws 1`] = `
@@ -27,5 +27,5 @@ exports[`APK validation Test APK validation should throw a specific error if AAP
 Error: mock error
 
 HINT: Check that the test-binary path in the active Detox configuration has been set to a path of an APK file.
-For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md"
+For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup"
 `;
diff --git a/detox/src/devices/runtime/drivers/android/__snapshots__/AndroidDriver.test.js.snap b/detox/src/devices/runtime/drivers/android/__snapshots__/AndroidDriver.test.js.snap
index 566c9cd80..57e0ef834 100644
--- a/detox/src/devices/runtime/drivers/android/__snapshots__/AndroidDriver.test.js.snap
+++ b/detox/src/devices/runtime/drivers/android/__snapshots__/AndroidDriver.test.js.snap
@@ -4,5 +4,5 @@ exports[`Android driver App installation should throw if auto test-binary path r
 "The test APK could not be found at path: 'testApkPathOf(absolutePathOf(mock-bin-path))'
 
 HINT: Try running the detox build command, and make sure it was configured to execute a build command (e.g. './gradlew assembleAndroidTest')
-For further assistance, visit the Android setup guide: https://github.com/wix/Detox/blob/master/docs/Introduction.Android.md"
+For further assistance, visit the project setup guide (select the Android tabs): https://wix.github.io/Detox/docs/introduction/project-setup"
 `;
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).
 
diff --git a/detox/test/e2e/jest.config.js b/detox/test/e2e/jest.config.js
index 4830228ff..da69b592b 100644
--- a/detox/test/e2e/jest.config.js
+++ b/detox/test/e2e/jest.config.js
@@ -15,6 +15,9 @@ module.exports = async () => {
   /** @type {import('jest-allure2-reporter').ReporterOptions} */
   const jestAllure2ReporterOptions = {
     overwrite: !process.env.CI,
+    attachments: {
+      fileHandler: 'copy',
+    },
     testCase: {
       labels: {
         package: ({ filePath }) => filePath.slice(1).join('/'),
@@ -44,6 +47,13 @@ module.exports = async () => {
   return {
     'rootDir': path.join(__dirname, '../..'),
     'testEnvironment': './test/e2e/testEnvironment.js',
+    'testEnvironmentOptions': {
+      'eventListeners': [
+        'jest-metadata/environment-listener',
+        'jest-allure2-reporter/environment-listener',
+        require.resolve('detox-allure2-adapter'),
+      ]
+    },
     'testRunner': './test/node_modules/jest-circus/runner',
     'testMatch': [
       '<rootDir>/test/e2e/**/*.test.{js,ts}',
diff --git a/detox/test/package.json b/detox/test/package.json
index 82c26ae4e..e261504fb 100644
--- a/detox/test/package.json
+++ b/detox/test/package.json
@@ -57,14 +57,16 @@
     "@typescript-eslint/parser": "^5.4.0",
     "cross-env": "^7.0.3",
     "detox": "^20.14.5",
+    "detox-allure2-adapter": "^1.0.0-alpha.2",
     "eslint": "^8.41.0",
     "eslint-plugin-unicorn": "^47.0.0",
     "execa": "^5.1.1",
     "express": "^4.15.3",
     "glob": "^7.2.0",
     "jest": "^29.2.1",
-    "jest-allure2-reporter": "2.0.0-alpha.11",
+    "jest-allure2-reporter": "^2.0.0-beta.2",
     "jest-junit": "^10.0.0",
+    "jest-metadata": "^1.3.1",
     "lodash": "^4.14.1",
     "metro-react-native-babel-preset": "0.73.9",
     "nyc": "^15.1.0",