From 2b4df68ed3b1a1cb28f54a1bb0377fe0bbe42ca0 Mon Sep 17 00:00:00 2001 From: d4vidi Date: Sun, 29 Dec 2024 19:23:19 +0200 Subject: [PATCH] Starting using 'gmsaas doctor' command (#4676) --- .../android/genycloud/exec/GenyCloudExec.js | 22 +++-- .../genycloud/exec/GenyCloudExec.test.js | 93 ++++++++++++------- .../android/GenycloudEnvValidator.js | 23 ++++- .../android/GenycloudEnvValidator.test.js | 88 ++++++++++++------ .../GenycloudEnvValidator.test.js.snap | 13 +++ 5 files changed, 165 insertions(+), 74 deletions(-) create mode 100644 detox/src/devices/validation/android/__snapshots__/GenycloudEnvValidator.test.js.snap diff --git a/detox/src/devices/allocation/drivers/android/genycloud/exec/GenyCloudExec.js b/detox/src/devices/allocation/drivers/android/genycloud/exec/GenyCloudExec.js index cd188a6cdb..85f9895586 100644 --- a/detox/src/devices/allocation/drivers/android/genycloud/exec/GenyCloudExec.js +++ b/detox/src/devices/allocation/drivers/android/genycloud/exec/GenyCloudExec.js @@ -3,7 +3,7 @@ const exec = require('../../../../../../utils/childProcess').execWithRetriesAndL class GenyCloudExec { constructor(binaryPath) { - this.binaryExec = `"${binaryPath}" --format compactjson`; + this.binaryExec = binaryPath; process.env.GMSAAS_USER_AGENT_EXTRA_DATA = process.env.GMSAAS_USER_AGENT_EXTRA_DATA || 'detox'; } @@ -11,6 +11,10 @@ class GenyCloudExec { return this._exec('--version'); } + doctor() { + return this._exec('doctor', { retries: 0 }, 'text'); + } + getRecipe(name) { return this._exec(`recipes list --name "${name}"`); } @@ -38,23 +42,25 @@ class GenyCloudExec { return this._exec(`instances stop ${instanceUUID}`, options); } - async _exec(args, options) { + async _exec(args, options, format = 'compactjson') { try { - const rawResult = await this.__exec(args, options); - return JSON.parse(rawResult); + const rawResult = await this.__exec(args, options, format); + return ( + format === 'compactjson' ? JSON.parse(rawResult) : rawResult + ); } catch (error) { throw new Error(error.stderr); } } - async __exec(args, _options) { - const options = { - ..._options, + async __exec(args, options, format) { + const _options = { + ...options, statusLogs: { retrying: true, }, }; - return (await exec(`${this.binaryExec} ${args}`, options )).stdout; + return (await exec(`"${this.binaryExec}" --format ${format} ${args}`, _options )).stdout; } } diff --git a/detox/src/devices/allocation/drivers/android/genycloud/exec/GenyCloudExec.test.js b/detox/src/devices/allocation/drivers/android/genycloud/exec/GenyCloudExec.test.js index 6d5a3cce15..e10e0593b3 100644 --- a/detox/src/devices/allocation/drivers/android/genycloud/exec/GenyCloudExec.test.js +++ b/detox/src/devices/allocation/drivers/android/genycloud/exec/GenyCloudExec.test.js @@ -19,12 +19,18 @@ describe('Genymotion-cloud executable', () => { const instanceUUID = 'mock-uuid'; const instanceName = 'detox-instance1'; - const givenSuccessResult = () => exec.mockResolvedValue({ + const givenSuccessJSONResult = () => exec.mockResolvedValue({ stdout: JSON.stringify(successResponse), }); - const givenErrorResult = () => exec.mockRejectedValue({ + const givenSuccessTextualResult = () => exec.mockResolvedValue({ + stdout: successResponse, + }); + const givenErrorJSONResult = () => exec.mockRejectedValue({ stderr: JSON.stringify(failResponse), }); + const givenErrorTextualResult = (errorMessage) => exec.mockRejectedValue({ + stderr: errorMessage, + }); let exec; let uut; @@ -40,43 +46,62 @@ describe('Genymotion-cloud executable', () => { delete process.env.GMSAAS_USER_AGENT_EXTRA_DATA; }); - describe.each([ - ['version', () => uut.getVersion(), `"mock/path/to/gmsaas" --format compactjson --version`], - ['Get Recipe', () => uut.getRecipe(recipeName), `"mock/path/to/gmsaas" --format compactjson recipes list --name "${recipeName}"`], - ['Get Instance', () => uut.getInstance(instanceUUID), `"mock/path/to/gmsaas" --format compactjson instances get ${instanceUUID}`], - ['Get Instances', () => uut.getInstances(), `"mock/path/to/gmsaas" --format compactjson instances list -q`], - ['Start Instance', () => uut.startInstance(recipeUUID, instanceName), `"mock/path/to/gmsaas" --format compactjson instances start --no-wait ${recipeUUID} "${instanceName}"`, { retries: 0 }], - ['ADB Connect', () => uut.adbConnect(instanceUUID), `"mock/path/to/gmsaas" --format compactjson instances adbconnect ${instanceUUID}`, { retries: 0 }], - ['Stop Instance', () => uut.stopInstance(instanceUUID), `"mock/path/to/gmsaas" --format compactjson instances stop ${instanceUUID}`, { retries: 3 }], - ])(`%s command`, (commandName, commandExecFn, expectedExec, expectedExecOptions) => { - it('should execute command by name', async () => { - givenSuccessResult(); - - const expectedOptions = { - ...expectedExecOptions, - statusLogs: { - retrying: true, - } - }; - - await commandExecFn(); - expect(exec).toHaveBeenCalledWith( - expectedExec, - expectedOptions, - ); + describe('JSON command', () => { + describe.each([ + ['version', () => uut.getVersion(), `"mock/path/to/gmsaas" --format compactjson --version`], + ['Get Recipe', () => uut.getRecipe(recipeName), `"mock/path/to/gmsaas" --format compactjson recipes list --name "${recipeName}"`], + ['Get Instance', () => uut.getInstance(instanceUUID), `"mock/path/to/gmsaas" --format compactjson instances get ${instanceUUID}`], + ['Get Instances', () => uut.getInstances(), `"mock/path/to/gmsaas" --format compactjson instances list -q`], + ['Start Instance', () => uut.startInstance(recipeUUID, instanceName), `"mock/path/to/gmsaas" --format compactjson instances start --no-wait ${recipeUUID} "${instanceName}"`, { retries: 0 }], + ['ADB Connect', () => uut.adbConnect(instanceUUID), `"mock/path/to/gmsaas" --format compactjson instances adbconnect ${instanceUUID}`, { retries: 0 }], + ['Stop Instance', () => uut.stopInstance(instanceUUID), `"mock/path/to/gmsaas" --format compactjson instances stop ${instanceUUID}`, { retries: 3 }], + ])(`%s`, (commandName, commandExecFn, expectedExec, expectedExecOptions) => { + it('should execute command by name', async () => { + givenSuccessJSONResult(); + + await commandExecFn(); + expect(exec).toHaveBeenCalledWith(expectedExec, expect.objectContaining(expectedExecOptions || {})); + }); + + it('should return the result', async () => { + givenSuccessJSONResult(); + + const result = await commandExecFn(); + expect(result).toEqual(successResponse); + }); + + it('should fail upon an error result', async () => { + givenErrorJSONResult(); + + await expect(commandExecFn()).rejects.toThrowError(JSON.stringify(failResponse)); + }); }); + }); - it('should return the result', async () => { - givenSuccessResult(); + describe('Textual command', () => { + describe.each([ + ['Doctor', () => uut.doctor(), `"mock/path/to/gmsaas" --format text doctor`, { retries: 0 }], + ])(`%s`, (commandName, commandExecFn, expectedExec, expectedExecOptions) => { + it('should execute command by name', async () => { + givenSuccessTextualResult(); - const result = await commandExecFn(); - expect(result).toEqual(successResponse); - }); + await commandExecFn(); + expect(exec).toHaveBeenCalledWith(expectedExec, expect.objectContaining(expectedExecOptions || {})); + }); + + it('should return the result', async () => { + givenSuccessTextualResult(); + + const result = await commandExecFn(); + expect(result).toEqual(successResponse); + }); - it('should fail upon an error result', async () => { - givenErrorResult(); + it('should fail upon an error result', async () => { + const errorMessage = 'Oh no, mocked error has occurred!'; + givenErrorTextualResult(errorMessage); - await expect(commandExecFn()).rejects.toThrowError(JSON.stringify(failResponse)); + await expect(commandExecFn()).rejects.toThrowError(errorMessage); + }); }); }); diff --git a/detox/src/devices/validation/android/GenycloudEnvValidator.js b/detox/src/devices/validation/android/GenycloudEnvValidator.js index 222d9d6f7a..2c705fb35f 100644 --- a/detox/src/devices/validation/android/GenycloudEnvValidator.js +++ b/detox/src/devices/validation/android/GenycloudEnvValidator.js @@ -6,6 +6,7 @@ const environment = require('../../../utils/environment'); const EnvironmentValidatorBase = require('../EnvironmentValidatorBase'); const MIN_GMSAAS_VERSION = '1.6.0'; +const MIN_GMSAAS_VERSION_WITH_DOCTOR = '1.11.0'; class GenycloudEnvValidator extends EnvironmentValidatorBase { /** @@ -18,11 +19,13 @@ class GenycloudEnvValidator extends EnvironmentValidatorBase { } async validate() { - await this._validateGmsaasVersion(); + const { version } = await this._exec.getVersion(); + + await this._validateGmsaasVersion(version); + await this._validateGmsaasDoctorCheck(version); } - async _validateGmsaasVersion() { - const { version } = await this._exec.getVersion(); + async _validateGmsaasVersion(version) { if (semver.lt(version, MIN_GMSAAS_VERSION)) { throw new DetoxRuntimeError({ message: `Your Genymotion-Cloud executable (found in ${environment.getGmsaasPath()}) is too old! (version ${version})`, @@ -30,6 +33,20 @@ class GenycloudEnvValidator extends EnvironmentValidatorBase { }); } } + + async _validateGmsaasDoctorCheck(version) { + if (semver.lt(version, MIN_GMSAAS_VERSION_WITH_DOCTOR)) { + return; + } + + try { + await this._exec.doctor(); + } catch (e) { + throw new DetoxRuntimeError({ + message: e.message, + }); + } + } } module.exports = GenycloudEnvValidator; diff --git a/detox/src/devices/validation/android/GenycloudEnvValidator.test.js b/detox/src/devices/validation/android/GenycloudEnvValidator.test.js index 3bda2d8076..b5c54c8335 100644 --- a/detox/src/devices/validation/android/GenycloudEnvValidator.test.js +++ b/detox/src/devices/validation/android/GenycloudEnvValidator.test.js @@ -18,43 +18,73 @@ describe('Genymotion-cloud test environment validator', () => { uut = new GenycloudEnvValidator({ exec }); }); - const givenGmsaasExecVersion = (version) => exec.getVersion.mockResolvedValue({ version }); - const givenProperGmsaasExecVersion = () => givenGmsaasExecVersion('1.6.0'); + const givenGmsaasVersion = (version) => exec.getVersion.mockResolvedValue({ version }); + const givenMinimalGmsaasVersion = () => givenGmsaasVersion('1.6.0'); + const givenFirstGmsaasVersionWithDoctor = () => givenGmsaasVersion('1.11.0'); + const givenLastGmsaasVersionWithoutDoctor = () => givenGmsaasVersion('1.10.0'); + const givenValidDoctorCheck = () => exec.doctor.mockResolvedValue({ exit_code: 0 }); + const givenFailedDoctorChecks = () => exec.doctor.mockRejectedValue(new Error( + 'Error: gmsaas is not configured properly' + + '\nOne or several issues have been detected:' + + '\n- Android SDK not configured.')); - it('should throw an error if gmsaas exec is too old (minor version < 6)', async () => { - givenGmsaasExecVersion('1.5.9'); + describe('version validations', () => { + beforeEach(() => { + givenValidDoctorCheck(); + }); - try { + it('should throw an error if gmsaas exec is too old (minor version < 6)', async () => { + givenGmsaasVersion('1.5.9'); + + try { + await uut.validate(); + } catch (e) { + expect(e.constructor.name).toEqual('DetoxRuntimeError'); + expect(e).toMatchSnapshot(); + return; + } + throw new Error('Expected an error'); + }); + + it('should accept the gmsaas exec if version is sufficiently new', async () => { + givenMinimalGmsaasVersion(); await uut.validate(); - } catch (e) { - expect(e.constructor.name).toEqual('DetoxRuntimeError'); - expect(e.toString()).toContain(`Your Genymotion-Cloud executable (found in ${MOCK_GMSAAS_PATH}) is too old! (version 1.5.9)`); - expect(e.toString()).toContain(`HINT: Detox requires version 1.6.0, or newer. To use 'android.genycloud' type devices, you must upgrade it, first.`); - return; - } - throw new Error('Expected an error'); - }); + }); - it('should accept the gmsaas exec if version is sufficiently new', async () => { - givenGmsaasExecVersion('1.6.0'); - await uut.validate(); - }); + it('should accept the gmsaas exec if version is more than sufficiently new', async () => { + givenGmsaasVersion('1.7.2'); + await uut.validate(); + }); + + it('should throw an error if gmsaas exec is too old (major version < 1)', async () => { + givenGmsaasVersion('0.6.0'); - it('should accept the gmsaas exec if version is more than sufficiently new', async () => { - givenGmsaasExecVersion('1.7.2'); - await uut.validate(); + await expect(uut.validate()) + .rejects + .toThrowError(`Your Genymotion-Cloud executable (found in ${MOCK_GMSAAS_PATH}) is too old! (version 0.6.0)`); + }); }); - it('should throw an error if gmsaas exec is too old (major version < 1)', async () => { - givenGmsaasExecVersion('0.6.0'); + describe('health validations', () => { + it('should throw if gmsaas doctor detects an error', async () => { + givenFirstGmsaasVersionWithDoctor(); + givenFailedDoctorChecks(); - await expect(uut.validate()) - .rejects - .toThrowError(`Your Genymotion-Cloud executable (found in ${MOCK_GMSAAS_PATH}) is too old! (version 0.6.0)`); - }); + await expect(uut.validate()).rejects.toMatchSnapshot(); + }); + + it('should pass if gmsaas doctor checks pass', async () => { + givenFirstGmsaasVersionWithDoctor(); + givenValidDoctorCheck(); + + await uut.validate(); + }); + + it('should not run doctor checks if gmsaas version is too old', async () => { + givenLastGmsaasVersionWithoutDoctor(); + givenFailedDoctorChecks(); - it('should not throw an error if properly logged in to gmsaas', async () => { - givenProperGmsaasExecVersion(); - await uut.validate(); + await uut.validate(); + }); }); }); diff --git a/detox/src/devices/validation/android/__snapshots__/GenycloudEnvValidator.test.js.snap b/detox/src/devices/validation/android/__snapshots__/GenycloudEnvValidator.test.js.snap new file mode 100644 index 0000000000..b2ea8a05f2 --- /dev/null +++ b/detox/src/devices/validation/android/__snapshots__/GenycloudEnvValidator.test.js.snap @@ -0,0 +1,13 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Genymotion-cloud test environment validator health validations should throw if gmsaas doctor detects an error 1`] = ` +[DetoxRuntimeError: Error: gmsaas is not configured properly +One or several issues have been detected: +- Android SDK not configured.] +`; + +exports[`Genymotion-cloud test environment validator version validations should throw an error if gmsaas exec is too old (minor version < 6) 1`] = ` +[DetoxRuntimeError: Your Genymotion-Cloud executable (found in /path/to/gmsaas) is too old! (version 1.5.9) + +HINT: Detox requires version 1.6.0, or newer. To use 'android.genycloud' type devices, you must upgrade it, first.] +`;