Skip to content

Commit

Permalink
Genycloud debug: ADB devices dump before each adbconnect retry (#4529)
Browse files Browse the repository at this point in the history
  • Loading branch information
d4vidi authored Jul 16, 2024
1 parent cc03547 commit 8d6bc20
Show file tree
Hide file tree
Showing 7 changed files with 105 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ class GenyCloudExec {
}

adbConnect(instanceUUID) {
return this._exec(`instances adbconnect ${instanceUUID}`);
return this._exec(`instances adbconnect ${instanceUUID}`, { retries: 0 });
}

stopInstance(instanceUUID) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ describe('Genymotion-cloud executable', () => {
['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}`],
['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) => {
])(`%s command`, (commandName, commandExecFn, expectedExec, expectedExecOptions) => {
it('should execute command by name', async () => {
givenSuccessResult();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
const log = require('../../../../../../utils/logger').child({ cat: 'device' });
const retry = require('../../../../../../utils/retry');

const Instance = require('./dto/GenyInstance');

class GenyInstanceLifecycleService {
constructor(genyCloudExec) {
/**
* @param { import('../exec/GenyCloudExec') } genyCloudExec
* @param { import('../../../../../common/drivers/android/exec/ADB') } adb
*/
constructor(genyCloudExec, adb) {
this._genyCloudExec = genyCloudExec;
this._adb = adb;
}

async createInstance(recipeUUID, instanceName) {
const result = await this._genyCloudExec.startInstance(recipeUUID, instanceName);
return new Instance(result.instance);
}

async adbConnectInstance(instanceUUID) {
const result = (await this._genyCloudExec.adbConnect(instanceUUID));
async adbConnectInstance(instanceUUID){
const doAdbConnect = async () =>
this._genyCloudExec.adbConnect(instanceUUID);
const beforeEachRetry = async () => {
try {
const { stdout } = await this._adb.devices({ retries: 0, verbosity: 'low' });
log.warn('adb-connect command failed, current ADB devices list:\n', stdout);
} catch (e) {
log.warn('adb-connect command failed; couldn\'t get the list of current devices (see error)', e);
}
return true;
};
const options = {
conditionFn: beforeEachRetry,
retries: 2,
};

const result = await retry(options, doAdbConnect);
return new Instance(result.instance);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,37 @@ describe('Genymotion-Cloud instance-lifecycle service', () => {
}
});

const adbDevicesOutput = [
'List of devices attached',
'localhost:12345\tdevice',
].join('\n');

let log;
/** @type { jest.Mocked<*> } */
let retry;
let adb;
let exec;
let uut;

beforeEach(() => {
jest.mock('../../../../../../utils/logger');
log = require('../../../../../../utils/logger');

jest.mock('../../../../../../utils/retry'/*, () => (_options, func) => func()*/);
retry = require('../../../../../../utils/retry');
retry.mockImplementation((_options, func) => func());

const ADB = jest.genMockFromModule('../../../../../common/drivers/android/exec/ADB');
adb = new ADB();
adb.devices.mockResolvedValue({
stdout: adbDevicesOutput,
});

const GenyCloudExec = jest.genMockFromModule('../exec/GenyCloudExec');
exec = new GenyCloudExec();

const GenyInstanceLifecycleService = require('./GenyInstanceLifecycleService');
uut = new GenyInstanceLifecycleService(exec);
uut = new GenyInstanceLifecycleService(exec, adb);
});

describe('device instance creation', () => {
Expand Down Expand Up @@ -62,6 +84,37 @@ describe('Genymotion-Cloud instance-lifecycle service', () => {
expect(result.uuid).toEqual(instance.uuid);
expect(result.constructor.name).toContain('Instance');
});

it('should wrap the command with a retry', async () => {
const instance = anInstance();
givenAdbConnectResult(instance);
givenRetryOnce();

await uut.adbConnectInstance(instance.uuid);
expect(retry).toHaveBeenCalledWith(expect.objectContaining({ retries: 2 }), expect.any(Function));
expect(exec.adbConnect).toHaveBeenCalledTimes(2);
expect(exec.adbConnect).toHaveBeenLastCalledWith(instance.uuid);
});

it('should log an adb-devices dump on retry', async () => {
const instance = anInstance();
givenAdbConnectResult(instance);
givenRetryOnce();

await uut.adbConnectInstance(instance.uuid);
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('adb-connect command failed'), adbDevicesOutput);
});

it('should overcome failing adb-devices dumping attempts', async () => {
const adbError = new Error('Yet another unexpected ADB adbError');
const instance = anInstance();
givenAdbConnectResult(instance);
givenRetryOnce();
adb.devices.mockRejectedValue(adbError);

await uut.adbConnectInstance(instance.uuid);
expect(log.warn).toHaveBeenCalledWith(expect.stringContaining('adb-connect command failed'), adbError);
});
});

describe('device instance deletion', () => {
Expand All @@ -85,4 +138,12 @@ describe('Genymotion-Cloud instance-lifecycle service', () => {
expect(result.constructor.name).toContain('Instance');
});
});

function givenRetryOnce() {
retry.mockImplementationOnce(async ({ conditionFn }, func) => {
await func();
await conditionFn();
return await func();
});
}
});
2 changes: 1 addition & 1 deletion detox/src/devices/allocation/factories/android.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ class Genycloud extends DeviceAllocatorFactory {
const recipeService = new RecipesService(exec);

const InstanceLifecycleService = require('../drivers/android/genycloud/services/GenyInstanceLifecycleService');
const instanceLifecycleService = new InstanceLifecycleService(exec);
const instanceLifecycleService = new InstanceLifecycleService(exec, adb);

const RecipeQuerying = require('../drivers/android/genycloud/GenyRecipeQuerying');
const recipeQuerying = new RecipeQuerying(recipeService);
Expand Down
8 changes: 2 additions & 6 deletions detox/src/devices/common/drivers/android/exec/ADB.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,8 @@ class ADB {
await this.adbCmd('', 'start-server', { retries: 0, verbosity: 'high' });
}

async killDaemon() {
await this.adbCmd('', 'kill-server', { retries: 0, verbosity: 'high' });
}

async devices() {
const { stdout } = await this.adbCmd('', 'devices', { verbosity: 'high' });
async devices(options) {
const { stdout } = await this.adbCmd('', 'devices', { verbosity: 'high', ...options });
/** @type {DeviceHandle[]} */
const devices = _.chain(stdout)
.trim()
Expand Down
15 changes: 10 additions & 5 deletions detox/src/devices/common/drivers/android/exec/ADB.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,18 +81,23 @@ describe('ADB', () => {
const { devices } = await adb.devices();
expect(devices.length).toEqual(0);
});

it(`should allow for option overrides`, async () => {
const options = {
retries: 100,
verbosity: 'low',
};

await adb.devices(options);
expect(execWithRetriesAndLogs).toHaveBeenCalledWith(expect.any(String), options);
});
});

describe('ADB Daemon (server)', () => {
it('should start the daemon', async () => {
await adb.startDaemon();
expect(execWithRetriesAndLogs).toHaveBeenCalledWith(`"${adbBinPath}" start-server`, { retries: 0, verbosity: 'high' });
});

it('should kill the daemon', async () => {
await adb.killDaemon();
expect(execWithRetriesAndLogs).toHaveBeenCalledWith(`"${adbBinPath}" kill-server`, { retries: 0, verbosity: 'high' });
});
});

it('should await device boot', async () => {
Expand Down

0 comments on commit 8d6bc20

Please sign in to comment.