Skip to content

Commit

Permalink
Starting using 'gmsaas doctor' command (#4676)
Browse files Browse the repository at this point in the history
  • Loading branch information
d4vidi authored Dec 29, 2024
1 parent 89a3f0e commit 2b4df68
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,18 @@ 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';
}

getVersion() {
return this._exec('--version');
}

doctor() {
return this._exec('doctor', { retries: 0 }, 'text');
}

getRecipe(name) {
return this._exec(`recipes list --name "${name}"`);
}
Expand Down Expand Up @@ -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;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
});
});
});

Expand Down
23 changes: 20 additions & 3 deletions detox/src/devices/validation/android/GenycloudEnvValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
/**
Expand All @@ -18,18 +19,34 @@ 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})`,
hint: `Detox requires version 1.6.0, or newer. To use 'android.genycloud' type devices, you must upgrade it, first.`,
});
}
}

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;
88 changes: 59 additions & 29 deletions detox/src/devices/validation/android/GenycloudEnvValidator.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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.]
`;

0 comments on commit 2b4df68

Please sign in to comment.