Repository: wix/Detox
Pull request #2560 (2020-12-30 21:07:44)

DeviceRegistry to support all handle types
## Description

Expand `DeviceRegistry` so as to be made capable of storing device-handles of 3 possible types, rather than just ~2:
- Explicit strings
- Plain objects (functions stripped away)
- New: Class instances.

That is achieved by making changes on two levels:
- Reimplementation of `DeviceRegistry`'s modification methods (e.g. device allocation / dispose) - also `includes`, so as to sanitize (i.e. strip-off functions, any class meta data, etc.) the given handle before using it. This is a non-API-breaking change.
- Change of registry whole-state query methods, namely `getRegisteredDevices()`, `readRegisteredDevices()` and `readRegisteredDevicesUNSAFE()` such that instead of returning an array, they'd return an abstraction (`DeviceHandlesList` class) that's different in two ways:
  - It allows for a full access to the device list, but only via the `sanitizedDevices` property which makes it clear for the caller that they are actually only seeing _sanitized_ handles -- implementation constraints 🤷🏻  (to be clear, this was how things were regardless of this change - it's just been made explicit by it).
  - It provides an `includes` method (equivalent to `Array.includes()`, formerly used) that can work with all of the 3 handle types (i.e. by sanitizing before making any comparisons) - allowing the user to provide a handle in the original form used for allocation.

For the latter, associated usage of these API's have been changed to match the changes.

Last but not least, in turn -- this fixes a regression in Genymotion-cloud shutdown when using the `--cleanup` flag, introduced due to the switching to class instances as device-registry handles for Genymotion instances cleanup, in #2476. That in fact was what motivated this change in the first place.


Patch:
diff --git a/detox/src/devices/DeviceRegistry.js b/detox/src/devices/DeviceRegistry.js
index 4691ff08a..d46def564 100644
--- a/detox/src/devices/DeviceRegistry.js
+++ b/detox/src/devices/DeviceRegistry.js
@@ -10,6 +10,22 @@ const readOptions = {
   encoding: 'utf8',
 };
 
+class DeviceHandlesList {
+  constructor(devices) {
+    this.rawDevices = Object.freeze(devices);
+  }
+
+  includes(deviceHandle) {
+    return DeviceHandlesList._includes(deviceHandle, this.rawDevices);
+  }
+
+  static _includes(deviceHandle, devices) {
+    const rawDeviceHandle = getRawDeviceHandle(deviceHandle);
+    const deviceEqualsFn = getDeviceEqualsFn(rawDeviceHandle);
+    return !!_.find(devices, deviceEqualsFn);
+  }
+}
+
 class DeviceRegistry {
   constructor({ lockfilePath }) {
     /***
@@ -36,7 +52,7 @@ class DeviceRegistry {
   }
 
   /***
-   * @param {string|Function} getDeviceHandle
+   * @param {string|Object|Function} getDeviceHandle
    * @returns {Promise<string>}
    */
   async allocateDevice(getDeviceHandle) {
@@ -48,7 +64,7 @@ class DeviceRegistry {
   }
 
   /***
-   * @param {string|Function} getDeviceHandle
+   * @param {string|Object|Function} getDeviceHandle
    * @returns {void}
    */
   async disposeDevice(getDeviceHandle) {
@@ -58,26 +74,41 @@ class DeviceRegistry {
     });
   }
 
+  /***
+   * @param {Object} deviceHandle
+   * @returns {boolean}
+   */
   includes(deviceHandle) {
-    const deviceEqualsFn = getDeviceEqualsFn(deviceHandle);
-    return !!_.find(this._lockfile.read(), deviceEqualsFn);
-  }
-
-  getRegisteredDevices() {
-    return this._lockfile.read();
+    const devices = this._lockfile.read();
+    return DeviceHandlesList._includes(deviceHandle, devices);
   }
 
+  /***
+   * @returns {DeviceHandlesList}
+   */
   async readRegisteredDevices() {
-    let result;
+    let result = null;
     await this._lockfile.exclusively(() => {
-      result = this.getRegisteredDevices();
+      result = new DeviceHandlesList(this._lockfile.read());
     })
     return result;
   }
 
+  /***
+   * @returns {DeviceHandlesList}
+   */
+  getRegisteredDevices() {
+    const deviceHandles = this._lockfile.read();
+    return new DeviceHandlesList(deviceHandles);
+  }
+
+  /***
+   * @returns {DeviceHandlesList}
+   */
   readRegisteredDevicesUNSAFE() {
     const contents = fs.readFileSync(this._lockfilePath, readOptions);
-    return JSON.parse(contents);
+    const devices = JSON.parse(contents);
+    return new DeviceHandlesList(devices);
   }
 
   /***
@@ -91,10 +122,11 @@ class DeviceRegistry {
    * @private
    */
   _toggleDeviceStatus(deviceHandle, busy) {
-    const deviceDifferFn = getDeviceDifferFn(deviceHandle);
+    const rawDeviceHandle = getRawDeviceHandle(deviceHandle);
+    const deviceDifferFn = getDeviceDifferFn(rawDeviceHandle);
     const state = this._lockfile.read();
     const newState = busy
-      ? _.concat(state, deviceHandle)
+      ? _.concat(state, rawDeviceHandle)
       : _.filter(state, deviceDifferFn);
     this._lockfile.write(newState);
   }
@@ -112,4 +144,8 @@ 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 ff8ed6f6c..53a4983f7 100644
--- a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js
+++ b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js
@@ -3,6 +3,7 @@ const onSignalExit = require('signal-exit');
 const AndroidDriver = require('../AndroidDriver');
 const GenyCloudDeviceAllocator = require('./GenyCloudDeviceAllocator');
 const GenyDeviceRegistryFactory = require('./GenyDeviceRegistryFactory');
+const GenyCloudInstanceHandle = require('./GenyCloudInstanceHandle');
 const GenyCloudExec = require('./exec/GenyCloudExec');
 const RecipesService = require('./services/GenyRecipesService');
 const InstanceLookupService = require('./services/GenyInstanceLookupService');
@@ -76,9 +77,11 @@ class GenyCloudDriver extends AndroidDriver {
   }
 
   async shutdown(instance) {
+    const instanceHandle = new GenyCloudInstanceHandle(instance);
+
     await this.emitter.emit('beforeShutdownDevice', { deviceId: instance.adbName });
     await this._instanceLifecycleService.deleteInstance(instance.uuid);
-    await this._deviceCleanupRegistry.disposeDevice(instance.uuid);
+    await this._deviceCleanupRegistry.disposeDevice(instanceHandle);
     await this.emitter.emit('shutdownDevice', { deviceId: instance.adbName });
   }
 
@@ -111,18 +114,20 @@ class GenyCloudDriver extends AndroidDriver {
   }
 
   static async globalInit() {
-    onSignalExit(() => {
-      const deviceCleanupRegistry = GenyDeviceRegistryFactory.forGlobalShutdown();
-      const instanceHandles = deviceCleanupRegistry.readRegisteredDevicesUNSAFE();
-      if (instanceHandles.length) {
-        reportGlobalCleanupSummary(instanceHandles);
+    onSignalExit((code, signal) => {
+      if (signal) {
+        const deviceCleanupRegistry = GenyDeviceRegistryFactory.forGlobalShutdown();
+        const { rawDevices: instanceHandles } = deviceCleanupRegistry.readRegisteredDevicesUNSAFE();
+        if (instanceHandles.length) {
+          reportGlobalCleanupSummary(instanceHandles);
+        }
       }
     });
   }
 
   static async globalCleanup() {
     const deviceCleanupRegistry = GenyDeviceRegistryFactory.forGlobalShutdown();
-    const instanceHandles = await deviceCleanupRegistry.readRegisteredDevices();
+    const { rawDevices: instanceHandles } = await deviceCleanupRegistry.readRegisteredDevices();
     if (instanceHandles.length) {
       const exec = new GenyCloudExec(environment.getGmsaasPath());
       const instanceLifecycleService = new InstanceLifecycleService(exec, null);
diff --git a/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.js b/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.js
index e4158b0b1..394775f8c 100644
--- a/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.js
+++ b/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.js
@@ -18,7 +18,7 @@ class GenyInstanceLookupService {
   }
 
   async _getRelevantInstances() {
-    const takenInstanceUUIDs = this.deviceRegistry.getRegisteredDevices();
+    const { rawDevices: takenInstanceUUIDs } = this.deviceRegistry.getRegisteredDevices();
     const isRelevant = (instance) =>
       (instance.isOnline() || instance.isInitializing()) &&
       this.instanceNaming.isFamilial(instance.name) &&
diff --git a/detox/src/devices/drivers/ios/SimulatorDriver.js b/detox/src/devices/drivers/ios/SimulatorDriver.js
index 1818b5993..8072f0d88 100644
--- a/detox/src/devices/drivers/ios/SimulatorDriver.js
+++ b/detox/src/devices/drivers/ios/SimulatorDriver.js
@@ -244,10 +244,10 @@ class SimulatorDriver extends IosDriver {
     let udid;
 
     const deviceQuery = this._adaptQuery(rawDeviceQuery);
-    const { free, busy } = await this._groupDevicesByStatus(deviceQuery);
+    const { free, taken } = await this._groupDevicesByStatus(deviceQuery);
 
     if (_.isEmpty(free)) {
-      const prototypeDevice = busy[0];
+      const prototypeDevice = taken[0];
       udid = this.applesimutils.create(prototypeDevice);
     } else {
       udid = free[0].udid;
@@ -258,14 +258,14 @@ class SimulatorDriver extends IosDriver {
 
   async _groupDevicesByStatus(deviceQuery) {
     const searchResults = await this._queryDevices(deviceQuery);
-    const takenDevices = this.deviceRegistry.getRegisteredDevices();
-    const { busy, free }  = _.groupBy(searchResults, ({ udid }) => takenDevices.includes(udid) ? 'busy' : 'free');
+    const { rawDevices: takenDevices } = this.deviceRegistry.getRegisteredDevices();
+    const { taken, free }  = _.groupBy(searchResults, ({ udid }) => takenDevices.includes(udid) ? 'taken' : 'free');
 
-    const targetOS = _.get(busy, '0.os.identifier');
+    const targetOS = _.get(taken, '0.os.identifier');
     const isMatching = targetOS && { os: { identifier: targetOS } };
 
     return {
-      busy: _.filter(busy, isMatching),
+      taken: _.filter(taken, isMatching),
       free: _.filter(free, isMatching),
     }
   }


Test patch:
diff --git a/detox/src/devices/DeviceRegistry.test.js b/detox/src/devices/DeviceRegistry.test.js
index 29c8a4d99..9f2e20487 100644
--- a/detox/src/devices/DeviceRegistry.test.js
+++ b/detox/src/devices/DeviceRegistry.test.js
@@ -2,6 +2,33 @@ 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',
+}
+
 describe('DeviceRegistry', () => {
   let DeviceRegistry;
 
@@ -23,13 +50,6 @@ describe('DeviceRegistry', () => {
       return registry.allocateDevice(() => deviceHandle);
     }
 
-    function expectRegisteredDevices(...deviceHandles) {
-      return registry.allocateDevice(() => {
-        expect(registry.getRegisteredDevices()).toEqual([ ...deviceHandles ]);
-        throw new Error('ignored'); // So it wouldn't really allocate anything
-      }).catch((e) => { if (e.message !== 'ignored') throw e });
-    }
-
     async function checkDeviceRegisteredAndDispose(deviceHandle) {
       return registry.disposeDevice(async () => {
         expect(registry.includes(deviceHandle)).toBe(true);
@@ -41,15 +61,54 @@ describe('DeviceRegistry', () => {
       return registry.disposeDevice(() => deviceHandle);
     }
 
-    async function checkDeviceIsNotRegistered(deviceHandle) {
+    async function expectDeviceNotRegistered(deviceHandle) {
       return registry.allocateDevice(async () => {
         expect(registry.includes(deviceHandle)).toBe(false);
         throw new Error('ignored'); // So it wouldn't really allocate anything
       }).catch((e) => { if (e.message !== 'ignored') throw e });
     }
 
-    async function checkRegisteredDevicesEqual(...deviceHandles) {
-      expect(await registry.readRegisteredDevices()).toEqual([ ...deviceHandles ]);
+    function expectIncludedInDevicesList(deviceHandle) {
+      return registry.allocateDevice(() => {
+        const registeredDevices = registry.getRegisteredDevices();
+        expect(registeredDevices.includes(deviceHandle)).toEqual(true);
+
+        throw new Error('ignored'); // So it wouldn't really allocate anything
+      }).catch((e) => { if (e.message !== 'ignored') throw e });
+    }
+
+    function expectDevicesListEquals(rawDevicesHandles) {
+      return registry.allocateDevice(() => {
+        const registeredDevices = registry.getRegisteredDevices();
+        expect(registeredDevices.rawDevices).toStrictEqual(rawDevicesHandles);
+
+        throw new Error('ignored'); // So it wouldn't really allocate anything
+      }).catch((e) => { if (e.message !== 'ignored') throw e });
+    }
+
+    async function expectIncludedInReadDevicesList(deviceHandle) {
+      const registeredDevices = await registry.readRegisteredDevices();
+      expect(registeredDevices.includes(deviceHandle)).toEqual(true);
+    }
+
+    async function expectNotIncludedInReadDevicesList(deviceHandle) {
+      const registeredDevices = await registry.readRegisteredDevices();
+      expect(registeredDevices.includes(deviceHandle)).not.toEqual(true);
+    }
+
+    async function expectReadDevicesListEquals(rawDeviceHandles) {
+      const registeredDevices = await registry.readRegisteredDevices();
+      expect(registeredDevices.rawDevices).toEqual(rawDeviceHandles);
+    }
+
+    async function expectIncludedInUnsafeDevicesList(deviceHandle) {
+      const registeredDevices = await registry.readRegisteredDevicesUNSAFE();
+      expect(registeredDevices.includes(deviceHandle)).toEqual(true);
+    }
+
+    async function expectedUnsafeDevicesListEquals(rawDeviceHandles) {
+      const registeredDevices = await registry.readRegisteredDevicesUNSAFE();
+      expect(registeredDevices.rawDevices).toEqual(rawDeviceHandles);
     }
 
     const assertForbiddenOutOfContextRegistryQuery = () =>
@@ -58,87 +117,111 @@ describe('DeviceRegistry', () => {
     const assertForbiddenOutOfContextDeviceListQuery = () =>
       expect(() => registry.getRegisteredDevices()).toThrowError();
 
-    it('should be able to tell whether a device is registered', async () => {
-      const deviceHandle = 'emulator-5554';
+    it('should be able to tell whether a device is registered - for query and disposal', async () => {
       await allocateDevice(deviceHandle);
       await checkDeviceRegisteredAndDispose(deviceHandle);
-      await checkDeviceIsNotRegistered(deviceHandle);
+      await expectDeviceNotRegistered(deviceHandle);
     });
 
-    it('should be able to tell whether a device is registered, given objects for handles', async () => {
-      const rawDeviceHandle = {
-        type: 'mocked-device-type',
-        adbName: 'localhost:11111',
-      };
-      const deviceHandle = {
-        ...rawDeviceHandle,
-        mockFunc: () => 'mocked-func-result',
-      };
+    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(deviceHandle);
-      await checkDeviceRegisteredAndDispose(rawDeviceHandle);
-      await checkDeviceIsNotRegistered(deviceHandle);
+    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 throw on attempt of checking whether a device is registered outside of allocation/disposal context', async () => {
-      const deviceHandle = 'emulator-5554';
-
       assertForbiddenOutOfContextRegistryQuery();
 
       await allocateDevice(deviceHandle);
       assertForbiddenOutOfContextRegistryQuery();
     });
 
-    it('should be able to fast-get a valid list of registered devices', async () => {
-      const deviceHandle = 'emulator-5554';
-      const anotherDeviceHandle = {
-        type: 'mocked-device-type',
-        adbName: 'emulator-5556',
-      };
-
+    it('should be able to in-context get a valid list of registered devices, and query its content', async () => {
       await allocateDevice(deviceHandle);
-      await allocateDevice(anotherDeviceHandle);
-      await expectRegisteredDevices(deviceHandle, anotherDeviceHandle);
+      await allocateDevice(deviceHandleAsObj);
+      await allocateDevice(deviceHandleAsClassInstance);
+      await expectIncludedInDevicesList(deviceHandle);
+      await expectIncludedInDevicesList(deviceHandleAsObj);
+      await expectIncludedInDevicesList(deviceHandleAsClassInstance);
     });
 
-    it('should throw on attempt of fast-getting registered devices list outside of allocation/disposal context', async () => {
-      const deviceHandle = 'emulator-5554';
+    it('should be able to in-context-get a valid list of (raw) registered devices as an array', async () => {
+      const expectedrawDevicesList = [
+        deviceHandle,
+        deviceHandleAsRawObj,
+        deviceHandleInstanceRaw,
+      ];
 
+      await allocateDevice(deviceHandle);
+      await allocateDevice(deviceHandleAsObj);
+      await allocateDevice(deviceHandleAsClassInstance);
+      await expectDevicesListEquals(expectedrawDevicesList);
+    });
+
+    it('should throw on an attempt of in-context getting registered devices list when outside of allocation/disposal context', async () => {
       assertForbiddenOutOfContextDeviceListQuery();
 
       await allocateDevice(deviceHandle);
       assertForbiddenOutOfContextDeviceListQuery();
     });
 
-    it('should allow UNSAFE-getting of registered devices list, even outside of allocation/disposal context', async () => {
-      const deviceHandle = 'emulator-5554';
+    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);
+    });
+
+    it('should be able to out-of-context-read a valid list of (raw) registered devices as an array', async () => {
+      const expectedDevicesList = [
+        deviceHandle,
+        deviceHandleAsRawObj,
+        deviceHandleInstanceRaw,
+      ];
 
       await allocateDevice(deviceHandle);
-      const result = registry.readRegisteredDevicesUNSAFE();
-      expect(result).toEqual([ deviceHandle ]);
+      await allocateDevice(deviceHandleAsObj);
+      await allocateDevice(deviceHandleAsClassInstance);
+      await expectReadDevicesListEquals(expectedDevicesList);
     });
 
-    it('should be able to read a valid list of registered devices', async () => {
-      const deviceHandle = 'emulator-5554';
-      const anotherDeviceHandle = 'emulator-5556';
+    it('should allow for UNSAFE (non-concurrent) reading of registered devices list, even outside of allocation/disposal context', async () => {
+      const expectedDevicesList = [
+        deviceHandle,
+        deviceHandleAsRawObj,
+        deviceHandleInstanceRaw,
+      ];
 
       await allocateDevice(deviceHandle);
-      await allocateDevice(anotherDeviceHandle);
-      await checkRegisteredDevicesEqual(deviceHandle, anotherDeviceHandle);
-      await disposeDevice(deviceHandle);
-      await checkRegisteredDevicesEqual(anotherDeviceHandle);
+      await allocateDevice(deviceHandleAsObj);
+      await allocateDevice(deviceHandleAsClassInstance);
+
+      await expectIncludedInUnsafeDevicesList(deviceHandle);
+      await expectIncludedInUnsafeDevicesList(deviceHandleAsObj);
+      await expectIncludedInUnsafeDevicesList(deviceHandleAsClassInstance);
+      await expectedUnsafeDevicesListEquals(expectedDevicesList);
     });
 
-    it('should be able to dispose devices with object-like handles', async () => {
-      const deviceId = {
-        type: 'mocked-device-type',
-        adbName: 'emulator-5554',
-      };
+    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 allocateDevice(deviceId);
-      await checkRegisteredDevicesEqual(deviceId);
-      await disposeDevice(deviceId);
-      await checkRegisteredDevicesEqual();
+      await disposeDevice(deviceHandleAsClassInstance);
+      await expectNotIncludedInReadDevicesList(deviceHandleAsClassInstance);
     });
 
     describe('.reset() method', () => {
diff --git a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
index fcb140279..bde74f777 100644
--- a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
+++ b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
@@ -365,7 +365,10 @@ describe('Genymotion-cloud driver', () => {
       it('should remove instance from device-registry', async () => {
         const instance = anInstance();
         await uut.shutdown(instance);
-        expect(deviceCleanupRegistry.disposeDevice).toHaveBeenCalledWith(instance.uuid);
+        expect(deviceCleanupRegistry.disposeDevice).toHaveBeenCalledWith({
+          name: instance.name,
+          uuid: instance.uuid,
+        });
       });
     });
 
@@ -380,7 +383,7 @@ describe('Genymotion-cloud driver', () => {
     const aPendingInstanceHandle = (name, uuid) => ({ name, uuid });
 
     describe('global clean-up', () => {
-      const givenDeletionPendingDevices = (devicesHandles) => deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue(devicesHandles);
+      const givenDeletionPendingDevices = (devicesHandles) => deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue({ rawDevices: devicesHandles });
       const givenNoDeletionPendingDevices = () => givenDeletionPendingDevices([]);
       const givenDeletionPendingInstances = (instances) => givenDeletionPendingDevices( _.map(instances, ({ uuid, name }) => ({ uuid, name }) ));
       const givenDeletionResult = (deletedInstance) => instanceLifecycleService.deleteInstance.mockResolvedValue(deletedInstance);
@@ -469,8 +472,8 @@ describe('Genymotion-cloud driver', () => {
 
     describe('global *emergency* clean-up', () => {
       const signalExitCallback = () => signalExit.mock.calls[0][0];
-      const invokeExitCallback = () => signalExitCallback()();
-      const givenCleanupPendingDevices = (devicesHandles) => deviceCleanupRegistry.readRegisteredDevicesUNSAFE.mockReturnValue(devicesHandles);
+      const invokeExitCallback = (signal = 'SIGINT') => signalExitCallback()(null, signal);
+      const givenCleanupPendingDevices = (devicesHandles) => deviceCleanupRegistry.readRegisteredDevicesUNSAFE.mockReturnValue({ rawDevices: devicesHandles });
       const givenNoCleanupPendingDevices = () => givenCleanupPendingDevices([]);
 
       it('should register a callback on global init via signal-exit, for an emergency global clean-up', async () => {
@@ -500,6 +503,18 @@ describe('Genymotion-cloud driver', () => {
         expect(logger.warn).not.toHaveBeenCalled();
         expect(logger.info).not.toHaveBeenCalled();
       });
+
+      it('should not warn if called with no signal', async () => {
+        givenCleanupPendingDevices([
+          aPendingInstanceHandle('aDevice', 'uuid'),
+        ]);
+
+        await GenyCloudDriver.globalInit();
+        invokeExitCallback(null);
+
+        expect(logger.warn).not.toHaveBeenCalled();
+        expect(logger.info).not.toHaveBeenCalled();
+      });
     });
   });
 });
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 d6e053e0a..08ee7260b 100644
--- a/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.test.js
+++ b/detox/src/devices/drivers/android/genycloud/services/GenyInstanceLookupService.test.js
@@ -1,3 +1,5 @@
+const _ = require('lodash');
+
 describe('Genymotion-Cloud instances lookup service', () => {
   let exec;
   let deviceRegistry;
@@ -39,8 +41,14 @@ describe('Genymotion-Cloud instances lookup service', () => {
     },
   });
 
-  const givenRegisteredInstances = (...instances) => deviceRegistry.getRegisteredDevices.mockReturnValue([ ...instances.map((instance) => instance.uuid) ]);
-  const givenNoRegisteredInstances = () => deviceRegistry.getRegisteredDevices.mockReturnValue([]);
+  function givenRegisteredInstances(...instances) {
+    const instanceUUIDs = _.map(instances, 'uuid');
+    deviceRegistry.getRegisteredDevices.mockReturnValue({
+      rawDevices: instanceUUIDs,
+      includes: instanceUUIDs.includes,
+    });
+  }
+  const givenNoRegisteredInstances = () => givenRegisteredInstances([]);
   const givenInstances = (...instances) => exec.getInstances.mockResolvedValue({ instances });
   const givenNoInstances = () => exec.getInstances.mockResolvedValue({ instances: [] });
   const givenAnInstance = (instance) => exec.getInstance.mockResolvedValue({ instance });
diff --git a/detox/src/devices/drivers/ios/SimulatorDriver.test.js b/detox/src/devices/drivers/ios/SimulatorDriver.test.js
index 71570132d..a7e919ee5 100644
--- a/detox/src/devices/drivers/ios/SimulatorDriver.test.js
+++ b/detox/src/devices/drivers/ios/SimulatorDriver.test.js
@@ -123,8 +123,13 @@ describe('IOS simulator driver', () => {
     beforeEach(() => {
       const SimulatorDriver = require('./SimulatorDriver');
       uut = new SimulatorDriver({ client: {}, emitter });
+
       jest.spyOn(uut.deviceRegistry, 'includes').mockReturnValue(false);
-      jest.spyOn(uut.deviceRegistry, 'getRegisteredDevices').mockReturnValue([]);
+      jest.spyOn(uut.deviceRegistry, 'getRegisteredDevices').mockReturnValue({
+        rawDevices: [],
+        includes: jest.fn().mockReturnValue(false),
+      });
+
       applesimutils = uut.applesimutils;
       applesimutils.list.mockImplementation(async () => require('./tools/applesimutils.mock')['--list']);
     });