Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Start using 'gmsaas doctor' command #4676

Merged
merged 3 commits into from
Dec 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
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.]
`;
Loading