Repository: wix/Detox
Pull request #2778 (2021-05-12 20:48:06)

Make DeviceRegistry DB-like: hold ids+data instead of objects
## 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 is associated with the issue described here: #2655.

<!--
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 reworked the device-registry implementation such that instead of allowing for the holding of objects (rather than plain strings as ID's), it now allows for storing ID's (strings) as DB-like keys, alongside associated data (arbitrary types).

The reason for ever allowing for objects storing was to support Genycloud, and it was a mistake. The main reason was that on top of holding an instance' UUID, we want to also have its name as meta-data, for proper logging during global tear-down.

Allowing for objects in device-registry is, by principle, wrong - because it makes the comparisons (i.e. in look-up queries) wrong in the sense that both the actual ID *AND* the meta data are forced to be identical in order to create a match. Effectively, only the ID should matter...

To put things in a more illustrative way, I've basically changed the registry's content format (in the case of genycloud) from this:

```
[
  {"uuid":"6151855a-4405-4690-8aa8-0e5524f3a4f9","name":"Detox-1620851733498.1"},
  {"uuid":"dd6b598f-a552-4393-bd96-e9923c01cccc","name":"Detox-1620851733498.2"}
]
```

to this:

```
[{
  "id":"6151855a-4405-4690-8aa8-0e5524f3a4f9",
  "data":{"name":"Detox-1620851733498.1"}
},
{
  "id":"dd6b598f-a552-4393-bd96-e9923c01cccc",
  "data":{"name":"Detox-1620851733498.2"}
}]
```
Ultimately, the resulted DeviceRegistry implementation is far simpler 👍🏻 

> ⚠️  Note: Storing meta-data in device-registry is nonetheless still somewhat of an abuse. A cleaner way would be to completely detach the locking mechanism from the actual data, and rather apply usage of an actual DB - as the likes of [Simple-JSONdb](https://github.com/nmaggioni/Simple-JSONdb) or [lowdb](https://github.com/typicode/lowdb). However, this feels like an overkill-refactor at the moment.

<!--
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 thanks for sincere feedback, I truly appreciate it! 🙏🏻 
Note that the point here was not to iron everything through. For that, we'll have to properly introduce #2193 (and we will - in due time) - because then all global data could be kept in-memory under the global context...
The goal here was to eliminate a data management practice that was downright wrong.

Patch:
diff --git a/detox/src/devices/DeviceRegistry.js b/detox/src/devices/DeviceRegistry.js
index 02f6783d7..cd5a253b8 100644
--- a/detox/src/devices/DeviceRegistry.js
+++ b/detox/src/devices/DeviceRegistry.js
@@ -4,25 +4,35 @@ const fs = require('fs-extra');
 const ExclusiveLockfile = require('../utils/ExclusiveLockfile');
 const safeAsync = require('../utils/safeAsync');
 
-const getDeviceEqualsFn = (deviceHandle) => (otherDeviceHandle) => _.isEqual(otherDeviceHandle, deviceHandle);
-const getDeviceDifferFn = (deviceHandle) => (otherDeviceHandle) => !_.isEqual(otherDeviceHandle, deviceHandle);
 const readOptions = {
   encoding: 'utf8',
 };
 
-class DeviceHandlesList {
+const FIELD_NAME_ID = 'id';
+const FIELD_NAME_DATA = 'data';
+
+class DevicesList {
   constructor(devices) {
-    this.rawDevices = Object.freeze(devices);
+    this._devices = Object.freeze(devices);
+  }
+
+  /**
+   * @returns {{id: string, data: *?}[]}
+   */
+  get rawDevices() {
+    return this._devices;
   }
 
-  includes(deviceHandle) {
-    return DeviceHandlesList._includes(deviceHandle, this.rawDevices);
+  /**
+   * @param {string} deviceId
+   * @returns {boolean}
+   */
+  includes(deviceId) {
+    return DevicesList._includes(deviceId, this._devices);
   }
 
-  static _includes(deviceHandle, devices) {
-    const rawDeviceHandle = getRawDeviceHandle(deviceHandle);
-    const deviceEqualsFn = getDeviceEqualsFn(rawDeviceHandle);
-    return !!_.find(devices, deviceEqualsFn);
+  static _includes(deviceId, devices) {
+    return _.some(devices, [FIELD_NAME_ID, deviceId]);
   }
 }
 
@@ -52,65 +62,66 @@ class DeviceRegistry {
   }
 
   /***
-   * @param {string|Object|Function} getDeviceHandle
+   * @param {string|Function} getDeviceId
+   * @param {*?} data
    * @returns {Promise<string>}
    */
-  async allocateDevice(getDeviceHandle) {
+  async allocateDevice(getDeviceId, data) {
     return this._lockfile.exclusively(async () => {
-      const deviceHandle = await safeAsync(getDeviceHandle);
-      this._toggleDeviceStatus(deviceHandle, true);
-      return deviceHandle;
+      const deviceId = await safeAsync(getDeviceId);
+      this._registerDevice(deviceId, data);
+      return deviceId;
     });
   }
 
   /***
-   * @param {string|Object|Function} getDeviceHandle
-   * @returns {void}
+   * @param {string|Function} getDeviceId
+   * @returns {Promise<void>}
    */
-  async disposeDevice(getDeviceHandle) {
+  async disposeDevice(getDeviceId) {
     await this._lockfile.exclusively(async () => {
-      const deviceId = await safeAsync(getDeviceHandle);
+      const deviceId = await safeAsync(getDeviceId);
       if (deviceId) {
-        this._toggleDeviceStatus(deviceId, false);
+        this._unregisterDevice(deviceId);
       }
     });
   }
 
   /***
-   * @param {Object} deviceHandle
+   * @param {string} deviceId
    * @returns {boolean}
    */
-  includes(deviceHandle) {
+  includes(deviceId) {
     const devices = this._lockfile.read();
-    return DeviceHandlesList._includes(deviceHandle, devices);
+    return DevicesList._includes(deviceId, devices);
   }
 
   /***
-   * @returns {DeviceHandlesList}
+   * @returns {DevicesList}
+   */
+  getRegisteredDevices() {
+    const devices = this._lockfile.read();
+    return new DevicesList(devices);
+  }
+
+  /***
+   * @returns {DevicesList}
    */
   async readRegisteredDevices() {
     let result = null;
     await this._lockfile.exclusively(() => {
-      result = new DeviceHandlesList(this._lockfile.read());
+      result = this.getRegisteredDevices();
     })
     return result;
   }
 
   /***
-   * @returns {DeviceHandlesList}
-   */
-  getRegisteredDevices() {
-    const deviceHandles = this._lockfile.read();
-    return new DeviceHandlesList(deviceHandles);
-  }
-
-  /***
-   * @returns {DeviceHandlesList}
+   * @returns {DevicesList}
    */
   readRegisteredDevicesUNSAFE() {
     const contents = fs.readFileSync(this._lockfilePath, readOptions);
     const devices = JSON.parse(contents);
-    return new DeviceHandlesList(devices);
+    return new DevicesList(devices);
   }
 
   /***
@@ -120,16 +131,30 @@ class DeviceRegistry {
     return [];
   }
 
-  /***
+  /**
+   * @private
+   */
+  _registerDevice(deviceId, data) {
+    const state = this._lockfile.read();
+    let newState = _.reject(state, [FIELD_NAME_ID, deviceId]);
+
+    const device = {};
+    device[FIELD_NAME_ID] = deviceId;
+
+    if (data) {
+      device[FIELD_NAME_DATA] = data;
+    }
+
+    newState = _.concat(newState, device);
+    this._lockfile.write(newState);
+  }
+
+  /**
    * @private
    */
-  _toggleDeviceStatus(deviceHandle, busy) {
-    const rawDeviceHandle = getRawDeviceHandle(deviceHandle);
-    const deviceDifferFn = getDeviceDifferFn(rawDeviceHandle);
+  _unregisterDevice(deviceId) {
     const state = this._lockfile.read();
-    const newState = busy
-      ? _.concat(state, rawDeviceHandle)
-      : _.filter(state, deviceDifferFn);
+    const newState = _.reject(state, [FIELD_NAME_ID, deviceId]);
     this._lockfile.write(newState);
   }
 
@@ -146,8 +171,4 @@ class DeviceRegistry {
   }
 }
 
-function getRawDeviceHandle(deviceHandle) {
-  return JSON.parse(JSON.stringify(deviceHandle));
-}
-
 module.exports = DeviceRegistry;
diff --git a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js
index f2f4554ae..e9298add3 100644
--- a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js
+++ b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js
@@ -116,7 +116,8 @@ class GenyCloudDriver extends AndroidDriver {
     onSignalExit((code, signal) => {
       if (signal) {
         const deviceCleanupRegistry = GenyDeviceRegistryFactory.forGlobalShutdown();
-        const { rawDevices: instanceHandles } = deviceCleanupRegistry.readRegisteredDevicesUNSAFE();
+        const { rawDevices  } = deviceCleanupRegistry.readRegisteredDevicesUNSAFE();
+        const instanceHandles = rawDevicesToInstanceHandles(rawDevices);
         if (instanceHandles.length) {
           reportGlobalCleanupSummary(instanceHandles);
         }
@@ -126,7 +127,8 @@ class GenyCloudDriver extends AndroidDriver {
 
   static async globalCleanup() {
     const deviceCleanupRegistry = GenyDeviceRegistryFactory.forGlobalShutdown();
-    const { rawDevices: instanceHandles } = await deviceCleanupRegistry.readRegisteredDevices();
+    const { rawDevices } = await deviceCleanupRegistry.readRegisteredDevices();
+    const instanceHandles = rawDevicesToInstanceHandles(rawDevices);
     if (instanceHandles.length) {
       const exec = new GenyCloudExec(environment.getGmsaasPath());
       const instanceLifecycleService = new InstanceLifecycleService(exec, null);
@@ -165,4 +167,10 @@ function reportGlobalCleanupSummary(deletionLeaks) {
   }
 }
 
+function rawDevicesToInstanceHandles(rawDevices) {
+  return rawDevices.map((rawDevice) => ({
+    uuid: rawDevice.id,
+    name: rawDevice.data.name,
+  }));
+}
 module.exports = GenyCloudDriver;
diff --git a/detox/src/devices/drivers/android/genycloud/GenyCloudInstanceHandle.js b/detox/src/devices/drivers/android/genycloud/GenyCloudInstanceHandle.js
deleted file mode 100644
index 3045ef023..000000000
--- a/detox/src/devices/drivers/android/genycloud/GenyCloudInstanceHandle.js
+++ /dev/null
@@ -1,8 +0,0 @@
-class GenyCloudInstanceHandle {
-  constructor(instance) {
-    this.uuid = instance.uuid;
-    this.name = instance.name;
-  }
-}
-
-module.exports = GenyCloudInstanceHandle;
diff --git a/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceLauncher.js b/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceLauncher.js
index 1557bd854..1cd8ca4c6 100644
--- a/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceLauncher.js
+++ b/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceLauncher.js
@@ -1,5 +1,4 @@
 const AndroidDeviceLauncher = require('../../AndroidDeviceLauncher');
-const GenyCloudInstanceHandle = require('../GenyCloudInstanceHandle');
 
 class GenyCloudInstanceLauncher extends AndroidDeviceLauncher {
   constructor(instanceLifecycleService, deviceCleanupRegistry, eventEmitter) {
@@ -19,17 +18,16 @@ class GenyCloudInstanceLauncher extends AndroidDeviceLauncher {
    * @returns {Promise<void>}
    */
   async launch(instance) {
-    const instanceHandle = new GenyCloudInstanceHandle(instance);
-    await this._deviceCleanupRegistry.allocateDevice(instanceHandle);
+    await this._deviceCleanupRegistry.allocateDevice(instance.uuid, { name: instance.name });
   }
 
   async shutdown(instance) {
-    const instanceHandle = new GenyCloudInstanceHandle(instance);
+    const { uuid } = instance;
 
-    await this._notifyPreShutdown(instance.adbName);
-    await this._instanceLifecycleService.deleteInstance(instance.uuid);
-    await this._deviceCleanupRegistry.disposeDevice(instanceHandle);
-    await this._notifyShutdownCompleted(instance.adbName);
+    await this._notifyPreShutdown(uuid);
+    await this._instanceLifecycleService.deleteInstance(uuid);
+    await this._deviceCleanupRegistry.disposeDevice(uuid);
+    await this._notifyShutdownCompleted(uuid);
   }
 }
 
diff --git a/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.js b/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.js
index 394775f8c..213f482c0 100644
--- a/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.js
+++ b/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.js
@@ -18,11 +18,11 @@ class GenyInstanceLookupService {
   }
 
   async _getRelevantInstances() {
-    const { rawDevices: takenInstanceUUIDs } = this.deviceRegistry.getRegisteredDevices();
+    const takenInstances = this.deviceRegistry.getRegisteredDevices();
     const isRelevant = (instance) =>
       (instance.isOnline() || instance.isInitializing()) &&
       this.instanceNaming.isFamilial(instance.name) &&
-      !takenInstanceUUIDs.includes(instance.uuid);
+      !takenInstances.includes(instance.uuid);
 
     const instances = await this._getAllInstances();
     return instances.filter(isRelevant);
diff --git a/detox/src/devices/drivers/ios/SimulatorDriver.js b/detox/src/devices/drivers/ios/SimulatorDriver.js
index 73817ddf0..5b6f79083 100644
--- a/detox/src/devices/drivers/ios/SimulatorDriver.js
+++ b/detox/src/devices/drivers/ios/SimulatorDriver.js
@@ -10,7 +10,6 @@ const SimulatorLogPlugin = require('../../../artifacts/log/ios/SimulatorLogPlugi
 const SimulatorRecordVideoPlugin = require('../../../artifacts/video/SimulatorRecordVideoPlugin');
 const SimulatorScreenshotPlugin = require('../../../artifacts/screenshot/SimulatorScreenshotPlugin');
 const temporaryPath = require('../../../artifacts/utils/temporaryPath');
-const DetoxConfigError = require('../../../errors/DetoxConfigError');
 const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
 const environment = require('../../../utils/environment');
 const argparse = require('../../../utils/argparse');
@@ -249,7 +248,8 @@ class SimulatorDriver extends IosDriver {
   async _groupDevicesByStatus(deviceQuery) {
     const searchResults = await this._queryDevices(deviceQuery);
     const { rawDevices: takenDevices } = this.deviceRegistry.getRegisteredDevices();
-    const { taken, free }  = _.groupBy(searchResults, ({ udid }) => takenDevices.includes(udid) ? 'taken' : 'free');
+    const takenUDIDs = _.map(takenDevices, 'id');
+    const { taken, free }  = _.groupBy(searchResults, ({ udid }) => takenUDIDs.includes(udid) ? 'taken' : 'free');
 
     const targetOS = _.get(taken, '0.os.identifier');
     const isMatching = targetOS && { os: { identifier: targetOS } };
@@ -273,7 +273,6 @@ class SimulatorDriver extends IosDriver {
               `It is advised only to specify a device type, e.g., "iPhone Xʀ" and avoid explicit search by OS version.`
       });
     }
-
     return result;
   }
 


Test patch:
diff --git a/detox/src/devices/DeviceRegistry.test.js b/detox/src/devices/DeviceRegistry.test.js
index 39e33c402..f65e21049 100644
--- a/detox/src/devices/DeviceRegistry.test.js
+++ b/detox/src/devices/DeviceRegistry.test.js
@@ -2,32 +2,9 @@ const fs = require('fs-extra');
 const tempfile = require('tempfile');
 const environment = require('../utils/environment');
 
-const deviceHandle = 'emulator-5554';
-
-const deviceHandleAsRawObj = {
-  type: 'mocked-device-type',
-  adbName: 'emulator-5556',
-};
-const deviceHandleAsObj = {
-  ...deviceHandleAsRawObj,
-  mockFn: () => 'mock-fn-result',
-};
-
-class DeviceHandle {
-  constructor() {
-    this.type = 'mocked-device-type';
-    this.adbName = 'localhost:11111';
-  }
-
-  mockFunc() {
-    return 'mocked-func-result';
-  }
-}
-const deviceHandleAsClassInstance = new DeviceHandle();
-const deviceHandleInstanceRaw = {
-  type: 'mocked-device-type',
-  adbName: 'localhost:11111',
-}
+const deviceId = 'emulator-5554';
+const deviceId2 = 'emulator-5556';
+const mockData = { mock: 'data' };
 
 describe('DeviceRegistry', () => {
   let DeviceRegistry;
@@ -46,14 +23,14 @@ describe('DeviceRegistry', () => {
       await fs.remove(lockfilePath);
     });
 
-    async function allocateDevice(deviceHandle) {
-      return registry.allocateDevice(() => deviceHandle);
+    async function allocateDevice(deviceId, data) {
+      return registry.allocateDevice(() => deviceId, data);
     }
 
-    async function checkDeviceRegisteredAndDispose(deviceHandle) {
+    async function checkDeviceRegisteredAndDispose(deviceId) {
       return registry.disposeDevice(async () => {
-        expect(registry.includes(deviceHandle)).toBe(true);
-        return deviceHandle;
+        expect(registry.includes(deviceId)).toBe(true);
+        return deviceId;
       });
     }
 
@@ -61,34 +38,34 @@ describe('DeviceRegistry', () => {
       return registry.disposeDevice(() => deviceHandle);
     }
 
-    async function expectDeviceNotRegistered(deviceHandle) {
+    async function expectDeviceNotRegistered(deviceId) {
       return registry.allocateDevice(async () => {
-        expect(registry.includes(deviceHandle)).toBe(false);
+        expect(registry.includes(deviceId)).toBe(false);
         throw new Error('ignored'); // So it wouldn't really allocate anything
       }).catch((e) => { if (e.message !== 'ignored') throw e });
     }
 
-    function expectIncludedInDevicesList(deviceHandle) {
+    function expectIncludedInDevicesList(deviceId) {
       return registry.allocateDevice(() => {
         const registeredDevices = registry.getRegisteredDevices();
-        expect(registeredDevices.includes(deviceHandle)).toEqual(true);
+        expect(registeredDevices.includes(deviceId)).toEqual(true);
 
         throw new Error('ignored'); // So it wouldn't really allocate anything
       }).catch((e) => { if (e.message !== 'ignored') throw e });
     }
 
-    function expectDevicesListEquals(rawDevicesHandles) {
+    function expectDevicesListEquals(rawDevices) {
       return registry.allocateDevice(() => {
         const registeredDevices = registry.getRegisteredDevices();
-        expect(registeredDevices.rawDevices).toStrictEqual(rawDevicesHandles);
+        expect(registeredDevices.rawDevices).toStrictEqual(rawDevices);
 
         throw new Error('ignored'); // So it wouldn't really allocate anything
       }).catch((e) => { if (e.message !== 'ignored') throw e });
     }
 
-    async function expectIncludedInReadDevicesList(deviceHandle) {
+    async function expectIncludedInReadDevicesList(deviceId) {
       const registeredDevices = await registry.readRegisteredDevices();
-      expect(registeredDevices.includes(deviceHandle)).toEqual(true);
+      expect(registeredDevices.includes(deviceId)).toEqual(true);
     }
 
     async function expectNotIncludedInReadDevicesList(deviceHandle) {
@@ -118,112 +95,81 @@ describe('DeviceRegistry', () => {
       expect(() => registry.getRegisteredDevices()).toThrowError();
 
     it('should be able to tell whether a device is registered - for query and disposal', async () => {
-      await allocateDevice(deviceHandle);
-      await checkDeviceRegisteredAndDispose(deviceHandle);
-      await expectDeviceNotRegistered(deviceHandle);
-    });
-
-    it('should be able to tell whether a device is registered, given objects as handles', async () => {
-      await allocateDevice(deviceHandleAsObj);
-      await checkDeviceRegisteredAndDispose(deviceHandleAsObj);
-      await expectDeviceNotRegistered(deviceHandleAsObj);
+      await allocateDevice(deviceId);
+      await checkDeviceRegisteredAndDispose(deviceId);
+      await expectDeviceNotRegistered(deviceId);
     });
 
-    it('should be able to tell whether a device is registered, given class instances as handles', async () => {
-      await allocateDevice(deviceHandleAsClassInstance);
-      await checkDeviceRegisteredAndDispose(deviceHandleAsClassInstance);
-      await expectDeviceNotRegistered(deviceHandleAsClassInstance);
+    it('should be able to tell whether a device is registered, even with custom data associated with it', async () => {
+      await allocateDevice(deviceId, { mock: 'data' });
+      await checkDeviceRegisteredAndDispose(deviceId);
+      await expectDeviceNotRegistered(deviceId);
     });
 
     it('should throw on attempt of checking whether a device is registered outside of allocation/disposal context', async () => {
       assertForbiddenOutOfContextRegistryQuery();
 
-      await allocateDevice(deviceHandle);
+      await allocateDevice(deviceId);
       assertForbiddenOutOfContextRegistryQuery();
     });
 
     it('should be able to in-context get a valid list of registered devices, and query its content', async () => {
-      await allocateDevice(deviceHandle);
-      await allocateDevice(deviceHandleAsObj);
-      await allocateDevice(deviceHandleAsClassInstance);
-      await expectIncludedInDevicesList(deviceHandle);
-      await expectIncludedInDevicesList(deviceHandleAsObj);
-      await expectIncludedInDevicesList(deviceHandleAsClassInstance);
+      await allocateDevice(deviceId);
+      await allocateDevice(deviceId2, { mock: 'data' });
+      await expectIncludedInDevicesList(deviceId);
+      await expectIncludedInDevicesList(deviceId2);
     });
 
-    it('should be able to in-context-get a valid list of (raw) registered devices as an array', async () => {
-      const expectedrawDevicesList = [
-        deviceHandle,
-        deviceHandleAsRawObj,
-        deviceHandleInstanceRaw,
+    it('should be able to in-context-get a valid list of registered devices as a raw objects array, also containing custom data', async () => {
+      const expectedDevicesList = [
+        { id: deviceId, },
+        { id: deviceId2, data: mockData, },
       ];
 
-      await allocateDevice(deviceHandle);
-      await allocateDevice(deviceHandleAsObj);
-      await allocateDevice(deviceHandleAsClassInstance);
-      await expectDevicesListEquals(expectedrawDevicesList);
+      await allocateDevice(deviceId);
+      await allocateDevice(deviceId2, mockData);
+      await expectDevicesListEquals(expectedDevicesList);
     });
 
     it('should throw on an attempt of in-context getting registered devices list when outside of allocation/disposal context', async () => {
       assertForbiddenOutOfContextDeviceListQuery();
 
-      await allocateDevice(deviceHandle);
+      await allocateDevice(deviceId);
       assertForbiddenOutOfContextDeviceListQuery();
     });
 
     it('should be able to out-of-context read a valid list of registered devices and query its content', async () => {
-      await allocateDevice(deviceHandle);
-      await allocateDevice(deviceHandleAsObj);
-      await allocateDevice(deviceHandleAsClassInstance);
-      await expectIncludedInReadDevicesList(deviceHandle);
-      await expectIncludedInReadDevicesList(deviceHandleAsObj);
+      await allocateDevice(deviceId);
+      await allocateDevice(deviceId2, { mock: 'data' });
+      await expectIncludedInReadDevicesList(deviceId);
+      await expectIncludedInReadDevicesList(deviceId2);
     });
 
-    it('should be able to out-of-context-read a valid list of (raw) registered devices as an array', async () => {
+    it('should be able to out-of-context-read a valid list of registered devices as a raw objects array, also containing custom data', async () => {
       const expectedDevicesList = [
-        deviceHandle,
-        deviceHandleAsRawObj,
-        deviceHandleInstanceRaw,
+        { id: deviceId, },
+        { id: deviceId2, data: mockData, },
       ];
 
-      await allocateDevice(deviceHandle);
-      await allocateDevice(deviceHandleAsObj);
-      await allocateDevice(deviceHandleAsClassInstance);
+      await allocateDevice(deviceId);
+      await allocateDevice(deviceId2, mockData);
       await expectReadDevicesListEquals(expectedDevicesList);
     });
 
     it('should allow for UNSAFE (non-concurrent) reading of registered devices list, even outside of allocation/disposal context', async () => {
       const expectedDevicesList = [
-        deviceHandle,
-        deviceHandleAsRawObj,
-        deviceHandleInstanceRaw,
+        { id: deviceId, },
+        { id: deviceId2, data: mockData, },
       ];
 
-      await allocateDevice(deviceHandle);
-      await allocateDevice(deviceHandleAsObj);
-      await allocateDevice(deviceHandleAsClassInstance);
+      await allocateDevice(deviceId);
+      await allocateDevice(deviceId2, mockData);
 
-      await expectIncludedInUnsafeDevicesList(deviceHandle);
-      await expectIncludedInUnsafeDevicesList(deviceHandleAsObj);
-      await expectIncludedInUnsafeDevicesList(deviceHandleAsClassInstance);
+      await expectIncludedInUnsafeDevicesList(deviceId);
+      await expectIncludedInUnsafeDevicesList(deviceId2);
       await expectedUnsafeDevicesListEquals(expectedDevicesList);
     });
 
-    it('should be able to dispose devices of all types', async () => {
-      await allocateDevice(deviceHandle);
-      await allocateDevice(deviceHandleAsObj);
-      await allocateDevice(deviceHandleAsClassInstance);
-
-      await disposeDevice(deviceHandle);
-      await expectNotIncludedInReadDevicesList(deviceHandle);
-
-      await disposeDevice(deviceHandleAsObj);
-      await expectNotIncludedInReadDevicesList(deviceHandleAsObj);
-
-      await disposeDevice(deviceHandleAsClassInstance);
-      await expectNotIncludedInReadDevicesList(deviceHandleAsClassInstance);
-    });
-
     it('should not fail when there were no actual device to dispose', async () => {
       await expect(disposeDevice(undefined)).resolves.not.toThrowError();
     });
diff --git a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
index dc616942b..8dd4b8107 100644
--- a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
+++ b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
@@ -198,10 +198,7 @@ describe('Genymotion-cloud driver', () => {
         const deviceQuery = aDeviceQuery();
         const result = await uut.acquireFreeDevice(deviceQuery);
 
-        expect(result).toEqual(expect.objectContaining({
-          adbName: instance.adbName,
-          uuid: instance.uuid,
-        }));
+        expect(result).toEqual(instance);
         expect(recipeQuerying().getRecipeFromQuery).toHaveBeenCalledWith(deviceQuery);
         expect(instanceAllocation().allocateDevice).toHaveBeenCalledWith(recipe);
       });
@@ -321,12 +318,16 @@ describe('Genymotion-cloud driver', () => {
       GenyCloudDriver = require('./GenyCloudDriver');
     });
 
-    const aPendingInstanceHandle = (name, uuid) => ({ name, uuid });
+    // As typically returned by the DeviceRegistry
+    const aPendingRawDevice = (name, uuid) => ({
+      id: uuid,
+      data: { name },
+    });
 
     describe('global clean-up', () => {
-      const givenDeletionPendingDevices = (devicesHandles) => deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue({ rawDevices: devicesHandles });
+      const givenDeletionPendingDevices = (rawDevices) => deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue({ rawDevices });
       const givenNoDeletionPendingDevices = () => givenDeletionPendingDevices([]);
-      const givenDeletionPendingInstances = (instances) => givenDeletionPendingDevices( _.map(instances, ({ uuid, name }) => ({ uuid, name }) ));
+      const givenDeletionPendingInstances = (instances) => givenDeletionPendingDevices( _.map(instances, ({ uuid, name }) => aPendingRawDevice(name, uuid)) );
       const givenDeletionResult = (deletedInstance) => instanceLifecycleService.deleteInstance.mockResolvedValue(deletedInstance);
 
       const anAssertablePendingPromise = () => {
@@ -343,11 +344,10 @@ describe('Genymotion-cloud driver', () => {
           .mockReturnValueOnce(killPromise1)
           .mockReturnValueOnce(killPromise2);
 
-        const deviceHandles = [
-          aPendingInstanceHandle('device1', 'uuid1'),
-          aPendingInstanceHandle('device2', 'uuid2'),
-        ];
-        givenDeletionPendingDevices(deviceHandles);
+        givenDeletionPendingDevices([
+          aPendingRawDevice('device1', 'uuid1'),
+          aPendingRawDevice('device2', 'uuid2'),
+        ]);
 
         await GenyCloudDriver.globalCleanup();
 
@@ -364,9 +364,9 @@ describe('Genymotion-cloud driver', () => {
           .mockRejectedValueOnce(new Error('mock-error2'));
 
         givenDeletionPendingDevices([
-          aPendingInstanceHandle('failing1', 'uuid1'),
-          aPendingInstanceHandle('nonfailing', 'uuid'),
-          aPendingInstanceHandle('failing2', 'uuid2'),
+          aPendingRawDevice('failing1', 'uuid1'),
+          aPendingRawDevice('nonfailing', 'uuid'),
+          aPendingRawDevice('failing2', 'uuid2'),
         ]);
 
         await GenyCloudDriver.globalCleanup();
@@ -414,7 +414,7 @@ describe('Genymotion-cloud driver', () => {
     describe('global *emergency* clean-up', () => {
       const signalExitCallback = () => signalExit.mock.calls[0][0];
       const invokeExitCallback = (signal = 'SIGINT') => signalExitCallback()(null, signal);
-      const givenCleanupPendingDevices = (devicesHandles) => deviceCleanupRegistry.readRegisteredDevicesUNSAFE.mockReturnValue({ rawDevices: devicesHandles });
+      const givenCleanupPendingDevices = (rawDevices) => deviceCleanupRegistry.readRegisteredDevicesUNSAFE.mockReturnValue({ rawDevices });
       const givenNoCleanupPendingDevices = () => givenCleanupPendingDevices([]);
 
       it('should register a callback on global init via signal-exit, for an emergency global clean-up', async () => {
@@ -425,7 +425,7 @@ describe('Genymotion-cloud driver', () => {
 
       it('should warn of leaking instances in signal-exit callback', async () => {
         givenCleanupPendingDevices([
-          aPendingInstanceHandle('aDevice', 'uuid'),
+          aPendingRawDevice('aDevice', 'uuid'),
         ]);
 
         await GenyCloudDriver.globalInit();
@@ -447,7 +447,7 @@ describe('Genymotion-cloud driver', () => {
 
       it('should not warn if called with no signal', async () => {
         givenCleanupPendingDevices([
-          aPendingInstanceHandle('aDevice', 'uuid'),
+          aPendingRawDevice('aDevice', 'uuid'),
         ]);
 
         await GenyCloudDriver.globalInit();
diff --git a/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceLauncher.test.js b/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceLauncher.test.js
index 816f7d1d7..0886691a3 100644
--- a/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceLauncher.test.js
+++ b/detox/src/devices/drivers/android/genycloud/helpers/GenyCloudInstanceLauncher.test.js
@@ -34,10 +34,7 @@ describe('Genymotion-Cloud instance launcher', () => {
       const instance = anInstance();
 
       await uut.launch(instance);
-      expect(deviceCleanupRegistry.allocateDevice).toHaveBeenCalledWith({
-        uuid: instance.uuid,
-        name: instance.name,
-      })
+      expect(deviceCleanupRegistry.allocateDevice).toHaveBeenCalledWith(instance.uuid, { name: instance.name });
     });
   });
 
@@ -58,17 +55,15 @@ describe('Genymotion-Cloud instance launcher', () => {
     it('should remove the instance from the cleanup registry', async () => {
       const instance = anInstance();
       await uut.shutdown(instance);
-      expect(deviceCleanupRegistry.disposeDevice).toHaveBeenCalledWith(expect.objectContaining({
-        uuid: instance.uuid,
-      }));
+      expect(deviceCleanupRegistry.disposeDevice).toHaveBeenCalledWith(instance.uuid);
     });
 
     it('should emit associated events', async () => {
       const instance = anInstance();
       await uut.shutdown(instance);
 
-      expect(eventEmitter.emit).toHaveBeenCalledWith('beforeShutdownDevice', { deviceId: instance.adbName });
-      expect(eventEmitter.emit).toHaveBeenCalledWith('shutdownDevice', { deviceId: instance.adbName });
+      expect(eventEmitter.emit).toHaveBeenCalledWith('beforeShutdownDevice', { deviceId: instance.uuid });
+      expect(eventEmitter.emit).toHaveBeenCalledWith('shutdownDevice', { deviceId: instance.uuid });
     });
 
     it('should not emit shutdownDevice prematurely', async () => {
diff --git a/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.test.js b/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.test.js
index 08ee7260b..65faf6600 100644
--- a/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.test.js
+++ b/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.test.js
@@ -44,8 +44,7 @@ describe('Genymotion-Cloud instances lookup service', () => {
   function givenRegisteredInstances(...instances) {
     const instanceUUIDs = _.map(instances, 'uuid');
     deviceRegistry.getRegisteredDevices.mockReturnValue({
-      rawDevices: instanceUUIDs,
-      includes: instanceUUIDs.includes,
+      includes: instanceUUIDs.includes.bind(instanceUUIDs),
     });
   }
   const givenNoRegisteredInstances = () => givenRegisteredInstances([]);
diff --git a/detox/src/devices/drivers/ios/SimulatorDriver.test.js b/detox/src/devices/drivers/ios/SimulatorDriver.test.js
index e5dbcb9ae..04d9c7407 100644
--- a/detox/src/devices/drivers/ios/SimulatorDriver.test.js
+++ b/detox/src/devices/drivers/ios/SimulatorDriver.test.js
@@ -120,17 +120,29 @@ describe('IOS simulator driver', () => {
   });
 
   describe('acquireFreeDevice', () => {
+    const givenUsedSimulators = (...UDIDs) => {
+      jest.spyOn(uut.deviceRegistry, 'getRegisteredDevices').mockReturnValue({
+        rawDevices: UDIDs.map((UDID) => ({ id: UDID })), // as typically returned by getRegisteredDevices()
+        includes: UDIDs.includes.bind(UDIDs),
+      });
+    }
+    const givenNoUsedSimulators = () => givenUsedSimulators();
+    const givenSystemDevices = (...deviceSpecs) => uut.applesimutils.list.mockResolvedValue([ ...deviceSpecs ]);
+    const givenCreatedDeviceUDID = (udid) => uut.applesimutils.create.mockReturnValue(udid);
+    const aDeviceSpec = (udid) => ({
+      udid,
+      os: {
+        identifier: 'mock-OS',
+      },
+    });
+
     let applesimutils;
 
     beforeEach(() => {
       const SimulatorDriver = require('./SimulatorDriver');
       uut = new SimulatorDriver({ client: {}, emitter });
 
-      jest.spyOn(uut.deviceRegistry, 'includes').mockReturnValue(false);
-      jest.spyOn(uut.deviceRegistry, 'getRegisteredDevices').mockReturnValue({
-        rawDevices: [],
-        includes: jest.fn().mockReturnValue(false),
-      });
+      givenNoUsedSimulators();
 
       applesimutils = uut.applesimutils;
       applesimutils.list.mockImplementation(async () => require('./tools/applesimutils.mock')['--list']);
@@ -189,12 +201,41 @@ describe('IOS simulator driver', () => {
         'Searching for device by type = "iPad 2" and by OS = "iOS 9.3.6" ...'
       );
     });
+
+    it('should create a device', async () => {
+      const udidUsed = 'mock-used-udid';
+      const udidNew = 'mock-new-udid';
+      const specUsed = aDeviceSpec(udidUsed);
+
+      givenSystemDevices(specUsed);
+      givenCreatedDeviceUDID(udidNew);
+      givenUsedSimulators(udidUsed);
+
+      const result = await uut.acquireFreeDevice('iPhone Mock');
+
+      expect(uut.applesimutils.create).toHaveBeenCalledWith(specUsed);
+      expect(result).toEqual(udidNew);
+    });
+
+    it('should reuse a matching device', async () => {
+      const udid = 'mock-device-udid';
+      const specUsed = aDeviceSpec(udid);
+
+      givenSystemDevices(specUsed);
+      givenNoUsedSimulators();
+
+      const result = await uut.acquireFreeDevice('iPhone Mock');
+
+      expect(result).toEqual(udid);
+      expect(uut.applesimutils.create).not.toHaveBeenCalled();
+    });
   });
 });
 
 class mockAppleSimUtils {
   constructor() {
     this.launch = jest.fn();
+    this.create = jest.fn();
     this.setBiometricEnrollment = jest.fn();
     this.matchBiometric = jest.fn();
     this.unmatchBiometric = jest.fn();