diff --git a/child.js b/child.js index a51d30b..3278fd5 100644 --- a/child.js +++ b/child.js @@ -13,7 +13,9 @@ function runCommand (command, opts) { }).catch(err => { if (err.code === 'ENOENT') { err = new Error( - require('./y.js')`npx: command not found: ${path.basename(cmd)}` + `npx: ${ + require('./y.js')`command not found: ${path.basename(cmd)}` + }` ) err.exitCode = 127 } @@ -51,7 +53,16 @@ function exec (cmd, args, opts) { return new Promise((resolve, reject) => { cp.exec(`${escapeArg(cmd, true)} ${ args.join(' ') - }`, opts, (err, stdout) => err ? reject(err) : resolve(stdout)) + }`, opts, (err, stdout) => { + if (err) { + if (typeof err.code === 'number') { + err.exitCode = err.code + } + reject(err) + } else { + resolve(stdout) + } + }) }) } @@ -63,7 +74,7 @@ function escapeArg (str, asPath) { .map(s => s.match(/\s+/) ? `"${s}"` : s) .join('\\') : process.platform === 'win32' - ? `"${path.normalize(str)}"` + ? `"${str}"` : str.match(/[^-_.~/\w]/) ? `'${str.replace(/'/g, "'\"'\"'")}'` : str diff --git a/package-lock.json b/package-lock.json index 47ad32a..73cbefd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -208,6 +208,12 @@ "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz", "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og=" }, + "caller": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/caller/-/caller-1.0.1.tgz", + "integrity": "sha1-uFGGD3Dhlds9J3OVqhp+I+ow7PU=", + "dev": true + }, "caller-path": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", @@ -641,12 +647,6 @@ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", "dev": true }, - "deeper": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/deeper/-/deeper-2.1.0.tgz", - "integrity": "sha1-vFZOX3MXT98gHgiwADDooU2nQ2g=", - "dev": true - }, "define-properties": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", @@ -4446,12 +4446,6 @@ "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", "dev": true }, - "only-shallow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/only-shallow/-/only-shallow-1.2.0.tgz", - "integrity": "sha1-cc7O26kyS8BRiu8Q7AgNMkncJGU=", - "dev": true - }, "opener": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz", @@ -4771,6 +4765,12 @@ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" }, + "require-inject": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/require-inject/-/require-inject-1.4.0.tgz", + "integrity": "sha1-YvDMG6SmugX2ePuT/xCx51dpkms=", + "dev": true + }, "require-main-filename": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", @@ -5149,9 +5149,9 @@ } }, "tap": { - "version": "10.3.4", - "resolved": "https://registry.npmjs.org/tap/-/tap-10.3.4.tgz", - "integrity": "sha512-6VvrmdikQVh+Mth7NMnGmfIMpugjvnsnLm/oBxvvxaue1g1N5PL+xb3KLoJBni7lCa7yuWIFCDn2X48J2T4LgA==", + "version": "10.5.1", + "resolved": "https://registry.npmjs.org/tap/-/tap-10.5.1.tgz", + "integrity": "sha512-1wfxVaE8y6JhVvZRJCn2HzC3ShiF5T17/kZynd2+tkszAWaOJ7ADzWcBKmxtTkXZ/6h4rZgt5qgw2OgQ02ehsw==", "dev": true, "dependencies": { "isexe": { @@ -5169,9 +5169,9 @@ } }, "tap-mocha-reporter": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/tap-mocha-reporter/-/tap-mocha-reporter-3.0.3.tgz", - "integrity": "sha1-5ZF/rT2acJV/m3xzbnk764fX2vE=", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/tap-mocha-reporter/-/tap-mocha-reporter-3.0.5.tgz", + "integrity": "sha512-YIoWejBBb+6gKOdu5B4H4oIKQhmRJsYGHSE5a6Mv87jriBDy/fAVLRVuMHTAP/vufYPcI3CKAck9VvnZxtQ4mA==", "dev": true }, "tap-parser": { @@ -5232,9 +5232,9 @@ "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=" }, "tmatch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-3.0.0.tgz", - "integrity": "sha1-fSBx3tu8WH8ZSs2jBnvQdHtnCZE=", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tmatch/-/tmatch-3.1.0.tgz", + "integrity": "sha512-W3MSATOCN4pVu2qFxmJLIArSifeSOFqnfx9hiUaVgOmeRoI2NbU7RNga+6G+L8ojlFeQge+ZPCclWyUpQ8UeNQ==", "dev": true }, "tough-cookie": { @@ -5267,6 +5267,12 @@ "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", "dev": true }, + "tsame": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tsame/-/tsame-1.1.2.tgz", + "integrity": "sha512-ovCs24PGjmByVPr9tSIOs/yjUX9sJl0grEmOsj9dZA/UknQkgPOKcUqM84aSCvt9awHuhc/boMzTg3BHFalxWw==", + "dev": true + }, "tunnel-agent": { "version": "0.4.3", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz", diff --git a/package.json b/package.json index 4d2ac6a..4ef5cb9 100644 --- a/package.json +++ b/package.json @@ -62,10 +62,11 @@ "marked-man": "^0.2.1", "mkdirp": "^0.5.1", "nyc": "^11.0.2", + "require-inject": "^1.4.0", "standard": "^10.0.2", "standard-version": "^4.2.0", "tacks": "^1.2.6", - "tap": "^10.3.4", + "tap": "^10.5.1", "weallbehave": "^1.2.0", "weallcontribute": "^1.0.8" }, diff --git a/test/child.js b/test/child.js new file mode 100644 index 0000000..cd96eec --- /dev/null +++ b/test/child.js @@ -0,0 +1,151 @@ +'use strict' + +const test = require('tap').test + +const child = require('../child.js') +const requireInject = require('require-inject') + +test('escapeArg on *nix', t => { + const origPlatform = process.platform + Object.defineProperty(process, 'platform', {value: 'linux'}) + t.equal(child.escapeArg('foo'), 'foo', 'standard arg left intact') + t.equal(child.escapeArg('foo bar'), '\'foo bar\'', '\'-escaped on *nix') + t.equal( + child.escapeArg('/foo bar/baz\'quux.JPg', true), + '\'/foo bar/baz\'"\'"\'quux.JPg\'', + 'paths escaped as usual' + ) + Object.defineProperty(process, 'platform', {value: origPlatform}) + t.done() +}) + +test('escapeArg on win32', t => { + const origPlatform = process.platform + Object.defineProperty(process, 'platform', {value: 'win32'}) + t.equal(child.escapeArg('foo'), '"foo"', 'standard arg escaped') + t.equal(child.escapeArg('foo bar'), '"foo bar"', '"-escaped on win32') + t.equal( + child.escapeArg('C:\\Foo bar\\baz\'"\\quux.JPg', true), + 'C:\\"Foo bar"\\baz\'"\\quux.JPg', + 'paths escaped as usual' + ) + Object.defineProperty(process, 'platform', {value: origPlatform}) + t.done() +}) + +test('exec', t => { + const child = requireInject('../child.js', { + 'child_process': { + exec (cmd, opts, cb) { + if (opts.fail) { + cb(new Error('exec failure requested')) + } else { + cb(null, {cmd, opts}) + } + } + } + }) + const origPlatform = process.platform + return child.exec('cmd', ['arg1', 'arg2'], {opt: 1}).then(ret => { + t.equal(ret.cmd, 'cmd arg1 arg2', 'command string concatenated') + t.deepEqual(ret.opts, {opt: 1}, 'options received!') + Object.defineProperty(process, 'platform', {value: 'linux'}) + return child.exec('/foo bar/baz .quux\\', ['arg1', 'arg 2']) + }).then(ret => { + t.equal(ret.cmd, "'/foo bar/baz .quux\\' arg1 arg 2", 'unix-style escapes') + Object.defineProperty(process, 'platform', {value: 'win32'}) + return child.exec('C:\\foo bar\\baz .quux\\a', ['arg1', 'arg 2']) + }).then(ret => { + t.equal(ret.cmd, 'C:\\"foo bar"\\"baz .quux"\\a arg1 arg 2', 'win32-style escapes') + Object.defineProperty(process, 'platform', {value: origPlatform}) + }).then(() => { + return child.exec('fail', [], {fail: true}).then(() => { + throw new Error('was supposed to fail') + }, err => { + t.equal(err.message, 'exec failure requested', 'got error') + }) + }) +}) + +test('exec (integration)', t => { + return child.exec('node', ['-p', '1+1']).then(stdout => { + t.equal(stdout.trim(), '2', 'node ran successfully') + return child.exec('node', ['-e', '"process.exit(123)"']).then(() => { + throw new Error('was not supposed to succeed') + }, err => { + t.equal(err.exitCode, 123, 'got the exit code from subproc') + }) + }) +}) + +test('spawn', t => { + return child.spawn('node', ['-p', '1+1']).then(res => { + t.deepEqual(res, { + code: 0, + stdout: '2\n', + stderr: '' + }) + return child.spawn('node', ['-e', 'process.exit(123)']).then(() => { + throw new Error('was not supposed to succeed') + }, err => { + t.equal(err.exitCode, 123, 'got the exit code from ') + }) + }) +}) + +test('runCommand with command arg', t => { + return child.runCommand('node', { + cmdOpts: ['-p', '1+1'], + stdio: 'pipe' + }).then(res => { + t.deepEqual(res, { + code: 0, + stdout: '2\n', + stderr: '' + }) + return child.runCommand('node', { + cmdOpts: ['-e', 'process.exit(123)'] + }).then(() => { + throw new Error('was not supposed to succeed') + }, err => { + t.equal(err.exitCode, 123, 'got the exit code from subproc') + }) + }).then(() => { + return child.runCommand('./not-a-command-at-all', {}).then(() => { + throw new Error('was not supposed to succeed') + }, err => { + t.match(err.message, /command not found/, 'error message reports ENOENT') + t.equal(err.exitCode, 127, '"not found" has code 127') + }) + }) +}) + +test('runCommand with opts.command', t => { + return child.runCommand(null, { + command: 'node', + cmdOpts: ['-p', '1+1'], + stdio: 'pipe' + }).then(res => { + t.deepEqual(res, { + code: 0, + stdout: '2\n', + stderr: '' + }) + }) +}) + +test('runCommand with opts.call and opts.shell', { + skip: process.platform === 'win32' && 'Windows passes different flags to shell' +}, t => { + return child.runCommand(null, { + shell: 'node', + call: './child.js', + stdio: 'pipe' + }).then(res => { + t.deepEqual(res, { + code: 0, + stdout: '', + stderr: '' + }) + }) +})