refactor: remove copied Adb.install from emulator.install (#1108)

`emulator.install` contains a copy of the code of `Adb.install` just to
be able to pass custom options to `execa`.

This change removes that duplicated code in favor of a new option in
`Adb.install` that allows to pass options through to `execa`.
This commit is contained in:
Raphael von der Grün 2020-11-17 09:06:44 +01:00 committed by GitHub
parent c144c08112
commit 671e1fd1c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 27 additions and 45 deletions

View File

@ -53,13 +53,17 @@ Adb.devices = function (opts) {
}); });
}; };
Adb.install = function (target, packagePath, opts) { Adb.install = function (target, packagePath, { replace = false, execOptions = {} } = {}) {
events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...'); events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...');
var args = ['-s', target, 'install']; var args = ['-s', target, 'install'];
if (opts && opts.replace) args.push('-r'); if (replace) args.push('-r');
return execa('adb', args.concat(packagePath), { cwd: os.tmpdir() }).then(({ stdout: output }) => {
// 'adb install' seems to always returns no error, even if installation fails const opts = { cwd: os.tmpdir(), ...execOptions };
// so we catching output to detect installation failure
return execa('adb', args.concat(packagePath), opts).then(({ stdout: output }) => {
// adb does not return an error code even if installation fails. Instead it puts a specific
// message to stdout, so we have to use RegExp matching to detect installation failure.
if (output.match(/Failure/)) { if (output.match(/Failure/)) {
if (output.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) { if (output.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
output += '\n\n' + 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' + output += '\n\n' + 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
@ -69,7 +73,7 @@ Adb.install = function (target, packagePath, opts) {
'\nEither uninstall an app or increment the versionCode.'; '\nEither uninstall an app or increment the versionCode.';
} }
return Promise.reject(new CordovaError('Failed to install apk to device: ' + output)); throw new CordovaError('Failed to install apk to target: ' + output);
} }
}); });
}; };

View File

