Skip to content

Commit b992852

Browse files
authored
Merge pull request wix#2019 from wix/adb-install-wtf
Adb install wtf
2 parents 6a81871 + a64c46c commit b992852

File tree

12 files changed

+375
-35
lines changed

12 files changed

+375
-35
lines changed

detox/local-cli/test.js

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,12 @@ module.exports.builder = {
132132
group: 'Execution:',
133133
describe: `Use Detox' custom console-logging implementation, for logging Detox (non-device) logs. Disabling will fallback to node.js / test-runner's implementation (e.g. Jest / Mocha).`,
134134
},
135+
'force-adb-install': {
136+
boolean: true,
137+
default: false,
138+
group: 'Execution:',
139+
describe: `Due to problems with the "adb install" command on Android, Detox resorts to a different scheme for install APK's. Setting true will disable that and force usage of "adb install", instead.`,
140+
}
135141
};
136142

137143
const collectExtraArgs = require('./utils/collectExtraArgs')(module.exports.builder);
@@ -231,6 +237,7 @@ module.exports.handler = async function test(program) {
231237
(program.artifactsLocation ? `--artifacts-location "${program.artifactsLocation}"` : ''),
232238
(program.deviceName ? `--device-name "${program.deviceName}"` : ''),
233239
(program.useCustomLogger ? `--use-custom-logger "${program.useCustomLogger}"` : ''),
240+
(program.forceAdbInstall ? `--force-adb-install "${program.forceAdbInstall}"` : ''),
234241
]),
235242
...getPassthroughArguments(),
236243
]).join(' ');
@@ -291,7 +298,8 @@ module.exports.handler = async function test(program) {
291298
'reportSpecs',
292299
'readOnlyEmu',
293300
'deviceLaunchArgs',
294-
'useCustomLogger'
301+
'useCustomLogger',
302+
'forceAdbInstall',
295303
]),
296304
DETOX_START_TIMESTAMP: Date.now(),
297305
};

detox/src/devices/drivers/android/AndroidDriver.js

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const InvocationManager = invoke.InvocationManager;
99
const ADB = require('./tools/ADB');
1010
const AAPT = require('./tools/AAPT');
1111
const APKPath = require('./tools/APKPath');
12+
const AppUninstallHelper = require('./tools/AppUninstallHelper');
1213
const DeviceDriverBase = require('../DeviceDriverBase');
1314
const DetoxApi = require('../../../android/espressoapi/Detox');
1415
const EspressoDetoxApi = require('../../../android/espressoapi/EspressoDetox');
@@ -64,40 +65,28 @@ class AndroidDriver extends DeviceDriverBase {
6465
}
6566

6667
async getBundleIdFromBinary(apkPath) {
67-
return await this.aapt.getPackageName(getAbsoluteBinaryPath(apkPath));
68+
const binaryPath = getAbsoluteBinaryPath(apkPath);
69+
return await this.aapt.getPackageName(binaryPath);
6870
}
6971

