Repository: wix/Detox
Pull request #2529 (2020-12-13 13:36:36)

Restore call to Genycloud global cleanup through detox global cleanup
## Description

Restore previously reverted call to our definition of Geny-cloud "global cleanup" (i.e. the driver method that stops all associated instances).
This was reverted a while ago because it has caused issues with other platforms, running on machines where `gmsaas` was not installed.


Patch:
diff --git a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js
index 2cc0df560..abc686067 100644
--- a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js
+++ b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.js
@@ -92,16 +92,13 @@ class GenyCloudDriver extends AndroidDriver {
     }
   }
 
-  static async globalCleanup(instanceLifecycleService) {
-    if (!instanceLifecycleService) {
-      const exec = new GenyCloudExec(environment.getGmsaasPath());
-      instanceLifecycleService = new InstanceLifecycleService(exec, null);
-    }
-
+  static async globalCleanup() {
     const deviceCleanupRegistry = GenyDeviceRegistryFactory.forGlobalShutdown();
     const deviceUUIDs = await deviceCleanupRegistry.readRegisteredDevices();
     if (deviceUUIDs.length) {
-      await doCleanup(instanceLifecycleService, deviceUUIDs)
+      const exec = new GenyCloudExec(environment.getGmsaasPath());
+      const instanceLifecycleService = new InstanceLifecycleService(exec, null);
+      await doCleanup(instanceLifecycleService, deviceUUIDs);
     }
   }
 }


Test patch:
diff --git a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
index d7551a697..d5c69a491 100644
--- a/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
+++ b/detox/src/devices/drivers/android/genycloud/GenyCloudDriver.test.js
@@ -2,52 +2,37 @@ const _ = require('lodash');
 
 const latestInstanceOf = (clazz) => _.last(clazz.mock.instances);
 