@ -30,7 +30,6 @@ var CordovaError = require('cordova-common').CordovaError;
var android_sdk = require('./android_sdk'); var android_sdk = require('./android_sdk');
var check_reqs = require('./check_reqs'); var check_reqs = require('./check_reqs');
var which = require('which'); var which = require('which');
var os = require('os');
// constants // constants
const ONE_SECOND = 1000; // in milliseconds const ONE_SECOND = 1000; // in milliseconds
@ -406,11 +405,6 @@ module.exports.install = function (givenTarget, buildResults) {
// or the app doesn't installed at all, so no error catching needed. // or the app doesn't installed at all, so no error catching needed.
return Promise.resolve().then(function () { return Promise.resolve().then(function () {
var apk_path = build.findBestApkForArchitecture(buildResults, target.arch); var apk_path = build.findBestApkForArchitecture(buildResults, target.arch);
var execOptions = {
cwd: os.tmpdir(),
timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds
killSignal: EXEC_KILL_SIGNAL
};
events.emit('log', 'Using apk: ' + apk_path); events.emit('log', 'Using apk: ' + apk_path);
events.emit('log', 'Package name: ' + pkgName); events.emit('log', 'Package name: ' + pkgName);
@ -419,29 +413,18 @@ module.exports.install = function (givenTarget, buildResults) {
// A special function to call adb install in specific environment w/ specific options. // A special function to call adb install in specific environment w/ specific options.
// Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119 // Introduced as a part of fix for http://issues.apache.org/jira/browse/CB-9119
// to workaround sporadic emulator hangs // to workaround sporadic emulator hangs
function adbInstallWithOptions (target, apk, opts) { function adbInstallWithOptions (...args) {
events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...'); return Adb.install(...args, {
replace: true,
const args = ['-s', target, 'install', '-r', apk]; execOptions: {
return execa('adb', args, opts).then(({ stdout }) => { timeout: INSTALL_COMMAND_TIMEOUT, // in milliseconds
// adb does not return an error code even if installation fails. Instead it puts a specific killSignal: EXEC_KILL_SIGNAL
// message to stdout, so we have to use RegExp matching to detect installation failure.
if (/Failure/.test(stdout)) {
if (stdout.match(/INSTALL_PARSE_FAILED_NO_CERTIFICATES/)) {
stdout += 'Sign the build using \'-- --keystore\' or \'--buildConfig\'' +
' or sign and deploy the unsigned apk manually using Android tools.';
} else if (stdout.match(/INSTALL_FAILED_VERSION_DOWNGRADE/)) {
stdout += 'You\'re trying to install apk with a lower versionCode that is already installed.' +
'\nEither uninstall an app or increment the versionCode.';
}
throw new CordovaError('Failed to install apk to emulator: ' + stdout);
} }
}); });
} }
function installPromise () { function installPromise () {
return adbInstallWithOptions(target.target, apk_path, execOptions).catch(function (error) { return adbInstallWithOptions(target.target, apk_path).catch(function (error) {
// CB-9557 CB-10157 only uninstall and reinstall app if the one that // CB-9557 CB-10157 only uninstall and reinstall app if the one that
// is already installed on device was signed w/different certificate // is already installed on device was signed w/different certificate
if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; } if (!/INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES/.test(error.toString())) { throw error; }
@ -452,7 +435,7 @@ module.exports.install = function (givenTarget, buildResults) {
// This promise is always resolved, even if 'adb uninstall' fails to uninstall app // This promise is always resolved, even if 'adb uninstall' fails to uninstall app
// or the app doesn't installed at all, so no error catching needed. // or the app doesn't installed at all, so no error catching needed.
return Adb.uninstall(target.target, pkgName).then(function () { return Adb.uninstall(target.target, pkgName).then(function () {
return adbInstallWithOptions(target.target, apk_path, execOptions); return adbInstallWithOptions(target.target, apk_path);
}); });
}); });
} }

View File

@ -582,7 +582,6 @@ describe('emulator', () => {
let AndroidManifestGetActivitySpy; let AndroidManifestGetActivitySpy;
let AdbSpy; let AdbSpy;
let buildSpy; let buildSpy;
let execaSpy;
let target; let target;
beforeEach(() => { beforeEach(() => {
@ -597,14 +596,12 @@ describe('emulator', () => {
AndroidManifestSpy = jasmine.createSpy('AndroidManifest').and.returnValue(AndroidManifestFns); AndroidManifestSpy = jasmine.createSpy('AndroidManifest').and.returnValue(AndroidManifestFns);
emu.__set__('AndroidManifest', AndroidManifestSpy); emu.__set__('AndroidManifest', AndroidManifestSpy);
AdbSpy = jasmine.createSpyObj('Adb', ['shell', 'start', 'uninstall']); AdbSpy = jasmine.createSpyObj('Adb', ['shell', 'start', 'install', 'uninstall']);
AdbSpy.shell.and.returnValue(Promise.resolve()); AdbSpy.shell.and.returnValue(Promise.resolve());
AdbSpy.start.and.returnValue(Promise.resolve()); AdbSpy.start.and.returnValue(Promise.resolve());
AdbSpy.install.and.returnValue(Promise.resolve());
AdbSpy.uninstall.and.returnValue(Promise.resolve()); AdbSpy.uninstall.and.returnValue(Promise.resolve());
emu.__set__('Adb', AdbSpy); emu.__set__('Adb', AdbSpy);
execaSpy = jasmine.createSpy('execa').and.resolveTo({});
emu.__set__('execa', execaSpy);
}); });
it('should get the full target object if only id is specified', () => { it('should get the full target object if only id is specified', () => {
@ -618,8 +615,7 @@ describe('emulator', () => {
it('should install to the passed target', () => { it('should install to the passed target', () => {
return emu.install(target, {}).then(() => { return emu.install(target, {}).then(() => {
const execCmd = execaSpy.calls.argsFor(0)[1].join(' '); expect(AdbSpy.install.calls.argsFor(0)[0]).toBe(target.target);
expect(execCmd).toContain(`-s ${target.target} install`);
}); });
}); });
@ -636,26 +632,25 @@ describe('emulator', () => {
return emu.install(target, buildResults).then(() => { return emu.install(target, buildResults).then(() => {
expect(buildSpy.findBestApkForArchitecture).toHaveBeenCalledWith(buildResults, target.arch); expect(buildSpy.findBestApkForArchitecture).toHaveBeenCalledWith(buildResults, target.arch);
const execCmd = execaSpy.calls.argsFor(0)[1].join(' '); expect(AdbSpy.install.calls.argsFor(0)[1]).toBe(apkPath);
expect(execCmd).toContain(`install -r ${apkPath}`);
}); });
}); });
it('should uninstall and reinstall app if failure is due to different certificates', () => { it('should uninstall and reinstall app if failure is due to different certificates', () => {
execaSpy.and.returnValues( AdbSpy.install.and.returnValues(
...['Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES', ''] Promise.reject('Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES'),
.map(out => Promise.resolve({ stdout: out })) Promise.resolve()
); );
return emu.install(target, {}).then(() => { return emu.install(target, {}).then(() => {
expect(execaSpy).toHaveBeenCalledTimes(2); expect(AdbSpy.install).toHaveBeenCalledTimes(2);
expect(AdbSpy.uninstall).toHaveBeenCalled(); expect(AdbSpy.uninstall).toHaveBeenCalled();
}); });
}); });
it('should throw any error not caused by different certificates', () => { it('should throw any error not caused by different certificates', () => {
const errorMsg = 'Failure: Failed to install'; const errorMsg = 'Failure: Failed to install';
execaSpy.and.resolveTo({ stdout: errorMsg }); AdbSpy.install.and.rejectWith(new CordovaError(errorMsg));
return emu.install(target, {}).then( return emu.install(target, {}).then(
() => fail('Unexpectedly resolved'), () => fail('Unexpectedly resolved'),