70-
async installApp(deviceId, binaryPath, testBinaryPath) {
71-
binaryPath = getAbsoluteBinaryPath(binaryPath);
72+
async installApp(deviceId, _binaryPath, _testBinaryPath) {
73+
const {
74+
binaryPath,
75+
testBinaryPath,
76+
} = this._getInstallPaths(_binaryPath, _testBinaryPath);
7277
await this.adb.install(deviceId, binaryPath);
73-
await this.adb.install(deviceId, testBinaryPath ? getAbsoluteBinaryPath(testBinaryPath) : this.getTestApkPath(binaryPath));
74-
}
75-
76-
async pressBack(deviceId) {
77-
await this.uiDevice.pressBack();
78-
}
79-
80-
getTestApkPath(originalApkPath) {
81-
const testApkPath = APKPath.getTestApkPath(originalApkPath);
82-
83-
if (!fs.existsSync(testApkPath)) {
84-
throw new Error(`'${testApkPath}' could not be found, did you run './gradlew assembleAndroidTest' ?`);
85-
}
86-
87-
return testApkPath;
78+
await this.adb.install(deviceId, testBinaryPath);
8879
}
8980

9081
async uninstallApp(deviceId, bundleId) {
9182
await this.emitter.emit('beforeUninstallApp', { deviceId, bundleId });
83+
const uninstallHelper = new AppUninstallHelper(this.adb);
84+
await uninstallHelper.uninstall(deviceId, bundleId);
85+
}
9286

93-
if (await this.adb.isPackageInstalled(deviceId, bundleId)) {
94-
await this.adb.uninstall(deviceId, bundleId);
95-
}
96-
97-
const testBundle = `${bundleId}.test`;
98-
if (await this.adb.isPackageInstalled(deviceId, testBundle)) {
99-
await this.adb.uninstall(deviceId, testBundle);
100-
}
87+
async uninstallAppByApk(deviceId, apkPath) {
88+
const bundleId = await this.getBundleIdFromBinary(apkPath);
89+
await this.uninstallApp(deviceId, bundleId);
10190
}
10291

10392
async launchApp(deviceId, bundleId, launchArgs, languageAndLocale) {
@@ -150,6 +139,10 @@ class AndroidDriver extends DeviceDriverBase {
150139
}
151140
}
152141

142+
async pressBack(deviceId) {
143+
await this.uiDevice.pressBack();
144+
}
145+
153146
async sendToHome(deviceId, params) {
154147
await this.uiDevice.pressHome();
155148
}
@@ -252,6 +245,24 @@ class AndroidDriver extends DeviceDriverBase {
252245
await this.invocationManager.execute(call);
253246
}
254247

248+
_getInstallPaths(_binaryPath, _testBinaryPath) {
249+
const binaryPath = getAbsoluteBinaryPath(_binaryPath);
250+
const testBinaryPath = _testBinaryPath ? getAbsoluteBinaryPath(_testBinaryPath) : this._getTestApkPath(binaryPath);
251+
return {
252+
binaryPath,
253+
testBinaryPath,
254+
};
255+
}
256+
257+
_getTestApkPath(originalApkPath) {
258+
const testApkPath = APKPath.getTestApkPath(originalApkPath);
259+
260+
if (!fs.existsSync(testApkPath)) {
261+
throw new Error(`'${testApkPath}' could not be found, did you run './gradlew assembleAndroidTest'?`);
262+
}
263+
return testApkPath;
264+
}
265+
255266
async _launchInstrumentationProcess(deviceId, bundleId, rawLaunchArgs) {
256267
const launchArgs = this._prepareLaunchArgs(rawLaunchArgs, true);
257268
const additionalLaunchArgs = this._prepareLaunchArgs({debug: false});

detox/src/devices/drivers/android/AndroidDriver.test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,21 @@ describe('Android driver', () => {
33
const deviceId = 'device-id-mock';
44
const bundleId = 'bundle-id-mock';
55

6+
const mockGetAbsoluteBinaryPathImpl = (x) => `absolutePathOf(${x})`;
7+
const mockAPKPathGetTestApkPathImpl = (x) => `testApkPathOf(${x})`;
8+
69
let logger;
710
let client;
11+
let getAbsoluteBinaryPath;
12+
let fs;
813
let exec;
914
beforeEach(() => {
15+
jest.mock('fs', () => ({
16+
existsSync: jest.fn(),
17+
realpathSync: jest.fn(),
18+
}));
19+
fs = require('fs');
20+
1021
jest.mock('../../../utils/encoding', () => ({
1122
encodeBase64: (x) => `base64(${x})`,
1223
}));
@@ -19,6 +30,15 @@ describe('Android driver', () => {
1930
InstrumentationLogsParser: mockInstrumentationLogsParserClass,
2031
}));
2132

33+
jest.mock('../../../utils/getAbsoluteBinaryPath', () =>
34+
jest.fn().mockImplementation((x) => `absolutePathOf(${x})`),
35+
);
36+
getAbsoluteBinaryPath = require('../../../utils/getAbsoluteBinaryPath');
37+
38+
jest.mock('./tools/APKPath', () => ({
39+
getTestApkPath: mockAPKPathGetTestApkPathImpl,
40+
}));
41+
2242
const mockLogger = {
2343
warn: jest.fn(),
2444
};
@@ -213,6 +233,49 @@ describe('Android driver', () => {
213233
});
214234
});
215235

236+
describe('App installation', () => {
237+
const binaryPath = 'mock-bin-path';
238+
const testBinaryPath = 'mock-test-bin-path';
239+
240+
it('should adb-install the app\'s binary', async () => {
241+
await uut.installApp(deviceId, binaryPath, testBinaryPath);
242+
243+
expect(getAbsoluteBinaryPath).toHaveBeenCalledWith(binaryPath);
244+
expect(uut.adb.install).toHaveBeenCalledWith(deviceId, mockGetAbsoluteBinaryPathImpl(binaryPath));
245+
});
246+
247+
it('should adb-install the test binary', async () => {
248+
await uut.installApp(deviceId, binaryPath, testBinaryPath);
249+
250+
expect(getAbsoluteBinaryPath).toHaveBeenCalledWith(binaryPath);
251+
expect(uut.adb.install).toHaveBeenCalledWith(deviceId, mockGetAbsoluteBinaryPathImpl(testBinaryPath));
252+
});
253+
254+
it('should resort to auto test-binary path resolution, if not specific', async () => {
255+
const expectedTestBinPath = mockAPKPathGetTestApkPathImpl(mockGetAbsoluteBinaryPathImpl(binaryPath));
256+
257+
fs.existsSync.mockReturnValue(true);
258+
259+
await uut.installApp(deviceId, binaryPath, undefined);
260+
261+
expect(fs.existsSync).toHaveBeenCalledWith(expectedTestBinPath);
262+
expect(uut.adb.install).toHaveBeenCalledWith(deviceId, expectedTestBinPath);
263+
});
264+
265+
it('should throw if auto test-binary path resolves an invalid file', async () => {
266+
const expectedTestBinPath = mockAPKPathGetTestApkPathImpl(mockGetAbsoluteBinaryPathImpl(binaryPath));
267+
268+
fs.existsSync.mockReturnValue(false);
269+
270+
try {
271+
await uut.installApp(deviceId, binaryPath, undefined);
272+
fail('Expected an error');
273+
} catch (err) {
274+
expect(err.message).toContain(`'${expectedTestBinPath}'`);
275+
}
276+
});
277+
});
278+
216279
describe('net-port reversing', () => {
217280
const deviceId = 1010;
218281
const port = 1337;
@@ -234,6 +297,7 @@ class mockADBClass {
234297
this.getInstrumentationRunner = jest.fn();
235298
this.reverse = jest.fn();
236299
this.reverseRemove = jest.fn();
300+
this.install = jest.fn();
237301

238302
this.adbBin = 'ADB binary mock';
239303
}

detox/src/devices/drivers/android/EmulatorDriver.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,16 @@ const AndroidDriver = require('./AndroidDriver');
66
const FreeEmulatorFinder = require('./emulator/FreeEmulatorFinder');
77
const AVDValidator = require('./emulator/AVDValidator');
88
const EmulatorLauncher = require('./emulator/EmulatorLauncher');
9+
const EmulatorVersionResolver = require('./emulator/EmulatorVersionResolver');
910
const { EmulatorExec } = require('./tools/EmulatorExec');
1011
const EmulatorTelnet = require('./tools/EmulatorTelnet');
11-
const EmulatorVersionResolver = require('./emulator/EmulatorVersionResolver');
12+
const AppInstallHelper = require('./tools/AppInstallHelper');
1213
const DetoxRuntimeError = require('../../../errors/DetoxRuntimeError');
1314
const DeviceRegistry = require('../../DeviceRegistry');
1415
const environment = require('../../../utils/environment');
1516
const retry = require('../../../utils/retry');
1617
const log = require('../../../utils/logger').child({ __filename });
18+
const argparse = require('../../../utils/argparse');
1719

1820
const DetoxEmulatorsPortRange = {
1921
min: 10000,
@@ -61,6 +63,22 @@ class EmulatorDriver extends AndroidDriver {
6163
return adbName;
6264
}
6365

66+
async installApp(deviceId, _binaryPath, _testBinaryPath) {
67+
if (argparse.getArgValue('force-adb-install') === 'true') {
68+
return await super.installApp(deviceId, _binaryPath, _testBinaryPath);
69+
}
70+
71+
const {
72+
binaryPath,
73+
testBinaryPath,
74+
} = this._getInstallPaths(_binaryPath, _testBinaryPath);
75+
76+
await this.uninstallAppByApk(deviceId, binaryPath);
77+
78+
const installHelper = new AppInstallHelper(this.adb);
79+
await installHelper.install(deviceId, binaryPath, testBinaryPath);
80+
}
81+
6482
async cleanup(adbName, bundleId) {
6583
await this.deviceRegistry.disposeDevice(adbName);
6684
await super.cleanup(adbName, bundleId);

detox/src/devices/drivers/android/tools/AAPT.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
const _ = require('lodash');
21
const {getAaptPath} = require('../../../../utils/environment');
3-
const path = require('path');
42
const exec = require('../../../../utils/exec').execWithRetriesAndLogs;
53
const escape = require('../../../../utils/pipeCommands').escape.inQuotedString;
64
const egrep = require('../../../../utils/pipeCommands').search.fragment;
7-
const fsext = require('../../../../utils/fsext');
85

96
class AAPT {
107
constructor() {

detox/src/devices/drivers/android/tools/ADB.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,10 @@ class ADB {
104104
}
105105
}
106106

107+
/*async*/ remoteInstall(deviceId, path) {
108+
return this.shell(deviceId, `pm install ${path}`);
109+
}
110+
107111
async uninstall(deviceId, appId) {
108112
await this.adbCmd(deviceId, `uninstall ${appId}`);
109113
}
@@ -216,6 +220,10 @@ class ADB {
216220
return this.spawn(deviceId, ['shell', shellCommand]);
217221
}
218222

223+
async push(deviceId, src, dst) {
224+
await this.adbCmd(deviceId, `push "${src}" "${dst}"`);
225+
}
226+
219227
async pull(deviceId, src, dst = '') {
220228
await this.adbCmd(deviceId, `pull "${src}" "${dst}"`);
221229
}

detox/src/devices/drivers/android/tools/ADB.test.js

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
describe('ADB', () => {
32
const adbBinPath = `/Android/sdk-mock/platform-tools/adb`;
43

@@ -130,9 +129,6 @@ describe('ADB', () => {
130129
});
131130
});
132131

133-
describe('lookup device', () => {
134-
});
135-
136132
it(`install`, async () => {
137133
await adb.install('emulator-5556', 'path inside "quotes" to/app');
138134

@@ -167,6 +163,27 @@ describe('ADB', () => {
167163
expect(await adb.pidof('', 'com.google.android.ext.services')).toBe(NaN);
168164
});
169165

166+
it('push', async () => {
167+
const deviceId = 'mockEmulator';
168+
const sourceFile = '/mock-source/file.xyz';
169+
const destFile = '/sdcard/file.abc';
170+
await adb.push(deviceId, sourceFile, destFile);
171+
172+
expect(exec).toHaveBeenCalledWith(
173+
expect.stringContaining(`-s mockEmulator push "${sourceFile}" "${destFile}"`),
174+
undefined, undefined, expect.anything());
175+
});
176+
177+
it('remote-install', async () => {
178+
const deviceId = 'mockEmulator';
179+
const binaryPath = '/mock-path/filename.mock';
180+
await adb.remoteInstall(deviceId, binaryPath);
181+
182+
expect(exec).toHaveBeenCalledWith(
183+
expect.stringContaining(`-s mockEmulator shell "pm install ${binaryPath}"`),
184+
undefined, undefined, expect.anything());
185+
});
186+
170187
describe('unlockScreen', () => {
171188
const deviceId = 'mockEmulator';
172189

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
const EMU_TEMP_PATH = '/data/local/tmp';
2+
const EMU_TEMP_INSTALL_PATH = `${EMU_TEMP_PATH}/detox`;
3+
4+
class AppInstallHelper {
5+
constructor(adb, tempDir = EMU_TEMP_INSTALL_PATH) {
6+
this._adb = adb;
7+
this._tempDir = tempDir;
8+
}
9+
10+
async install(deviceId, appBinaryPath, testBinaryPath) {
11+
await this._prepareTempDirOnDevice(deviceId);
12+
13+
const appBinaryPathOnTarget = this._getTargetBinaryPath(false);
14+
await this._pushAndInstallBinary(deviceId, appBinaryPath, appBinaryPathOnTarget);
15+
16+
const testBinaryPathOnTarget = this._getTargetBinaryPath(true);
17+
await this._pushAndInstallBinary(deviceId, testBinaryPath, testBinaryPathOnTarget);
18+
}
19+
20+
async _prepareTempDirOnDevice(deviceId) {
21+
await this._adb.shell(deviceId, `rm -fr ${this._tempDir}`);
22+
await this._adb.shell(deviceId, `mkdir -p ${this._tempDir}`);
23+
}
24+
25+
async _pushAndInstallBinary(deviceId, binaryPath, binaryPathOnTarget) {
26+
await this._adb.push(deviceId, binaryPath, binaryPathOnTarget);
27+
await this._adb.remoteInstall(deviceId, binaryPathOnTarget);
28+
}
29+
30+
_getTargetBinaryPath(isTestBinary) {
31+
const filename = isTestBinary ? 'Test.apk' : 'Application.apk';
32+
return `${this._tempDir}/${filename}`;
33+
}
34+
}
35+
36+
module.exports = AppInstallHelper;

0 commit comments

Comments
 (0)