From fd57909730fd5a105ac93055a0dd31776079fc5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Raphael=20von=20der=20Gr=C3=BCn?= Date: Mon, 6 Jan 2020 23:15:22 +0100 Subject: [PATCH] chore: replace superspawn & child_process with execa (#862) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: added execa dependency Co-authored-by: Raphael von der Grün * chore: execa - drop superspawn in android_sdk Co-authored-by: Raphael von der Grün * chore: execa - drop superspawn in build * chore: execa - drop superspawn in check_reqs Plus: Remove useless trimming of execa output Co-authored-by: Raphael von der Grün * chore: execa - drop superspawn in emulator Co-authored-by: Raphael von der Grün * chore: execa - drop superspawn in device Co-authored-by: Raphael von der Grün * chore: execa - drop superspawn in run_java_unit_tests * chore: execa - drop superspawn in ProjectBuilder Co-authored-by: Raphael von der Grün * chore: execa - drop superspawn in adb * chore: execa - drop superspawn in plugin.spec * chore: execa - replace child_process in log * chore: execa - replace child_process in check_reqs * chore: execa - replace child_process in emulator Co-authored-by: エリス --- bin/templates/cordova/lib/Adb.js | 16 ++--- bin/templates/cordova/lib/android_sdk.js | 6 +- bin/templates/cordova/lib/build.js | 6 +- .../cordova/lib/builders/ProjectBuilder.js | 15 ++--- bin/templates/cordova/lib/check_reqs.js | 17 +++--- bin/templates/cordova/lib/device.js | 4 +- bin/templates/cordova/lib/emulator.js | 51 +++++++--------- bin/templates/cordova/lib/log.js | 15 +---- package.json | 1 + spec/e2e/plugin.spec.js | 5 +- spec/unit/Adb.spec.js | 34 +++++------ spec/unit/android_sdk.spec.js | 12 ++-- spec/unit/builders/ProjectBuilder.spec.js | 37 ++++++------ spec/unit/device.spec.js | 12 ++-- spec/unit/emulator.spec.js | 60 +++++++++---------- test/run_java_unit_tests.js | 4 +- 16 files changed, 138 insertions(+), 157 deletions(-) diff --git a/bin/templates/cordova/lib/Adb.js b/bin/templates/cordova/lib/Adb.js index b6ad8f10..4cebcbac 100644 --- a/bin/templates/cordova/lib/Adb.js +++ b/bin/templates/cordova/lib/Adb.js @@ -19,8 +19,8 @@ var Q = require('q'); var os = require('os'); +var execa = require('execa'); var events = require('cordova-common').events; -var spawn = require('cordova-common').superspawn.spawn; var CordovaError = require('cordova-common').CordovaError; var Adb = {}; @@ -44,7 +44,7 @@ function isEmulator (line) { * devices/emulators */ Adb.devices = function (opts) { - return spawn('adb', ['devices'], { cwd: os.tmpdir() }).then(function (output) { + return execa('adb', ['devices'], { cwd: os.tmpdir() }).then(({ stdout: output }) => { return output.split('\n').filter(function (line) { // Filter out either real devices or emulators, depending on options return (line && opts && opts.emulators) ? isEmulator(line) : isDevice(line); @@ -58,7 +58,7 @@ Adb.install = function (target, packagePath, opts) { events.emit('verbose', 'Installing apk ' + packagePath + ' on target ' + target + '...'); var args = ['-s', target, 'install']; if (opts && opts.replace) args.push('-r'); - return spawn('adb', args.concat(packagePath), { cwd: os.tmpdir() }).then(function (output) { + return execa('adb', args.concat(packagePath), { cwd: os.tmpdir() }).then(({ stdout: output }) => { // 'adb install' seems to always returns no error, even if installation fails // so we catching output to detect installation failure if (output.match(/Failure/)) { @@ -77,24 +77,24 @@ Adb.install = function (target, packagePath, opts) { Adb.uninstall = function (target, packageId) { events.emit('verbose', 'Uninstalling package ' + packageId + ' from target ' + target + '...'); - return spawn('adb', ['-s', target, 'uninstall', packageId], { cwd: os.tmpdir() }); + return execa('adb', ['-s', target, 'uninstall', packageId], { cwd: os.tmpdir() }).then(({ stdout }) => stdout); }; Adb.shell = function (target, shellCommand) { events.emit('verbose', 'Running adb shell command "' + shellCommand + '" on target ' + target + '...'); var args = ['-s', target, 'shell']; shellCommand = shellCommand.split(/\s+/); - return spawn('adb', args.concat(shellCommand), { cwd: os.tmpdir() }).catch(function (output) { + return execa('adb', args.concat(shellCommand), { cwd: os.tmpdir() }).catch((error) => { return Q.reject(new CordovaError('Failed to execute shell command "' + - shellCommand + '"" on device: ' + output)); + shellCommand + '"" on device: ' + error)); }); }; Adb.start = function (target, activityName) { events.emit('verbose', 'Starting application "' + activityName + '" on target ' + target + '...'); - return Adb.shell(target, 'am start -W -a android.intent.action.MAIN -n' + activityName).catch(function (output) { + return Adb.shell(target, 'am start -W -a android.intent.action.MAIN -n' + activityName).catch((error) => { return Q.reject(new CordovaError('Failed to start application "' + - activityName + '"" on device: ' + output)); + activityName + '"" on device: ' + error)); }); }; diff --git a/bin/templates/cordova/lib/android_sdk.js b/bin/templates/cordova/lib/android_sdk.js index 1c0ab204..9074847c 100755 --- a/bin/templates/cordova/lib/android_sdk.js +++ b/bin/templates/cordova/lib/android_sdk.js @@ -17,7 +17,7 @@ under the License. */ -var superspawn = require('cordova-common').superspawn; +const execa = require('execa'); var suffix_number_regex = /(\d+)$/; // Used for sorting Android targets, example strings to sort: @@ -77,11 +77,11 @@ function parse_targets (output) { } module.exports.list_targets_with_android = function () { - return superspawn.spawn('android', ['list', 'target']).then(parse_targets); + return execa('android', ['list', 'target']).then(result => parse_targets(result.stdout)); }; module.exports.list_targets_with_avdmanager = function () { - return superspawn.spawn('avdmanager', ['list', 'target']).then(parse_targets); + return execa('avdmanager', ['list', 'target']).then(result => parse_targets(result.stdout)); }; module.exports.list_targets = function () { diff --git a/bin/templates/cordova/lib/build.js b/bin/templates/cordova/lib/build.js index 0695539e..c3399012 100644 --- a/bin/templates/cordova/lib/build.js +++ b/bin/templates/cordova/lib/build.js @@ -28,7 +28,7 @@ var Adb = require('./Adb'); var builders = require('./builders/builders'); var events = require('cordova-common').events; -var spawn = require('cordova-common').superspawn.spawn; +const execa = require('execa'); var CordovaError = require('cordova-common').CordovaError; var PackageType = require('./PackageType'); @@ -212,11 +212,11 @@ module.exports.detectArchitecture = function (target) { // Could probably find a x-platform version of killall, but I'm not actually // sure that this scenario even happens on non-OSX machines. events.emit('verbose', 'adb timed out while detecting device/emulator architecture. Killing adb and trying again.'); - return spawn('killall', ['adb']).then(function () { + return execa('killall', ['adb']).then(function () { return helper().then(null, function () { // The double kill is sadly often necessary, at least on mac. events.emit('warn', 'adb timed out a second time while detecting device/emulator architecture. Killing adb and trying again.'); - return spawn('killall', ['adb']).then(function () { + return execa('killall', ['adb']).then(function () { return helper().then(null, function () { return Q.reject(new CordovaError('adb timed out a third time while detecting device/emulator architecture. Try unplugging & replugging the device.')); }); diff --git a/bin/templates/cordova/lib/builders/ProjectBuilder.js b/bin/templates/cordova/lib/builders/ProjectBuilder.js index bb2a49a9..335578de 100644 --- a/bin/templates/cordova/lib/builders/ProjectBuilder.js +++ b/bin/templates/cordova/lib/builders/ProjectBuilder.js @@ -17,11 +17,10 @@ under the License. */ -var Q = require('q'); var fs = require('fs'); var path = require('path'); var shell = require('shelljs'); -var spawn = require('cordova-common').superspawn.spawn; +const execa = require('execa'); var events = require('cordova-common').events; var CordovaError = require('cordova-common').CordovaError; var check_reqs = require('../check_reqs'); @@ -80,7 +79,7 @@ class ProjectBuilder { if (fs.existsSync(gradlePath)) { // Literally do nothing, for some reason this works, while !fs.existsSync didn't on Windows } else { - return spawn(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' }); + return execa(gradle_cmd, ['-p', this.root, 'wrapper', '-b', wrapperGradle], { stdio: 'inherit' }); } } @@ -250,16 +249,16 @@ class ProjectBuilder { var wrapper = path.join(this.root, 'gradlew'); var args = this.getArgs(opts.buildType === 'debug' ? 'debug' : 'release', opts); - return spawn(wrapper, args, { stdio: 'inherit' }) + return execa(wrapper, args, { stdio: 'inherit' }) .catch(function (error) { if (error.toString().indexOf('failed to find target with hash string') >= 0) { return check_reqs.check_android_target(error).then(function () { // If due to some odd reason - check_android_target succeeds // we should still fail here. - return Q.reject(error); + throw error; }); } - return Q.reject(error); + throw error; }); } @@ -267,9 +266,7 @@ class ProjectBuilder { var builder = this; var wrapper = path.join(this.root, 'gradlew'); var args = builder.getArgs('clean', opts); - return Q().then(function () { - return spawn(wrapper, args, { stdio: 'inherit' }); - }) + return execa(wrapper, args, { stdio: 'inherit' }) .then(function () { shell.rm('-rf', path.join(builder.root, 'out')); diff --git a/bin/templates/cordova/lib/check_reqs.js b/bin/templates/cordova/lib/check_reqs.js index 6435e01a..c3338e7d 100644 --- a/bin/templates/cordova/lib/check_reqs.js +++ b/bin/templates/cordova/lib/check_reqs.js @@ -19,8 +19,8 @@ under the License. */ +const execa = require('execa'); var shelljs = require('shelljs'); -var child_process = require('child_process'); var Q = require('q'); var path = require('path'); var fs = require('fs'); @@ -28,7 +28,6 @@ var os = require('os'); var REPO_ROOT = path.join(__dirname, '..', '..', '..', '..'); var PROJECT_ROOT = path.join(__dirname, '..', '..'); var CordovaError = require('cordova-common').CordovaError; -var superspawn = require('cordova-common').superspawn; var android_sdk = require('./android_sdk'); function forgivingWhichSync (cmd) { @@ -71,7 +70,7 @@ module.exports.get_target = function () { // Returns a promise. Called only by build and clean commands. module.exports.check_ant = function () { - return superspawn.spawn('ant', ['-version']).then(function (output) { + return execa('ant', ['-version']).then(({ stdout: output }) => { // Parse Ant version from command output return /version ((?:\d+\.)+(?:\d+))/i.exec(output)[1]; }).catch(function (err) { @@ -89,7 +88,7 @@ module.exports.get_gradle_wrapper = function () { // OK, This hack only works on Windows, not on Mac OS or Linux. We will be deleting this eventually! if (module.exports.isWindows()) { - var result = child_process.spawnSync(path.join(__dirname, 'getASPath.bat')); + var result = execa.sync(path.join(__dirname, 'getASPath.bat')); // console.log('result.stdout =' + result.stdout.toString()); // console.log('result.stderr =' + result.stderr.toString()); @@ -157,8 +156,8 @@ module.exports.check_java = function () { var find_java = '/usr/libexec/java_home'; var default_java_error_msg = 'Failed to find \'JAVA_HOME\' environment variable. Try setting it manually.'; if (fs.existsSync(find_java)) { - return superspawn.spawn(find_java).then(function (stdout) { - process.env['JAVA_HOME'] = stdout.trim(); + return execa(find_java).then(({ stdout }) => { + process.env['JAVA_HOME'] = stdout; }).catch(function (err) { if (err) { throw new CordovaError(default_java_error_msg); @@ -194,11 +193,9 @@ module.exports.check_java = function () { } } }).then(function () { - return Q.denodeify(child_process.exec)('javac -version') - .then(outputs => { - // outputs contains two entries: stdout and stderr + return execa('javac', ['-version'], { all: true }) + .then(({ all: output }) => { // Java <= 8 writes version info to stderr, Java >= 9 to stdout - const output = outputs.join('').trim(); const match = /javac\s+([\d.]+)/i.exec(output); return match && match[1]; }, () => { diff --git a/bin/templates/cordova/lib/device.js b/bin/templates/cordova/lib/device.js index 1559e9b2..f0ea6d4d 100644 --- a/bin/templates/cordova/lib/device.js +++ b/bin/templates/cordova/lib/device.js @@ -19,11 +19,11 @@ under the License. */ +const execa = require('execa'); var build = require('./build'); var path = require('path'); var Adb = require('./Adb'); var AndroidManifest = require('./AndroidManifest'); -var spawn = require('cordova-common').superspawn.spawn; var CordovaError = require('cordova-common').CordovaError; var events = require('cordova-common').events; @@ -37,7 +37,7 @@ module.exports.list = function (lookHarder) { // adb kill-server doesn't seem to do the trick. // Could probably find a x-platform version of killall, but I'm not actually // sure that this scenario even happens on non-OSX machines. - return spawn('killall', ['adb']).then(function () { + return execa('killall', ['adb']).then(function () { events.emit('verbose', 'Restarting adb to see if more devices are detected.'); return Adb.devices(); }, function () { diff --git a/bin/templates/cordova/lib/emulator.js b/bin/templates/cordova/lib/emulator.js index fcdc1704..b5750520 100644 --- a/bin/templates/cordova/lib/emulator.js +++ b/bin/templates/cordova/lib/emulator.js @@ -19,6 +19,7 @@ under the License. */ +const execa = require('execa'); var android_versions = require('android-versions'); var retry = require('./retry'); var build = require('./build'); @@ -26,7 +27,6 @@ var path = require('path'); var Adb = require('./Adb'); var AndroidManifest = require('./AndroidManifest'); var events = require('cordova-common').events; -var superspawn = require('cordova-common').superspawn; var CordovaError = require('cordova-common').CordovaError; var shelljs = require('shelljs'); var android_sdk = require('./android_sdk'); @@ -34,7 +34,6 @@ var check_reqs = require('./check_reqs'); var os = require('os'); var fs = require('fs'); -var child_process = require('child_process'); // constants var ONE_SECOND = 1000; // in milliseconds @@ -53,7 +52,7 @@ function forgivingWhichSync (cmd) { } module.exports.list_images_using_avdmanager = function () { - return superspawn.spawn('avdmanager', ['list', 'avd']).then(function (output) { + return execa('avdmanager', ['list', 'avd']).then(({ stdout: output }) => { var response = output.split('\n'); var emulator_list = []; for (var i = 1; i < response.length; i++) { @@ -113,7 +112,7 @@ module.exports.list_images_using_avdmanager = function () { }; module.exports.list_images_using_android = function () { - return superspawn.spawn('android', ['list', 'avd']).then(function (output) { + return execa('android', ['list', 'avd']).then(({ stdout: output }) => { var response = output.split('\n'); var emulator_list = []; for (var i = 1; i < response.length; i++) { @@ -229,7 +228,7 @@ module.exports.list_started = function () { // Returns a promise. // TODO: we should remove this, there's a more robust method under android_sdk.js module.exports.list_targets = function () { - return superspawn.spawn('android', ['list', 'targets'], { cwd: os.tmpdir() }).then(function (output) { + return execa('android', ['list', 'targets'], { cwd: os.tmpdir() }).then(({ stdout: output }) => { var target_out = output.split('\n'); var targets = []; for (var i = target_out.length; i >= 0; i--) { @@ -294,8 +293,7 @@ module.exports.start = function (emulator_ID, boot_timeout) { var emulator_dir = path.dirname(shelljs.which('emulator')); var args = ['-avd', emulatorId, '-port', port]; // Don't wait for it to finish, since the emulator will probably keep running for a long time. - child_process - .spawn('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir }) + execa('emulator', args, { stdio: 'inherit', detached: true, cwd: emulator_dir }) .unref(); // wait for emulator to start @@ -387,22 +385,22 @@ module.exports.wait_for_boot = function (emulator_id, time_remaining) { module.exports.create_image = function (name, target) { console.log('Creating new avd named ' + name); if (target) { - return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', target]).then(null, function (error) { + return execa('android', ['create', 'avd', '--name', name, '--target', target]).then(null, function (error) { console.error('ERROR : Failed to create emulator image : '); console.error(' Do you have the latest android targets including ' + target + '?'); - console.error(error); + console.error(error.message); }); } else { console.log('WARNING : Project target not found, creating avd with a different target but the project may fail to install.'); // TODO: there's a more robust method for finding targets in android_sdk.js - return superspawn.spawn('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]).then(function () { + return execa('android', ['create', 'avd', '--name', name, '--target', this.list_targets()[0]]).then(function () { // TODO: This seems like another error case, even though it always happens. console.error('ERROR : Unable to create an avd emulator, no targets found.'); console.error('Ensure you have targets available by running the "android" command'); return Promise.reject(new CordovaError()); }, function (error) { console.error('ERROR : Failed to create emulator image : '); - console.error(error); + console.error(error.message); }); } }; @@ -474,24 +472,21 @@ module.exports.install = function (givenTarget, buildResults) { function adbInstallWithOptions (target, apk, opts) { events.emit('verbose', 'Installing apk ' + apk + ' on ' + target + '...'); - var command = 'adb -s ' + target + ' install -r "' + apk + '"'; - return new Promise(function (resolve, reject) { - child_process.exec(command, opts, function (err, stdout, stderr) { - if (err) reject(new CordovaError('Error executing "' + command + '": ' + stderr)); - // 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. - else 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.'; - } + const args = ['-s', target, 'install', '-r', apk]; + return execa('adb', args, opts).then(({ stdout }) => { + // 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 (/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.'; + } - reject(new CordovaError('Failed to install apk to emulator: ' + stdout)); - } else resolve(stdout); - }); + throw new CordovaError('Failed to install apk to emulator: ' + stdout); + } }); } diff --git a/bin/templates/cordova/lib/log.js b/bin/templates/cordova/lib/log.js index ec69f8c1..db0ba410 100644 --- a/bin/templates/cordova/lib/log.js +++ b/bin/templates/cordova/lib/log.js @@ -21,8 +21,7 @@ var path = require('path'); var os = require('os'); -var Q = require('q'); -var child_process = require('child_process'); +var execa = require('execa'); var ROOT = path.join(__dirname, '..', '..'); /* @@ -30,8 +29,7 @@ var ROOT = path.join(__dirname, '..', '..'); * Returns a promise. */ module.exports.run = function () { - var d = Q.defer(); - var adb = child_process.spawn('adb', ['logcat'], { cwd: os.tmpdir() }); + var adb = execa('adb', ['logcat'], { cwd: os.tmpdir(), stderr: 'inherit' }); adb.stdout.on('data', function (data) { var lines = data ? data.toString().split('\n') : []; @@ -39,14 +37,7 @@ module.exports.run = function () { console.log(out.join('\n')); }); - adb.stderr.on('data', console.error); - adb.on('close', function (code) { - if (code > 0) { - d.reject('Failed to run logcat command.'); - } else d.resolve(); - }); - - return d.promise; + return adb; }; module.exports.help = function () { diff --git a/package.json b/package.json index d1513439..54feca9e 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "android-versions": "^1.4.0", "compare-func": "^1.3.2", "cordova-common": "^3.2.0", + "execa": "^3.2.0", "nopt": "^4.0.1", "properties-parser": "^0.3.1", "q": "^1.5.1", diff --git a/spec/e2e/plugin.spec.js b/spec/e2e/plugin.spec.js index 509a0721..30fc7b6f 100644 --- a/spec/e2e/plugin.spec.js +++ b/spec/e2e/plugin.spec.js @@ -21,7 +21,8 @@ const os = require('os'); const fs = require('fs'); const path = require('path'); const shell = require('shelljs'); -const { PluginInfoProvider, superspawn } = require('cordova-common'); +const execa = require('execa'); +const { PluginInfoProvider } = require('cordova-common'); const createBin = path.join(__dirname, '../../bin/create'); const fakePluginPath = path.join(__dirname, 'fixtures/cordova-plugin-fake'); @@ -44,7 +45,7 @@ describe('plugin add', function () { const pluginInfo = new PluginInfoProvider().get(fakePluginPath); return Promise.resolve() - .then(() => superspawn.spawn(createBin, [projectPath, projectid, projectname])) + .then(() => execa(createBin, [projectPath, projectid, projectname])) .then(() => { const Api = require(path.join(projectPath, 'cordova/Api.js')); return new Api('android', projectPath).addPlugin(pluginInfo); diff --git a/spec/unit/Adb.spec.js b/spec/unit/Adb.spec.js index 24e3cb43..593470bc 100644 --- a/spec/unit/Adb.spec.js +++ b/spec/unit/Adb.spec.js @@ -33,12 +33,12 @@ emulator-5554\tdevice const downgradeError = 'adb: failed to install app.apk: Failure[INSTALL_FAILED_VERSION_DOWNGRADE]'; let Adb; - let spawnSpy; + let execaSpy; beforeEach(() => { Adb = rewire('../../bin/templates/cordova/lib/Adb'); - spawnSpy = jasmine.createSpy('spawn'); - Adb.__set__('spawn', spawnSpy); + execaSpy = jasmine.createSpy('execa'); + Adb.__set__('execa', execaSpy); }); describe('isDevice', () => { @@ -61,7 +61,7 @@ emulator-5554\tdevice describe('devices', () => { beforeEach(() => { - spawnSpy.and.returnValue(Promise.resolve(adbOutput)); + execaSpy.and.returnValue(Promise.resolve({ stdout: adbOutput })); }); it('should return only devices if no options are specified', () => { @@ -81,12 +81,12 @@ emulator-5554\tdevice describe('install', () => { beforeEach(() => { - spawnSpy.and.returnValue(Promise.resolve('')); + execaSpy.and.returnValue(Promise.resolve({ stdout: '' })); }); it('should target the passed device id to adb', () => { return Adb.install(deviceId).then(() => { - const args = spawnSpy.calls.argsFor(0); + const args = execaSpy.calls.argsFor(0); expect(args[0]).toBe('adb'); const adbArgs = args[1].join(' '); @@ -96,7 +96,7 @@ emulator-5554\tdevice it('should add the -r flag if opts.replace is set', () => { return Adb.install(deviceId, '', { replace: true }).then(() => { - const adbArgs = spawnSpy.calls.argsFor(0)[1]; + const adbArgs = execaSpy.calls.argsFor(0)[1]; expect(adbArgs).toContain('-r'); }); }); @@ -105,13 +105,13 @@ emulator-5554\tdevice const packagePath = 'build/test/app.apk'; return Adb.install(deviceId, packagePath).then(() => { - const adbArgs = spawnSpy.calls.argsFor(0)[1]; + const adbArgs = execaSpy.calls.argsFor(0)[1]; expect(adbArgs).toContain(packagePath); }); }); it('should reject with a CordovaError if the adb output suggests a failure', () => { - spawnSpy.and.returnValue(Promise.resolve(alreadyExistsError)); + execaSpy.and.returnValue(Promise.resolve({ stdout: alreadyExistsError })); return Adb.install(deviceId, '').then( () => fail('Unexpectedly resolved'), @@ -124,7 +124,7 @@ emulator-5554\tdevice // The following two tests are somewhat brittle as they are dependent on the // exact message returned. But it is better to have them tested than not at all. it('should give a more specific error message if there is a certificate failure', () => { - spawnSpy.and.returnValue(Promise.resolve(certificateError)); + execaSpy.and.returnValue(Promise.resolve({ stdout: certificateError })); return Adb.install(deviceId, '').then( () => fail('Unexpectedly resolved'), @@ -136,7 +136,7 @@ emulator-5554\tdevice }); it('should give a more specific error message if there is a downgrade error', () => { - spawnSpy.and.returnValue(Promise.resolve(downgradeError)); + execaSpy.and.returnValue(Promise.resolve({ stdout: downgradeError })); return Adb.install(deviceId, '').then( () => fail('Unexpectedly resolved'), @@ -151,10 +151,10 @@ emulator-5554\tdevice describe('uninstall', () => { it('should call adb uninstall with the correct arguments', () => { const packageId = 'io.cordova.test'; - spawnSpy.and.returnValue(Promise.resolve('')); + execaSpy.and.returnValue(Promise.resolve({ stdout: '' })); return Adb.uninstall(deviceId, packageId).then(() => { - const args = spawnSpy.calls.argsFor(0); + const args = execaSpy.calls.argsFor(0); expect(args[0]).toBe('adb'); const adbArgs = args[1]; @@ -169,10 +169,10 @@ emulator-5554\tdevice const shellCommand = 'ls -l /sdcard'; it('should run the passed command on the target device', () => { - spawnSpy.and.returnValue(Promise.resolve('')); + execaSpy.and.returnValue(Promise.resolve({ stdout: '' })); return Adb.shell(deviceId, shellCommand).then(() => { - const args = spawnSpy.calls.argsFor(0); + const args = execaSpy.calls.argsFor(0); expect(args[0]).toBe('adb'); const adbArgs = args[1].join(' '); @@ -184,7 +184,7 @@ emulator-5554\tdevice it('should reject with a CordovaError on failure', () => { const errorMessage = 'shell error'; - spawnSpy.and.returnValue(Promise.reject(errorMessage)); + execaSpy.and.rejectWith(new Error(errorMessage)); return Adb.shell(deviceId, shellCommand).then( () => fail('Unexpectedly resolved'), @@ -214,7 +214,7 @@ emulator-5554\tdevice it('should reject with a CordovaError on a shell error', () => { const errorMessage = 'Test Start error'; - spyOn(Adb, 'shell').and.returnValue(Promise.reject(errorMessage)); + spyOn(Adb, 'shell').and.rejectWith(new CordovaError(errorMessage)); return Adb.start(deviceId, activityName).then( () => fail('Unexpectedly resolved'), diff --git a/spec/unit/android_sdk.spec.js b/spec/unit/android_sdk.spec.js index 0b0f422f..e5e0c322 100644 --- a/spec/unit/android_sdk.spec.js +++ b/spec/unit/android_sdk.spec.js @@ -17,16 +17,18 @@ under the License. */ -const superspawn = require('cordova-common').superspawn; const fs = require('fs'); const path = require('path'); const rewire = require('rewire'); describe('android_sdk', () => { let android_sdk; + let execaSpy; beforeEach(() => { android_sdk = rewire('../../bin/templates/cordova/lib/android_sdk'); + execaSpy = jasmine.createSpy('execa'); + android_sdk.__set__('execa', execaSpy); }); describe('sort_by_largest_numerical_suffix', () => { @@ -59,14 +61,14 @@ describe('android_sdk', () => { describe('list_targets_with_android', () => { it('should invoke `android` with the `list target` command and _not_ the `list targets` command, as the plural form is not supported in some Android SDK Tools versions', () => { - spyOn(superspawn, 'spawn').and.returnValue(new Promise(() => {}, () => {})); + execaSpy.and.returnValue(Promise.resolve({ stdout: '' })); android_sdk.list_targets_with_android(); - expect(superspawn.spawn).toHaveBeenCalledWith('android', ['list', 'target']); + expect(execaSpy).toHaveBeenCalledWith('android', ['list', 'target']); }); it('should parse and return results from `android list targets` command', () => { const testTargets = fs.readFileSync(path.join('spec', 'fixtures', 'sdk25.2-android_list_targets.txt'), 'utf-8'); - spyOn(superspawn, 'spawn').and.returnValue(Promise.resolve(testTargets)); + execaSpy.and.returnValue(Promise.resolve({ stdout: testTargets })); return android_sdk.list_targets_with_android().then(list => { [ 'Google Inc.:Google APIs:23', @@ -87,7 +89,7 @@ describe('android_sdk', () => { describe('list_targets_with_avdmanager', () => { it('should parse and return results from `avdmanager list target` command', () => { const testTargets = fs.readFileSync(path.join('spec', 'fixtures', 'sdk25.3-avdmanager_list_target.txt'), 'utf-8'); - spyOn(superspawn, 'spawn').and.returnValue(Promise.resolve(testTargets)); + execaSpy.and.returnValue(Promise.resolve({ stdout: testTargets })); return android_sdk.list_targets_with_avdmanager().then(list => { expect(list).toContain('android-25'); diff --git a/spec/unit/builders/ProjectBuilder.spec.js b/spec/unit/builders/ProjectBuilder.spec.js index 7104271e..bc1960ba 100644 --- a/spec/unit/builders/ProjectBuilder.spec.js +++ b/spec/unit/builders/ProjectBuilder.spec.js @@ -19,7 +19,6 @@ const fs = require('fs'); const path = require('path'); -const Q = require('q'); const rewire = require('rewire'); const CordovaError = require('cordova-common').CordovaError; @@ -29,12 +28,12 @@ describe('ProjectBuilder', () => { let builder; let ProjectBuilder; - let spawnSpy; + let execaSpy; beforeEach(() => { - spawnSpy = jasmine.createSpy('spawn').and.returnValue(Q.defer().promise); + execaSpy = jasmine.createSpy('execa').and.returnValue(new Promise(() => {})); ProjectBuilder = rewire('../../../bin/templates/cordova/lib/builders/ProjectBuilder'); - ProjectBuilder.__set__('spawn', spawnSpy); + ProjectBuilder.__set__('execa', execaSpy); builder = new ProjectBuilder(rootDir); }); @@ -120,13 +119,13 @@ describe('ProjectBuilder', () => { it('should run the provided gradle command if a gradle wrapper does not already exist', () => { spyOn(fs, 'existsSync').and.returnValue(false); builder.runGradleWrapper('/my/sweet/gradle'); - expect(spawnSpy).toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object)); + expect(execaSpy).toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object)); }); it('should do nothing if a gradle wrapper exists in the project directory', () => { spyOn(fs, 'existsSync').and.returnValue(true); builder.runGradleWrapper('/my/sweet/gradle'); - expect(spawnSpy).not.toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object)); + expect(execaSpy).not.toHaveBeenCalledWith('/my/sweet/gradle', jasmine.any(Array), jasmine.any(Object)); }); }); @@ -182,34 +181,34 @@ describe('ProjectBuilder', () => { builder.build({}); - expect(spawnSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), testArgs, jasmine.anything()); + expect(execaSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), testArgs, jasmine.anything()); }); it('should reject if the spawn fails', () => { - const errorMessage = 'ERROR: Failed to spawn'; - spawnSpy.and.returnValue(Q.reject(errorMessage)); + const errorMessage = 'Test error'; + execaSpy.and.rejectWith(new Error(errorMessage)); return builder.build({}).then( () => fail('Unexpectedly resolved'), - err => { - expect(err).toBe(errorMessage); + error => { + expect(error.message).toBe(errorMessage); } ); }); it('should check the Android target if failed to find target', () => { const checkReqsSpy = jasmine.createSpyObj('check_reqs', ['check_android_target']); - const errorMessage = 'ERROR: failed to find target with hash string'; + const testError = 'failed to find target with hash string'; ProjectBuilder.__set__('check_reqs', checkReqsSpy); - checkReqsSpy.check_android_target.and.returnValue(Q.resolve()); - spawnSpy.and.returnValue(Q.reject(errorMessage)); + checkReqsSpy.check_android_target.and.returnValue(Promise.resolve()); + execaSpy.and.rejectWith(testError); return builder.build({}).then( () => fail('Unexpectedly resolved'), - err => { - expect(checkReqsSpy.check_android_target).toHaveBeenCalledWith(errorMessage); - expect(err).toBe(errorMessage); + error => { + expect(checkReqsSpy.check_android_target).toHaveBeenCalledWith(testError); + expect(error).toBe(testError); } ); }); @@ -222,7 +221,7 @@ describe('ProjectBuilder', () => { shellSpy = jasmine.createSpyObj('shell', ['rm']); ProjectBuilder.__set__('shell', shellSpy); spyOn(builder, 'getArgs'); - spawnSpy.and.returnValue(Promise.resolve()); + execaSpy.and.returnValue(Promise.resolve()); }); it('should get arguments for cleaning', () => { @@ -238,7 +237,7 @@ describe('ProjectBuilder', () => { builder.getArgs.and.returnValue(gradleArgs); return builder.clean(opts).then(() => { - expect(spawnSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), gradleArgs, jasmine.anything()); + expect(execaSpy).toHaveBeenCalledWith(path.join(rootDir, 'gradlew'), gradleArgs, jasmine.anything()); }); }); diff --git a/spec/unit/device.spec.js b/spec/unit/device.spec.js index 27b6c62c..d5adcaa1 100644 --- a/spec/unit/device.spec.js +++ b/spec/unit/device.spec.js @@ -42,12 +42,12 @@ describe('device', () => { }); it('should kill adb and try to get devices again if none are found the first time, and `lookHarder` is set', () => { - const spawnSpy = jasmine.createSpy('spawn').and.returnValue(Promise.resolve()); - device.__set__('spawn', spawnSpy); + const execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve()); + device.__set__('execa', execaSpy); AdbSpy.devices.and.returnValues(Promise.resolve([]), Promise.resolve(DEVICE_LIST)); return device.list(true).then(list => { - expect(spawnSpy).toHaveBeenCalledWith('killall', ['adb']); + expect(execaSpy).toHaveBeenCalledWith('killall', ['adb']); expect(list).toBe(DEVICE_LIST); expect(AdbSpy.devices).toHaveBeenCalledTimes(2); }); @@ -55,12 +55,12 @@ describe('device', () => { it('should return the empty list if killing adb fails', () => { const emptyDevices = []; - const spawnSpy = jasmine.createSpy('spawn').and.returnValue(Promise.reject()); - device.__set__('spawn', spawnSpy); + const execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.reject()); + device.__set__('execa', execaSpy); AdbSpy.devices.and.returnValues(Promise.resolve(emptyDevices)); return device.list(true).then(list => { - expect(spawnSpy).toHaveBeenCalledWith('killall', ['adb']); + expect(execaSpy).toHaveBeenCalledWith('killall', ['adb']); expect(list).toBe(emptyDevices); expect(AdbSpy.devices).toHaveBeenCalledTimes(1); }); diff --git a/spec/unit/emulator.spec.js b/spec/unit/emulator.spec.js index 3f42fe7e..8ae01e54 100644 --- a/spec/unit/emulator.spec.js +++ b/spec/unit/emulator.spec.js @@ -24,7 +24,6 @@ const shelljs = require('shelljs'); const CordovaError = require('cordova-common').CordovaError; const check_reqs = require('../../bin/templates/cordova/lib/check_reqs'); -const superspawn = require('cordova-common').superspawn; describe('emulator', () => { const EMULATOR_LIST = ['emulator-5555', 'emulator-5556', 'emulator-5557']; @@ -37,7 +36,9 @@ describe('emulator', () => { describe('list_images_using_avdmanager', () => { it('should properly parse details of SDK Tools 25.3.1 `avdmanager` output', () => { const avdList = fs.readFileSync(path.join('spec', 'fixtures', 'sdk25.3-avdmanager_list_avd.txt'), 'utf-8'); - spyOn(superspawn, 'spawn').and.returnValue(Promise.resolve(avdList)); + + let execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve({ stdout: avdList })); + emu.__set__('execa', execaSpy); return emu.list_images_using_avdmanager().then(list => { expect(list).toBeDefined(); @@ -51,14 +52,18 @@ describe('emulator', () => { describe('list_images_using_android', () => { it('should invoke `android` with the `list avd` command and _not_ the `list avds` command, as the plural form is not supported in some Android SDK Tools versions', () => { - spyOn(superspawn, 'spawn').and.returnValue(new Promise(() => {}, () => {})); + let execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve({ stdout: '' })); + emu.__set__('execa', execaSpy); + emu.list_images_using_android(); - expect(superspawn.spawn).toHaveBeenCalledWith('android', ['list', 'avd']); + expect(execaSpy).toHaveBeenCalledWith('android', ['list', 'avd']); }); it('should properly parse details of SDK Tools pre-25.3.1 `android list avd` output', () => { const avdList = fs.readFileSync(path.join('spec', 'fixtures', 'sdk25.2-android_list_avd.txt'), 'utf-8'); - spyOn(superspawn, 'spawn').and.returnValue(Promise.resolve(avdList)); + + let execaSpy = jasmine.createSpy('execa').and.returnValue(Promise.resolve({ stdout: avdList })); + emu.__set__('execa', execaSpy); return emu.list_images_using_android().then(list => { expect(list).toBeDefined(); @@ -249,7 +254,7 @@ describe('emulator', () => { let emulator; let AdbSpy; let checkReqsSpy; - let childProcessSpy; + let execaSpy; let shellJsSpy; beforeEach(() => { @@ -268,9 +273,10 @@ describe('emulator', () => { checkReqsSpy = jasmine.createSpyObj('create_reqs', ['getAbsoluteAndroidCmd']); emu.__set__('check_reqs', checkReqsSpy); - childProcessSpy = jasmine.createSpyObj('child_process', ['spawn']); - childProcessSpy.spawn.and.returnValue(jasmine.createSpyObj('spawnFns', ['unref'])); - emu.__set__('child_process', childProcessSpy); + execaSpy = jasmine.createSpy('execa').and.returnValue( + jasmine.createSpyObj('spawnFns', ['unref']) + ); + emu.__set__('execa', execaSpy); spyOn(emu, 'get_available_port').and.returnValue(Promise.resolve(port)); spyOn(emu, 'wait_for_emulator').and.returnValue(Promise.resolve('randomname')); @@ -291,7 +297,7 @@ describe('emulator', () => { return emu.start().then(() => { // This is the earliest part in the code where we can hook in and check // the emulator that has been selected. - const spawnArgs = childProcessSpy.spawn.calls.argsFor(0); + const spawnArgs = execaSpy.calls.argsFor(0); expect(spawnArgs[1]).toContain(emulator.name); }); }); @@ -302,7 +308,7 @@ describe('emulator', () => { return emu.start(emulator.name).then(() => { expect(emu.best_image).not.toHaveBeenCalled(); - const spawnArgs = childProcessSpy.spawn.calls.argsFor(0); + const spawnArgs = execaSpy.calls.argsFor(0); expect(spawnArgs[1]).toContain(emulator.name); }); }); @@ -576,7 +582,7 @@ describe('emulator', () => { let AndroidManifestGetActivitySpy; let AdbSpy; let buildSpy; - let childProcessSpy; + let execaSpy; let target; beforeEach(() => { @@ -597,9 +603,8 @@ describe('emulator', () => { AdbSpy.uninstall.and.returnValue(Promise.resolve()); emu.__set__('Adb', AdbSpy); - childProcessSpy = jasmine.createSpyObj('child_process', ['exec']); - childProcessSpy.exec.and.callFake((cmd, opts, callback) => callback()); - emu.__set__('child_process', childProcessSpy); + execaSpy = jasmine.createSpy('execa').and.resolveTo({}); + emu.__set__('execa', execaSpy); }); it('should get the full target object if only id is specified', () => { @@ -613,7 +618,7 @@ describe('emulator', () => { it('should install to the passed target', () => { return emu.install(target, {}).then(() => { - const execCmd = childProcessSpy.exec.calls.argsFor(0)[0]; + const execCmd = execaSpy.calls.argsFor(0)[1].join(' '); expect(execCmd).toContain(`-s ${target.target} install`); }); }); @@ -631,33 +636,26 @@ describe('emulator', () => { return emu.install(target, buildResults).then(() => { expect(buildSpy.findBestApkForArchitecture).toHaveBeenCalledWith(buildResults, target.arch); - const execCmd = childProcessSpy.exec.calls.argsFor(0)[0]; - expect(execCmd).toMatch(new RegExp(`install.*${apkPath}`)); + const execCmd = execaSpy.calls.argsFor(0)[1].join(' '); + expect(execCmd).toContain(`install -r ${apkPath}`); }); }); it('should uninstall and reinstall app if failure is due to different certificates', () => { - let execAlreadyCalled; - childProcessSpy.exec.and.callFake((cmd, opts, callback) => { - if (!execAlreadyCalled) { - execAlreadyCalled = true; - callback(null, 'Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES'); - } else { - callback(); - } - }); + execaSpy.and.returnValues( + ...['Failure: INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES', ''] + .map(out => Promise.resolve({ stdout: out })) + ); return emu.install(target, {}).then(() => { - expect(childProcessSpy.exec).toHaveBeenCalledTimes(2); + expect(execaSpy).toHaveBeenCalledTimes(2); expect(AdbSpy.uninstall).toHaveBeenCalled(); }); }); it('should throw any error not caused by different certificates', () => { const errorMsg = 'Failure: Failed to install'; - childProcessSpy.exec.and.callFake((cmd, opts, callback) => { - callback(null, errorMsg); - }); + execaSpy.and.resolveTo({ stdout: errorMsg }); return emu.install(target, {}).then( () => fail('Unexpectedly resolved'), diff --git a/test/run_java_unit_tests.js b/test/run_java_unit_tests.js index 3f99d282..6aa72197 100644 --- a/test/run_java_unit_tests.js +++ b/test/run_java_unit_tests.js @@ -21,7 +21,7 @@ var Q = require('q'); var path = require('path'); -var superspawn = require('cordova-common').superspawn; +var execa = require('execa'); var ProjectBuilder = require('../bin/templates/cordova/lib/builders/ProjectBuilder'); Q.resolve() @@ -42,7 +42,7 @@ process.on('unhandledRejection', err => { function gradlew () { const wrapperPath = path.join(__dirname, 'gradlew'); - return superspawn.spawn(wrapperPath, Array.from(arguments), { + return execa(wrapperPath, Array.from(arguments), { stdio: 'inherit', cwd: __dirname });