-const mockBaseClassesDependencies = () => {
-  jest.mock('../exec/ADB');
-  jest.mock('../exec/AAPT');
-  jest.mock('../tools/APKPath');
-  jest.mock('../tools/TempFileXfer');
-  jest.mock('../tools/AppInstallHelper');
-  jest.mock('../tools/AppUninstallHelper');
-  jest.mock('../tools/MonitoredInstrumentation');
-  jest.mock('../../../../artifacts/utils/AndroidDevicePathBuilder');
-  jest.mock('../../../../android/espressoapi/UiDeviceProxy');
-  jest.mock('../../../../utils/logger');
-};
+describe('Genymotion-cloud driver', () => {
+  class MockInstanceLifecycleService {
+    constructor(...args) {
+      Object.assign(this, instanceLifecycleService);
+      this.ctor(...args);
+    }
+  }
 
-const mockDirectDependencies = () => {
-  jest.mock('../../../../utils/environment');
-  jest.mock('./exec/GenyCloudExec');
-  jest.mock('./GenyDeviceRegistryFactory');
-  jest.mock('./services/GenyRecipesService');
-  jest.mock('./services/GenyInstanceLookupService');
-  jest.mock('./services/GenyInstanceLifecycleService');
-  jest.mock('./services/GenyAuthService');
-  jest.mock('./services/GenyInstanceNaming');
-  jest.mock('../../../../utils/getAbsoluteBinaryPath');
-};
+  const MOCK_GMSAAS_PATH = '/path/to/gmsaas';
 
-const aRecipe = () => ({
-  uuid: 'mock-recipe-uuid',
-  name: 'mock-recipe-name',
-  toString: () => 'mock-recipe-toString()-result',
-});
+  const aRecipe = () => ({
+    uuid: 'mock-recipe-uuid',
+    name: 'mock-recipe-name',
+    toString: () => 'mock-recipe-toString()-result',
+  });
 
-const anInstance = () => ({
-  uuid: 'mock-instance-uuid',
-  adbName: 'mock-instance-adb-name',
-});
+  const anInstance = () => ({
+    uuid: 'mock-instance-uuid',
+    adbName: 'mock-instance-adb-name',
+  });
 
-const aDeviceQuery = () => ({
-  query: 'mock',
-});
+  const aDeviceQuery = () => ({
+    query: 'mock',
+  });
 
-describe('Genymotion-cloud driver', () => {
   beforeEach(mockBaseClassesDependencies);
   beforeEach(mockDirectDependencies);
 
   let logger;
   let emitter;
+  let invocationManager;
   let environment;
   let adbObj;
   let Exec;
@@ -55,10 +40,7 @@ describe('Genymotion-cloud driver', () => {
   let deviceRegistry;
   let deviceCleanupRegistry;
   let deviceAllocator;
-  let InstanceLifecycleService;
   let authServiceObj;
-  let GenyCloudDriver;
-  let uut;
   beforeEach(() => {
     logger = require('../../../../utils/logger');
 
@@ -66,10 +48,10 @@ describe('Genymotion-cloud driver', () => {
     emitter = new Emitter();
 
     environment = require('../../../../utils/environment');
-    environment.getGmsaasPath.mockReturnValue('/path/to/gmsaas');
+    environment.getGmsaasPath.mockReturnValue(MOCK_GMSAAS_PATH);
 
     const { InvocationManager } = jest.genMockFromModule('../../../../invoke');
-    const invocationManager = new InvocationManager();
+    invocationManager = new InvocationManager();
 
     const ADB = require('../exec/ADB');
     adbObj = () => latestInstanceOf(ADB);
@@ -93,308 +75,358 @@ describe('Genymotion-cloud driver', () => {
     const DeviceAllocator = require('./GenyCloudDeviceAllocator');
     deviceAllocator = () => latestInstanceOf(DeviceAllocator);
 
-    InstanceLifecycleService = require('./services/GenyInstanceLifecycleService');
-
     const AuthService = require('./services/GenyAuthService');
     authServiceObj = () => latestInstanceOf(AuthService);
-
-    GenyCloudDriver = require('./GenyCloudDriver');
-    uut = new GenyCloudDriver({
-      invocationManager,
-      emitter,
-      client: {},
-    });
-  });
-
-  it('should return a generic name at pre-init', () => {
-    expect(uut.name).toEqual('Unspecified Genymotion Cloud Emulator');
   });
 
-  it('should initialize the common executable using the path set by the environment', async () => {
-    expect(Exec).toHaveBeenCalledWith('/path/to/gmsaas');
+  let instanceLifecycleService;
+  beforeEach(() => {
+    const InstanceLifecycleService = jest.genMockFromModule('./services/GenyInstanceLifecycleService');
+    instanceLifecycleService = new InstanceLifecycleService();
+    instanceLifecycleService.ctor = jest.fn();
+    jest.mock('./services/GenyInstanceLifecycleService', () => MockInstanceLifecycleService);
   });
 
-  describe('preparation', () => {
-    it('should throw error if not logged-in to gmsaas', async () => {
-      authServiceObj().getLoginEmail.mockResolvedValue(null);
-
-      try {
-        await uut.prepare();
-        fail('Expected an error');
-      } catch (e) {
-        expect(e.constructor.name).toEqual('DetoxRuntimeError');
-        expect(e.toString()).toContain('Cannot run tests using a Genymotion-cloud emulator, because Genymotion was not logged-in to!');
-        expect(e.toString()).toContain(`HINT: Log-in to Genymotion-cloud by running this command (and following instructions):\n/path/to/gmsaas auth login --help`);
-      }
+  describe('instance scope', () => {
+    let GenyCloudDriver;
+    let uut;
+    beforeEach(() => {
+      GenyCloudDriver = require('./GenyCloudDriver');
+      uut = new GenyCloudDriver({
+        invocationManager,
+        emitter,
+        client: {},
+      });
     });
 
-    it('should not throw an error if properly logged in to gmsaas', async () => {
-      authServiceObj().getLoginEmail.mockResolvedValue('detox@wix.com');
-      await uut.prepare();
+    it('should return a generic name at pre-init', () => {
+      expect(uut.name).toEqual('Unspecified Genymotion Cloud Emulator');
     });
-  });
-
-  describe('device (instance) allocation', () => {
-    const givenNoRecipes = () => deviceQueryHelper().getRecipeFromQuery.mockResolvedValue(null);
-    const givenResolvedRecipeForQuery = (recipe) => deviceQueryHelper().getRecipeFromQuery.mockResolvedValue(recipe);
-    const givenDeviceAllocationResult = ({ instance, isNew = false }) => deviceAllocator().allocateDevice.mockResolvedValue({ instance, isNew });
 
-    it('should get a recipe and allocate a device', async () => {
-      const recipe = aRecipe();
-      const instance = anInstance();
-      givenResolvedRecipeForQuery(recipe);
-      givenDeviceAllocationResult({ instance });
+    it('should initialize the common executable using the path set by the environment', async () => {
+      expect(Exec).toHaveBeenCalledWith(MOCK_GMSAAS_PATH);
+    });
 
-      const deviceQuery = aDeviceQuery();
-      const result = await uut.acquireFreeDevice(deviceQuery);
+    describe('preparation', () => {
+      it('should throw error if not logged-in to gmsaas', async () => {
+        authServiceObj().getLoginEmail.mockResolvedValue(null);
+
+        try {
+          await uut.prepare();
+          fail('Expected an error');
+        } catch (e) {
+          expect(e.constructor.name).toEqual('DetoxRuntimeError');
+          expect(e.toString()).toContain('Cannot run tests using a Genymotion-cloud emulator, because Genymotion was not logged-in to!');
+          expect(e.toString()).toContain(`HINT: Log-in to Genymotion-cloud by running this command (and following instructions):\n${MOCK_GMSAAS_PATH} auth login --help`);
+        }
+      });
 
-      expect(result).toEqual({
-        adbName: instance.adbName,
-        uuid: instance.uuid,
+      it('should not throw an error if properly logged in to gmsaas', async () => {
+        authServiceObj().getLoginEmail.mockResolvedValue('detox@wix.com');
+        await uut.prepare();
       });
-      expect(deviceQueryHelper().getRecipeFromQuery).toHaveBeenCalledWith(deviceQuery);
-      expect(deviceAllocator().allocateDevice).toHaveBeenCalledWith(recipe);
     });
 
-    it('should throw a descriptive error recipe not found', async () => {
-      const deviceQuery = aDeviceQuery();
-      givenNoRecipes();
-      givenDeviceAllocationResult({ instance: anInstance() });
-
-      try {
-        await uut.acquireFreeDevice(deviceQuery);
-        fail('Expected an error');
-      } catch (e) {
-        expect(e.toString()).toContain('No Genycloud devices found for recipe!');
-        expect(e.toString()).toContain('HINT: Check that your Genycloud account has a template associated with your Detox device configuration: ' + JSON.stringify(deviceQuery));
-      }
-    });
+    describe('device (instance) allocation', () => {
+      const givenNoRecipes = () => deviceQueryHelper().getRecipeFromQuery.mockResolvedValue(null);
+      const givenResolvedRecipeForQuery = (recipe) => deviceQueryHelper().getRecipeFromQuery.mockResolvedValue(recipe);
+      const givenDeviceAllocationResult = ({ instance, isNew = false }) => deviceAllocator().allocateDevice.mockResolvedValue({ instance, isNew });
+
+      it('should get a recipe and allocate a device', async () => {
+        const recipe = aRecipe();
+        const instance = anInstance();
+        givenResolvedRecipeForQuery(recipe);
+        givenDeviceAllocationResult({ instance });
+
+        const deviceQuery = aDeviceQuery();
+        const result = await uut.acquireFreeDevice(deviceQuery);
+
+        expect(result).toEqual({
+          adbName: instance.adbName,
+          uuid: instance.uuid,
+        });
+        expect(deviceQueryHelper().getRecipeFromQuery).toHaveBeenCalledWith(deviceQuery);
+        expect(deviceAllocator().allocateDevice).toHaveBeenCalledWith(recipe);
+      });
 
-    it('should emit a bootDevice event', async () => {
-      const recipe = aRecipe();
-      const instance = anInstance();
-      givenResolvedRecipeForQuery(recipe);
-      givenDeviceAllocationResult({ instance });
+      it('should throw a descriptive error recipe not found', async () => {
+        const deviceQuery = aDeviceQuery();
+        givenNoRecipes();
+        givenDeviceAllocationResult({ instance: anInstance() });
+
+        try {
+          await uut.acquireFreeDevice(deviceQuery);
+          fail('Expected an error');
+        } catch (e) {
+          expect(e.toString()).toContain('No Genycloud devices found for recipe!');
+          expect(e.toString()).toContain('HINT: Check that your Genycloud account has a template associated with your Detox device configuration: ' + JSON.stringify(deviceQuery));
+        }
+      });
 
-      await uut.acquireFreeDevice(aDeviceQuery());
+      it('should emit a bootDevice event', async () => {
+        const recipe = aRecipe();
+        const instance = anInstance();
+        givenResolvedRecipeForQuery(recipe);
+        givenDeviceAllocationResult({ instance });
 
-      expect(emitter.emit).toHaveBeenCalledWith('bootDevice', { coldBoot: false, deviceId: instance.adbName, type: recipe.name });
-    });
+        await uut.acquireFreeDevice(aDeviceQuery());
 
-    it('should fail if even emission fails', async () => {
-      const recipe = aRecipe();
-      const instance = anInstance();
-      givenResolvedRecipeForQuery(recipe);
-      givenDeviceAllocationResult({ instance });
+        expect(emitter.emit).toHaveBeenCalledWith('bootDevice', { coldBoot: false, deviceId: instance.adbName, type: recipe.name });
+      });
 
-      const error = new Error('mocked error');
-      try {
-        emitter.emit.mockRejectedValue(error);
-        await uut.acquireFreeDevice(aDeviceQuery());
-        fail('Expected an error');
-      } catch (e) {
-        expect(e).toEqual(error);
-      }
-    });
+      it('should fail if even emission fails', async () => {
+        const recipe = aRecipe();
+        const instance = anInstance();
+        givenResolvedRecipeForQuery(recipe);
+        givenDeviceAllocationResult({ instance });
+
+        const error = new Error('mocked error');
+        try {
+          emitter.emit.mockRejectedValue(error);
+          await uut.acquireFreeDevice(aDeviceQuery());
+          fail('Expected an error');
+        } catch (e) {
+          expect(e).toEqual(error);
+        }
+      });
 
-    it('should emit coldBoot=true in bootDevice event', async () => {
-      const recipe = aRecipe();
-      const instance = anInstance();
-      givenResolvedRecipeForQuery(recipe);
-      givenDeviceAllocationResult({ instance, isNew: true });
+      it('should emit coldBoot=true in bootDevice event', async () => {
+        const recipe = aRecipe();
+        const instance = anInstance();
+        givenResolvedRecipeForQuery(recipe);
+        givenDeviceAllocationResult({ instance, isNew: true });
 
-      await uut.acquireFreeDevice(aDeviceQuery());
+        await uut.acquireFreeDevice(aDeviceQuery());
 
-      expect(emitter.emit).toHaveBeenCalledWith('bootDevice', { coldBoot: true, deviceId: instance.adbName, type: recipe.name });
-    });
+        expect(emitter.emit).toHaveBeenCalledWith('bootDevice', { coldBoot: true, deviceId: instance.adbName, type: recipe.name });
+      });
 
-    it('should return a descriptive device name in post-allocation state', async () => {
-      const instance = anInstance();
-      givenResolvedRecipeForQuery(aRecipe());
-      givenDeviceAllocationResult({ instance });
+      it('should return a descriptive device name in post-allocation state', async () => {
+        const instance = anInstance();
+        givenResolvedRecipeForQuery(aRecipe());
+        givenDeviceAllocationResult({ instance });
 
-      await uut.acquireFreeDevice(aDeviceQuery());
+        await uut.acquireFreeDevice(aDeviceQuery());
 
-      expect(uut.name).toEqual(`GenyCloud:${instance.name} (${instance.uuid} ${instance.adbName})`);
-    });
+        expect(uut.name).toEqual(`GenyCloud:${instance.name} (${instance.uuid} ${instance.adbName})`);
+      });
 
-    it('should init ADB\'s API-level', async () => {
-      const instance = anInstance();
-      givenResolvedRecipeForQuery(aRecipe());
-      givenDeviceAllocationResult({ instance });
+      it('should init ADB\'s API-level', async () => {
+        const instance = anInstance();
+        givenResolvedRecipeForQuery(aRecipe());
+        givenDeviceAllocationResult({ instance });
 
-      await uut.acquireFreeDevice(aDeviceQuery());
+        await uut.acquireFreeDevice(aDeviceQuery());
 
-      expect(adbObj().apiLevel).toHaveBeenCalledWith(instance.adbName);
-    });
+        expect(adbObj().apiLevel).toHaveBeenCalledWith(instance.adbName);
+      });
 
-    it('should disable native animations', async () => {
-      const instance = anInstance();
-      givenResolvedRecipeForQuery(aRecipe());
-      givenDeviceAllocationResult({ instance });
+      it('should disable native animations', async () => {
+        const instance = anInstance();
+        givenResolvedRecipeForQuery(aRecipe());
+        givenDeviceAllocationResult({ instance });
 
-      await uut.acquireFreeDevice(aDeviceQuery());
+        await uut.acquireFreeDevice(aDeviceQuery());
 
-      expect(adbObj().disableAndroidAnimations).toHaveBeenCalledWith(instance.adbName);
+        expect(adbObj().disableAndroidAnimations).toHaveBeenCalledWith(instance.adbName);
+      });
     });
-  });
 
-  describe('app installation', () => {
-    const appInstallHelperObj = () => latestInstanceOf(AppInstallHelperClass);
+    describe('app installation', () => {
+      const appInstallHelperObj = () => latestInstanceOf(AppInstallHelperClass);
 
-    let AppInstallHelperClass;
-    let getAbsoluteBinaryPath;
-    beforeEach(() => {
-      AppInstallHelperClass = require('../tools/AppInstallHelper');
-      getAbsoluteBinaryPath = require('../../../../utils/getAbsoluteBinaryPath');
-    });
+      let AppInstallHelperClass;
+      let getAbsoluteBinaryPath;
+      beforeEach(() => {
+        AppInstallHelperClass = require('../tools/AppInstallHelper');
+        getAbsoluteBinaryPath = require('../../../../utils/getAbsoluteBinaryPath');
+      });
 
-    it('should install using install helper', async () => {
-      getAbsoluteBinaryPath
-        .mockReturnValueOnce('bin-install-path')
-        .mockReturnValueOnce('testbin-install-path');
+      it('should install using install helper', async () => {
+        getAbsoluteBinaryPath
+          .mockReturnValueOnce('bin-install-path')
+          .mockReturnValueOnce('testbin-install-path');
 
-      const deviceId = anInstance();
-      await uut.installApp(deviceId, 'bin-path', 'testbin-path');
-      expect(appInstallHelperObj().install).toHaveBeenCalledWith(deviceId.adbName, 'bin-install-path', 'testbin-install-path');
+        const deviceId = anInstance();
+        await uut.installApp(deviceId, 'bin-path', 'testbin-path');
+        expect(appInstallHelperObj().install).toHaveBeenCalledWith(deviceId.adbName, 'bin-install-path', 'testbin-install-path');
+      });
     });
-  });
 
-  describe('clean-up', () => {
-    const instrumentationObj = () => latestInstanceOf(Instrumentation);
+    describe('clean-up', () => {
+      const instrumentationObj = () => latestInstanceOf(Instrumentation);
 
-    let Instrumentation;
-    beforeEach(() => {
-      Instrumentation = require('../tools/MonitoredInstrumentation');
-    });
+      let Instrumentation;
+      beforeEach(() => {
+        Instrumentation = require('../tools/MonitoredInstrumentation');
+      });
 
-    it('should dispose an instance based on its UUID', async () => {
-      const instance = anInstance();
-      await uut.cleanup(instance, 'bundle-id');
-      expect(deviceRegistry.disposeDevice).toHaveBeenCalledWith(instance.uuid);
-    });
+      it('should dispose an instance based on its UUID', async () => {
+        const instance = anInstance();
+        await uut.cleanup(instance, 'bundle-id');
+        expect(deviceRegistry.disposeDevice).toHaveBeenCalledWith(instance.uuid);
+      });
 
-    it('should kill instrumentation', async () => {
-      await uut.cleanup(anInstance(), 'bundle-id');
-      expect(instrumentationObj().terminate).toHaveBeenCalled();
+      it('should kill instrumentation', async () => {
+        await uut.cleanup(anInstance(), 'bundle-id');
+        expect(instrumentationObj().terminate).toHaveBeenCalled();
+      });
     });
-  });
 
-  describe('shutdown', () => {
-    const instanceLifecycleServiceObj = () => latestInstanceOf(InstanceLifecycleService);
+    describe('shutdown', () => {
+      const givenAnInstanceDeletionError = () => instanceLifecycleService.deleteInstance.mockRejectedValue(new Error());
 
-    const givenAnInstanceDeletionError = () => instanceLifecycleServiceObj().deleteInstance.mockRejectedValue(new Error());
+      it('should delete the instance', async () => {
+        const instance = anInstance();
+        await uut.shutdown(instance);
+        expect(instanceLifecycleService.deleteInstance).toHaveBeenCalledWith(instance.uuid);
+      });
 
-    it('should delete the instance', async () => {
-      const instance = anInstance();
-      await uut.shutdown(instance);
-      expect(instanceLifecycleServiceObj().deleteInstance).toHaveBeenCalledWith(instance.uuid);
-    });
+      it('should throw if deletion fails', async () => {
+        givenAnInstanceDeletionError();
 
-    it('should throw if deletion fails', async () => {
-      givenAnInstanceDeletionError();
+        try {
+          await uut.shutdown(anInstance());
+          fail('Expected an error');
+        } catch (e) {}
+      });
 
-      try {
-        await uut.shutdown(anInstance());
-        fail('Expected an error');
-      } catch (e) {}
-    });
+      it('should emit associated events', async () => {
+        const instance = anInstance();
+        await uut.shutdown(instance);
+        expect(emitter.emit).toHaveBeenCalledWith('beforeShutdownDevice', { deviceId: instance.adbName });
+        expect(emitter.emit).toHaveBeenCalledWith('shutdownDevice', { deviceId: instance.adbName });
+      });
 
-    it('should emit associated events', async () => {
-      const instance = anInstance();
-      await uut.shutdown(instance);
-      expect(emitter.emit).toHaveBeenCalledWith('beforeShutdownDevice', { deviceId: instance.adbName });
-      expect(emitter.emit).toHaveBeenCalledWith('shutdownDevice', { deviceId: instance.adbName });
-    });
+      it('should not emit shutdown even in case of failure', async () => {
+        givenAnInstanceDeletionError();
 
-    it('should not emit shutdown even in case of failure', async () => {
-      givenAnInstanceDeletionError();
+        try {
+          await uut.shutdown(anInstance());
+          fail('Expected an error');
+        } catch (e) {
+          expect(emitter.emit).toHaveBeenCalledWith('beforeShutdownDevice', expect.anything());
+          expect(emitter.emit).not.toHaveBeenCalledWith('shutdownDevice', expect.anything());
+        }
+      });
 
-      try {
-        await uut.shutdown(anInstance());
-        fail('Expected an error');
-      } catch (e) {
-        expect(emitter.emit).toHaveBeenCalledWith('beforeShutdownDevice', expect.anything());
-        expect(emitter.emit).not.toHaveBeenCalledWith('shutdownDevice', expect.anything());
-      }
+      it('should remove instance from device-registry', async () => {
+        const instance = anInstance();
+        await uut.shutdown(instance);
+        expect(deviceCleanupRegistry.disposeDevice).toHaveBeenCalledWith(instance.uuid);
+      });
     });
 
-    it('should remove instance from device-registry', async () => {
-      const instance = anInstance();
-      await uut.shutdown(instance);
-      expect(deviceCleanupRegistry.disposeDevice).toHaveBeenCalledWith(instance.uuid);
-    });
   });
 
-  describe('global (static) clean-up', () => {
-    const anInstanceLifecycleService = () => {
-      const InstanceLifecycleService = require('./services/GenyInstanceLifecycleService');
-      return new InstanceLifecycleService();
-    }
-
-    const assertablePendingPromise = () => {
-      let promiseAck = jest.fn();
-      const promise = new Promise(resolve => setTimeout(resolve, 1)).then(promiseAck);
-      promise.assertResolved = () => expect(promiseAck).toHaveBeenCalled();
-      return promise;
-    };
-
-    it('should kill all devices preregistered for clean-up', async () => {
-      const killPromise1 = assertablePendingPromise();
-      const killPromise2 = assertablePendingPromise();
-
-      const instanceLifecycleService = anInstanceLifecycleService();
-      instanceLifecycleService.deleteInstance
-        .mockReturnValueOnce(killPromise1)
-        .mockReturnValueOnce(killPromise2);
+  describe('class scope', () => {
+    let GenyCloudDriver;
+    beforeEach(() => {
+      GenyCloudDriver = require('./GenyCloudDriver');
+    });
 
-      const deviceUUIDs = ['device1-uuid', 'device2-uuid'];
-      deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue(deviceUUIDs);
+    describe('global clean-up', () => {
+      const givenDeletionPendingDevices = (deviceUUIDs) => deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue(deviceUUIDs);
+      const givenNoDeletionPendingDevices = () => givenDeletionPendingDevices([]);
+      const givenDeletionPendingInstances = (instances) => givenDeletionPendingDevices(_.map(instances, 'uuid'));
+      const givenDeletionResult = (deletedInstance) => instanceLifecycleService.deleteInstance.mockResolvedValue(deletedInstance);
+
+      const assertablePendingPromise = () => {
+        let promiseAck = jest.fn();
+        const promise = new Promise(resolve => setTimeout(resolve, 1)).then(promiseAck);
+        promise.assertResolved = () => expect(promiseAck).toHaveBeenCalled();
+        return promise;
+      };
+
+      it('should kill all deletion-pending device', async () => {
+        const killPromise1 = assertablePendingPromise();
+        const killPromise2 = assertablePendingPromise();
+        instanceLifecycleService.deleteInstance
+          .mockReturnValueOnce(killPromise1)
+          .mockReturnValueOnce(killPromise2);
+
+        const deviceUUIDs = ['device1-uuid', 'device2-uuid'];
+        givenDeletionPendingDevices(deviceUUIDs);
+
+        await GenyCloudDriver.globalCleanup();
+
+        killPromise1.assertResolved();
+        killPromise2.assertResolved();
+        expect(instanceLifecycleService.deleteInstance).toHaveBeenCalledWith(deviceUUIDs[0]);
+        expect(instanceLifecycleService.deleteInstance).toHaveBeenCalledWith(deviceUUIDs[1]);
+      });
 
-      await GenyCloudDriver.globalCleanup(instanceLifecycleService);
+      it('should warn of instances deletion rejects', async () => {
+        instanceLifecycleService.deleteInstance
+          .mockRejectedValueOnce(new Error('mock-error1'))
+          .mockResolvedValueOnce(anInstance())
+          .mockRejectedValueOnce(new Error('mock-error2'));
 
-      killPromise1.assertResolved();
-      killPromise2.assertResolved();
-      expect(instanceLifecycleService.deleteInstance).toHaveBeenCalledWith(deviceUUIDs[0]);
-      expect(instanceLifecycleService.deleteInstance).toHaveBeenCalledWith(deviceUUIDs[1]);
-    });
+        givenDeletionPendingDevices(['failing-uuid1', 'nonfailing-uuid', 'failing-uuid2']);
 
-    it('should fallback to a default lifecycle-service', async () => {
-      Exec.mockReset();
+        await GenyCloudDriver.globalCleanup();
 
-      deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue([]);
-      await GenyCloudDriver.globalCleanup();
+        expect(logger.warn).toHaveBeenCalledWith({ event: 'GENYCLOUD_TEARDOWN' }, 'WARNING! Detected a Genymotion cloud instance leakage, for the following instances:');
+        expect(logger.warn).toHaveBeenCalledWith({ event: 'GENYCLOUD_TEARDOWN' }, expect.stringMatching(/failing-uuid1:.*mock-error1/));
+        expect(logger.warn).toHaveBeenCalledWith({ event: 'GENYCLOUD_TEARDOWN' }, expect.stringMatching(/failing-uuid2:.*mock-error2/));
+        expect(logger.warn).toHaveBeenCalledTimes(3);
+      });
 
-      expect(Exec).toHaveBeenCalledWith('/path/to/gmsaas');
-    });
+      it('should not warn of deletion rejects if all went well', async () => {
+        const instance = anInstance();
+        givenDeletionPendingInstances([instance]);
+        givenDeletionResult(instance);
 
-    it('should warn of instances cleanup rejects', async () => {
-      const instanceLifecycleService = anInstanceLifecycleService();
-      instanceLifecycleService.deleteInstance
-        .mockRejectedValueOnce(new Error('mock-error1'))
-        .mockResolvedValueOnce(anInstance())
-        .mockRejectedValueOnce(new Error('mock-error2'));
+        await GenyCloudDriver.globalCleanup();
 
-      const deviceUUIDs = ['failing-uuid1', 'nonfailing-uuid', 'failing-uuid2'];
-      deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue(deviceUUIDs);
+        expect(logger.warn).not.toHaveBeenCalled();
+      });
 
-      await GenyCloudDriver.globalCleanup(instanceLifecycleService);
+      it('should properly init the instances service and the delegated exec', async () => {
+        const instance = anInstance();
+        givenDeletionPendingInstances([instance]);
+        givenDeletionResult(instance);
 
-      expect(logger.warn).toHaveBeenCalledWith({ event: 'GENYCLOUD_TEARDOWN' }, 'WARNING! Detected a Genymotion cloud instance leakage, for the following instances:');
-      expect(logger.warn).toHaveBeenCalledWith({ event: 'GENYCLOUD_TEARDOWN' }, expect.stringMatching(/failing-uuid1:.*mock-error1/));
-      expect(logger.warn).toHaveBeenCalledWith({ event: 'GENYCLOUD_TEARDOWN' }, expect.stringMatching(/failing-uuid2:.*mock-error2/));
-    });
+        await GenyCloudDriver.globalCleanup();
 
-    it('should not warn of cleanup rejects if all went well', async () => {
-      const instanceLifecycleService = anInstanceLifecycleService();
-      instanceLifecycleService.deleteInstance.mockResolvedValue(anInstance());
+        expect(instanceLifecycleService.ctor).toHaveBeenCalledWith(latestInstanceOf(Exec), null);
+        expect(Exec).toHaveBeenCalledWith(MOCK_GMSAAS_PATH);
+      });
 
-      const deviceUUIDs = ['device-uuid1'];
-      deviceCleanupRegistry.readRegisteredDevices.mockResolvedValue(deviceUUIDs);
+      it('should not init the instances service and its delegates if there are no deletion-pending devices', async () => {
+        givenNoDeletionPendingDevices();
 
-      await GenyCloudDriver.globalCleanup(instanceLifecycleService);
+        await GenyCloudDriver.globalCleanup();
 
-      expect(logger.warn).not.toHaveBeenCalled();
+        expect(Exec).not.toHaveBeenCalled();
+        expect(environment.getGmsaasPath).not.toHaveBeenCalled();
+        expect(instanceLifecycleService.ctor).not.toHaveBeenCalled();
+      });
     });
   });
+
 });
+
+function mockBaseClassesDependencies() {
+  jest.mock('../exec/ADB');
+  jest.mock('../exec/AAPT');
+  jest.mock('../tools/APKPath');
+  jest.mock('../tools/TempFileXfer');
+  jest.mock('../tools/AppInstallHelper');
+  jest.mock('../tools/AppUninstallHelper');
+  jest.mock('../tools/MonitoredInstrumentation');
+  jest.mock('../../../../artifacts/utils/AndroidDevicePathBuilder');
+  jest.mock('../../../../android/espressoapi/UiDeviceProxy');
+  jest.mock('../../../../utils/logger');
+}
+
+function mockDirectDependencies() {
+  jest.mock('../../../../utils/environment');
+  jest.mock('./exec/GenyCloudExec');
+  jest.mock('./GenyDeviceRegistryFactory');
+  jest.mock('./services/GenyRecipesService');
+  jest.mock('./services/GenyInstanceLookupService');
+  jest.mock('./services/GenyAuthService');
+  jest.mock('./services/GenyInstanceNaming');
+  jest.mock('../../../../utils/getAbsoluteBinaryPath');
+};
diff --git a/detox/test/e2e/global-teardown.js b/detox/test/e2e/global-teardown.js
index 6aab43663..c2625cf2f 100644
--- a/detox/test/e2e/global-teardown.js
+++ b/detox/test/e2e/global-teardown.js
@@ -1,7 +1,7 @@
 const detox = require('detox');
 
 async function globalTeardown() {
-  // await detox.globalCleanup();
+  await detox.globalCleanup();
 }
 
 module.exports = globalTeardown;