From f5a346aa9ae0c401f8f0d0d4ced45db2e9070ca0 Mon Sep 17 00:00:00 2001 From: Gar Date: Wed, 10 Jan 2024 13:23:15 -0800 Subject: [PATCH] chore: rewrite tests --- lib/run-script-pkg.js | 2 + map.js | 1 - test/is-server-package.js | 27 -- test/is-windows.js | 12 - test/make-spawn-args.js | 287 ++++++--------- test/node-gyp-bin.js | 17 - test/package-envs.js | 46 --- test/run-script-pkg.js | 737 +++++++++++++++----------------------- test/run-script.js | 145 +++++++- test/set-path.js | 53 --- test/validate-options.js | 38 +- 11 files changed, 548 insertions(+), 817 deletions(-) delete mode 100644 map.js delete mode 100644 test/is-server-package.js delete mode 100644 test/is-windows.js delete mode 100644 test/node-gyp-bin.js delete mode 100644 test/package-envs.js delete mode 100644 test/set-path.js diff --git a/lib/run-script-pkg.js b/lib/run-script-pkg.js index a551828..1495fa3 100644 --- a/lib/run-script-pkg.js +++ b/lib/run-script-pkg.js @@ -93,6 +93,8 @@ const runScriptPkg = async options => { return p.catch(er => { const { signal } = er + // coverage disabled because win32 never emits signals + /* istanbul ignore next */ if (stdio === 'inherit' && signal) { // by the time we reach here, the child has already exited. we send the // signal back to ourselves again so that npm will exit with the same diff --git a/map.js b/map.js deleted file mode 100644 index a08c787..0000000 --- a/map.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = test => test.replace(/^test/, 'lib') diff --git a/test/is-server-package.js b/test/is-server-package.js deleted file mode 100644 index 9af4052..0000000 --- a/test/is-server-package.js +++ /dev/null @@ -1,27 +0,0 @@ -const t = require('tap') -const requireInject = require('require-inject') -const isServerPackage = require('../lib/is-server-package.js') - -t.test('returns true if server.js present', async t => { - const path = t.testdir({ - 'server.js': '', - }) - t.equal(await isServerPackage(path), true) -}) - -t.test('returns false if server.js missing', async t => { - const path = t.testdir({}) - t.equal(await isServerPackage(path), false) -}) - -t.test('returns false if server.js not a file', async t => { - const path = t.testdir({ - 'server.js': {}, - }) - t.equal(await isServerPackage(path), false) -}) - -t.test('works without fs.promises', async t => { - t.doesNotThrow(() => - requireInject('../lib/is-server-package', { fs: { ...require('fs'), promises: null } })) -}) diff --git a/test/is-windows.js b/test/is-windows.js deleted file mode 100644 index 7d17317..0000000 --- a/test/is-windows.js +++ /dev/null @@ -1,12 +0,0 @@ -const t = require('tap') -const isWindows = require('../lib/is-windows.js') - -if (!process.env.__FAKE_TESTING_PLATFORM__) { - t.equal(isWindows, process.platform === 'win32', 'actual') - const fake = isWindows ? 'unix' : 'win32' - t.spawn(process.execPath, [__filename, fake], { env: { - __FAKE_TESTING_PLATFORM__: fake, - } }) -} else { - t.equal(isWindows, process.platform !== 'win32', 'fake') -} diff --git a/test/make-spawn-args.js b/test/make-spawn-args.js index 0801597..cc1f719 100644 --- a/test/make-spawn-args.js +++ b/test/make-spawn-args.js @@ -1,181 +1,124 @@ const t = require('tap') -const requireInject = require('require-inject') -const isWindows = require('../lib/is-windows.js') - -if (!process.env.__FAKE_TESTING_PLATFORM__) { - const fake = isWindows ? 'posix' : 'win32' - t.spawn(process.execPath, [__filename, fake], { env: { - ...process.env, - __FAKE_TESTING_PLATFORM__: fake, - } }) -} - -const { dirname } = require('path') -const resolve = (...args) => { - const root = isWindows ? 'C:\\Temp' : '/tmp' - return [root, ...args].join(isWindows ? '\\' : '/') -} - -const makeSpawnArgs = requireInject('../lib/make-spawn-args.js', { - path: { - dirname, - resolve, +const spawk = require('spawk') +const runScript = require('..') + +const pkg = { + name: '@npmcli/run-script-test-package', + version: '1.0.0-test', + config: { + test_string: 'a value', + test_array: ['a string', 'another string'], + test_null: null, + test_false: false, }, -}) - -if (isWindows) { - t.test('windows', t => { - const comSpec = process.env.ComSpec - process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe' - t.teardown(() => { - process.env.ComSpec = comSpec - }) - - t.test('simple script', (t) => { - const [cmd, args, opts] = makeSpawnArgs({ - event: 'event', - path: 'path', - cmd: 'script "quoted parameter"; second command', - }) - t.equal(cmd, 'script "quoted parameter"; second command') - t.strictSame(args, []) - t.hasStrict(opts, { - env: { - npm_package_json: 'C:\\Temp\\path\\package.json', - npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script "quoted parameter"; second command', - npm_config_node_gyp: require.resolve('node-gyp/bin/node-gyp.js'), - }, - shell: true, - stdio: undefined, - cwd: 'path', - }, 'got expected options') - - t.end() - }) - - t.test('event with invalid characters runs', (t) => { - const [cmd, args, opts] = makeSpawnArgs({ - event: 'event<:>\x03', // everything after the word "event" is invalid - path: 'path', - cmd: 'script "quoted parameter"; second command', - }) - t.equal(cmd, 'script "quoted parameter"; second command') - t.strictSame(args, []) - t.hasStrict(opts, { - env: { - npm_package_json: 'C:\\Temp\\path\\package.json', - npm_lifecycle_event: 'event<:>\x03', - npm_lifecycle_script: 'script "quoted parameter"; second command', - npm_config_node_gyp: require.resolve('node-gyp/bin/node-gyp.js'), - }, - shell: true, - stdio: undefined, - cwd: 'path', - }, 'got expected options') - - t.end() - }) - - t.test('with a funky scriptShell', (t) => { - const [cmd, args, opts] = makeSpawnArgs({ - event: 'event', - path: 'path', - cmd: 'script "quoted parameter"; second command', - scriptShell: 'blrpop', - }) - t.equal(cmd, 'script "quoted parameter"; second command') - t.strictSame(args, []) - t.hasStrict(opts, { - env: { - npm_package_json: 'C:\\Temp\\path\\package.json', - npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script "quoted parameter"; second command', - }, - shell: 'blrpop', - stdio: undefined, - cwd: 'path', - }, 'got expected options') - - t.end() - }) - - t.test('with cmd.exe as scriptShell', (t) => { - const [cmd, args, opts] = makeSpawnArgs({ - event: 'event', - path: 'path', - cmd: 'script', - args: ['"quoted parameter";', 'second command'], - scriptShell: 'cmd.exe', - }) - t.equal(cmd, 'script') - t.strictSame(args, ['"quoted parameter";', 'second command']) - t.hasStrict(opts, { - env: { - npm_package_json: 'C:\\Temp\\path\\package.json', - npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script', - }, - shell: 'cmd.exe', - stdio: undefined, - cwd: 'path', - }, 'got expected options') - - t.end() - }) + engines: { + node: '>0.10', + npm: '>1.2.3', + }, + bin: 'index.js', + scripts: { + test: 'echo test', + 'weird\x04': 'echo weird', + }, +} - t.end() +t.test('spawn args', async t => { + const testdir = t.testdir({}) + await t.test('defaults', async t => { + spawk.spawn( + /.*/, + a => a.includes('echo test'), + e => { + return e.cwd === testdir && + e.stdio === 'pipe' && + e.stdioString === undefined && + e.shell === false && + e.env.npm_package_json.endsWith('package.json') && + e.env.npm_package_name === pkg.name && + e.env.npm_package_version === pkg.version && + e.env.npm_package_config_test_null === '' && + e.env.npm_package_config_test_false === '' && + e.env.npm_package_config_test_string === pkg.config.test_string && + e.env.npm_package_config_test_array === pkg.config.test_array.join('\n\n') && + e.env.npm_package_bin === pkg.bin && + e.env.npm_package_engines_npm === pkg.engines.npm && + e.env.npm_package_engines_node === pkg.engines.node && + e.env.npm_lifecycle_event === 'test' && + e.env.npm_lifecycle_script === 'echo test' && + e.env.npm_config_node_gyp === require.resolve('node-gyp/bin/node-gyp.js') + } + ) + await t.resolves(() => runScript({ + pkg, + path: testdir, + event: 'test', + })) + t.ok(spawk.done()) }) -} else { - t.test('posix', t => { - t.test('simple script', (t) => { - const [cmd, args, opts] = makeSpawnArgs({ - event: 'event', - path: 'path', - cmd: 'script', - args: ['"quoted parameter";', 'second command'], - }) - t.equal(cmd, 'script') - t.strictSame(args, ['"quoted parameter";', 'second command']) - t.hasStrict(opts, { - env: { - npm_package_json: '/tmp/path/package.json', - npm_lifecycle_event: 'event', - npm_lifecycle_script: 'script', - }, - shell: true, - stdio: undefined, - cwd: 'path', - windowsVerbatimArguments: undefined, - }, 'got expected options') - t.end() - }) + await t.test('provided env', async t => { + spawk.spawn( + /.*/, + a => a.includes('echo test'), + e => { + return e.env.test_fixture === 'a string' + } + ) + await t.resolves(() => runScript({ + pkg, + path: testdir, + env: { + test_fixture: 'a string', + }, + event: 'test', + })) + t.ok(spawk.done()) + }) - t.test('event with invalid characters runs', (t) => { - const [cmd, args, opts] = makeSpawnArgs({ - event: 'event<:>/\x04', - path: 'path', - cmd: 'script', - args: ['"quoted parameter";', 'second command'], - }) - t.equal(cmd, 'script') - t.strictSame(args, ['"quoted parameter";', 'second command']) - t.hasStrict(opts, { - env: { - npm_package_json: '/tmp/path/package.json', - npm_lifecycle_event: 'event<:>/\x04', - npm_lifecycle_script: 'script', - }, - shell: true, - stdio: undefined, - cwd: 'path', - windowsVerbatimArguments: undefined, - }, 'got expected options') + await t.test('provided args', async t => { + spawk.spawn( + /.*/, + a => a.find(arg => arg.includes('echo test') && arg.includes('argtest')) + ) + await t.resolves(() => runScript({ + pkg, + path: testdir, + args: ['argtest'], + event: 'test', + })) + t.ok(spawk.done()) + }) - t.end() - }) + t.test('event with invalid characters', async t => { + spawk.spawn( + /.*/, + a => a.includes('echo weird'), + e => { + return e.env.npm_lifecycle_event === 'weird\x04' && + e.env.npm_lifecycle_script === 'echo weird' + } + ) + await t.resolves(() => runScript({ + pkg, + path: testdir, + event: 'weird\x04', + })) + t.ok(spawk.done()) + }) - t.end() + await t.test('provided binPaths', async t => { + spawk.spawn( + /.*/, + false, + e => (e.env.PATH || e.env.Path).startsWith('/tmp/test-fixture/binpath') + ) + await t.resolves(() => runScript({ + pkg, + binPaths: ['/tmp/test-fixture/binpath'], + path: testdir, + args: ['test arg'], + event: 'test', + })) + t.ok(spawk.done()) }) -} +}) diff --git a/test/node-gyp-bin.js b/test/node-gyp-bin.js deleted file mode 100644 index b922b68..0000000 --- a/test/node-gyp-bin.js +++ /dev/null @@ -1,17 +0,0 @@ -const t = require('tap') - -const { exec } = require('child_process') - -const { delimiter, resolve } = require('path') -const PATH = [ - resolve(__dirname, '../lib/node-gyp-bin'), - ...(process.env.PATH.split(delimiter)), -].filter(p => !/\.bin/.test(p)).join(delimiter) - -const expect = 'v' + require('node-gyp/package.json').version + '\n' - -exec('node-gyp --version', { env: { - npm_config_node_gyp: require.resolve('node-gyp/bin/node-gyp.js'), - PATH, -} }, (er, stdout) => - t.equal(stdout, expect, 'should run expected node-gyp version')) diff --git a/test/package-envs.js b/test/package-envs.js deleted file mode 100644 index ecf2614..0000000 --- a/test/package-envs.js +++ /dev/null @@ -1,46 +0,0 @@ -const t = require('tap') -const packageEnvs = require('../lib/package-envs.js') - -t.strictSame(packageEnvs({}, { - name: 'name', - version: 'version', - config: { - number: 420.69, - null: null, - empty: '', - zero: 0, - false: false, - nested: { object: { is: { included: true } } }, - }, - bin: { - foo: './bar.js', - bar: false, - }, - engines: ['array', 'of', 'strings'], - some: { other: 'thing' }, -}), { - npm_package_name: 'name', - npm_package_version: 'version', - npm_package_config_number: '420.69', - npm_package_config_null: '', - npm_package_config_empty: '', - npm_package_config_zero: '0', - npm_package_config_false: '', - npm_package_config_nested_object_is_included: 'true', - npm_package_engines: 'array\n\nof\n\nstrings', - npm_package_bin_foo: './bar.js', - npm_package_bin_bar: '', -}) - -const foo = { foo: 'bar' } -const ret = packageEnvs(foo, { - name: 'name', - version: 'version', -}) -t.not(ret, foo, 'returns new object') -t.strictSame(ret, { - foo: 'bar', - npm_package_name: 'name', - npm_package_version: 'version', -}, 'new env object has new stuff') -t.strictSame(foo, { foo: 'bar' }, 'original unmodified') diff --git a/test/run-script-pkg.js b/test/run-script-pkg.js index 67aaa6b..9ff5f92 100644 --- a/test/run-script-pkg.js +++ b/test/run-script-pkg.js @@ -1,486 +1,319 @@ -const { EventEmitter } = require('events') const t = require('tap') -const requireInject = require('require-inject') +const spawk = require('spawk') +const runScript = require('..') -let fakeIsNodeGypPackage = false -let SIGNAL = null -const EXIT_CODE = 0 +const isWindows = process.platform === 'win32' +const emptyDir = t.testdir({}) -const runScriptPkg = requireInject('../lib/run-script-pkg.js', { - '../lib/make-spawn-args.js': options => ['sh', ['-c', options.cmd], options], - '@npmcli/promise-spawn': (...args) => { - const p = SIGNAL || EXIT_CODE - ? Promise.reject(Object.assign(new Error('test command failed'), { - signal: SIGNAL, - code: EXIT_CODE, - })) - : Promise.resolve(args) - p.process = new EventEmitter() - return p - }, - '@npmcli/node-gyp': { - isNodeGypPackage: async (path) => Promise.resolve(fakeIsNodeGypPackage), - defaultGypInstallScript: 'node-gyp rebuild' }, -}) - -t.test('pkg has no scripts, early exit', t => runScriptPkg({ - event: 'foo', - pkg: {}, -}).then(res => t.strictSame(res, { code: 0, signal: null }))) - -t.test('pkg has no scripts, early exit', t => runScriptPkg({ - event: 'install', - pkg: {}, -}).then(res => t.strictSame(res, { code: 0, signal: null }))) - -t.test('pkg has no scripts, no server.js', t => runScriptPkg({ - event: 'start', - pkg: {}, - path: t.testdir({}), -}).then(res => t.strictSame(res, { code: 0, signal: null }))) - -t.test('pkg has server.js, start not specified', async t => { - const path = t.testdir({ 'server.js': '' }) - const res = await runScriptPkg({ - event: 'start', - path, - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - pkg: { - _id: 'foo@1.2.3', - scripts: {}, - }, - }) - t.strictSame(res, ['sh', ['-c', 'node server.js'], { - stdioString: undefined, - event: 'start', - path, - scriptShell: 'sh', - args: [], - binPaths: false, - env: { - environ: 'value', - }, - stdio: 'pipe', - cmd: 'node server.js', - }, { - event: 'start', - script: 'node server.js', - pkgid: 'foo@1.2.3', - path, - }]) -}) - -t.test('pkg has server.js, start not specified, with args', async t => { - const path = t.testdir({ 'server.js': '' }) - const res = await runScriptPkg({ - event: 'start', - path, - scriptShell: 'sh', - env: { - environ: 'value', - }, - args: ['a', 'b', 'c'], - binPaths: false, - stdio: 'pipe', - pkg: { - _id: 'foo@1.2.3', - scripts: {}, - }, - }) - t.strictSame(res, ['sh', ['-c', 'node server.js'], { - stdioString: undefined, - event: 'start', - path, - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - cmd: 'node server.js', - args: ['a', 'b', 'c'], - binPaths: false, - }, { - event: 'start', - script: 'node server.js', - pkgid: 'foo@1.2.3', - path, - }]) -}) - -t.test('pkg has no foo script, early exit', t => runScriptPkg({ - event: 'foo', - pkg: { scripts: {} }, -}).then(res => t.strictSame(res, { code: 0, signal: null }))) - -t.test('pkg has no foo script, but custom cmd provided', t => runScriptPkg({ - event: 'foo', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - cmd: 'bar', - pkg: { - _id: 'foo@1.2.3', - scripts: {}, - }, -}).then(res => t.strictSame(res, ['sh', ['-c', 'bar'], { - stdioString: undefined, - event: 'foo', - path: 'path', - scriptShell: 'sh', - args: [], - binPaths: false, - env: { - environ: 'value', - }, - stdio: 'pipe', - cmd: 'bar', -}, { - event: 'foo', - script: 'bar', - pkgid: 'foo@1.2.3', - path: 'path', -}]))) - -t.test('do the banner when stdio is inherited, handle line breaks', t => { - const logs = [] - const consoleLog = console.log - console.log = (...args) => logs.push(args) - t.teardown(() => console.log = consoleLog) - return runScriptPkg({ - event: 'foo', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'inherit', - cmd: 'bar\nbaz\n', - pkg: { - _id: 'foo@1.2.3', - scripts: {}, - }, - }).then(res => t.strictSame(res, ['sh', ['-c', 'bar\nbaz\n'], { - stdioString: undefined, - event: 'foo', - path: 'path', - scriptShell: 'sh', - args: [], - binPaths: false, - env: { - environ: 'value', - }, - stdio: 'inherit', - cmd: 'bar\nbaz\n', - }, { - event: 'foo', - script: 'bar\nbaz\n', - pkgid: 'foo@1.2.3', - path: 'path', - }])).then(() => t.strictSame(logs, [['\n> foo@1.2.3 foo\n> bar\n> baz\n']])) -}) - -t.test('do not show banner when stdio is inherited, if suppressed', t => { - const logs = [] - const consoleLog = console.log - console.log = (...args) => logs.push(args) - t.teardown(() => console.log = consoleLog) - return runScriptPkg({ - event: 'foo', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'inherit', - cmd: 'bar', - pkg: { - _id: 'foo@1.2.3', - scripts: {}, - }, - banner: false, - }).then(res => t.strictSame(res, ['sh', ['-c', 'bar'], { - stdioString: undefined, - event: 'foo', - path: 'path', - scriptShell: 'sh', - args: [], - binPaths: false, - env: { - environ: 'value', - }, - stdio: 'inherit', - cmd: 'bar', - }, { - event: 'foo', - script: 'bar', - pkgid: 'foo@1.2.3', - path: 'path', - }])).then(() => t.strictSame(logs, [])) -}) +const pkill = process.kill +const consoleLog = console.log -t.test('do the banner with no pkgid', t => { +const mockConsole = t => { const logs = [] - const consoleLog = console.log console.log = (...args) => logs.push(args) t.teardown(() => console.log = consoleLog) - return runScriptPkg({ - event: 'foo', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'inherit', - cmd: 'bar', - args: ['baz', 'buzz'], - pkg: { - scripts: {}, - }, - }).then(res => t.strictSame(res, ['sh', ['-c', 'bar'], { - stdioString: undefined, - event: 'foo', - path: 'path', - scriptShell: 'sh', - binPaths: false, - env: { - environ: 'value', - }, - stdio: 'inherit', - cmd: 'bar', - args: ['baz', 'buzz'], - }, { - event: 'foo', - script: 'bar', - path: 'path', - pkgid: undefined, - }])).then(() => t.strictSame(logs, [['\n> foo\n> bar baz buzz\n']])) -}) + return logs +} -t.test('pkg has foo script', t => runScriptPkg({ - event: 'foo', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - pkg: { - _id: 'foo@1.2.3', - scripts: { - foo: 'bar', - }, - }, -}).then(res => t.strictSame(res, ['sh', ['-c', 'bar'], { - stdioString: undefined, - event: 'foo', - path: 'path', - scriptShell: 'sh', - args: [], - binPaths: false, - env: { - environ: 'value', - }, - stdio: 'pipe', - cmd: 'bar', -}, { - event: 'foo', - script: 'bar', - pkgid: 'foo@1.2.3', - path: 'path', -}]))) +t.test('run-script-pkg', async t => { + await t.test('do the banner when stdio is inherited, handle line breaks', async t => { + const logs = mockConsole(t) + spawk.spawn('sh', a => a.includes('bar\nbaz\n')) + await runScript({ + event: 'foo', + path: emptyDir, + scriptShell: 'sh', + env: { + environ: 'value', + }, + stdio: 'inherit', + cmd: 'bar\nbaz\n', + pkg: { + _id: 'foo@1.2.3', + scripts: {}, + }, + }) + t.strictSame(logs, [['\n> foo@1.2.3 foo\n> bar\n> baz\n']]) + t.ok(spawk.done()) + }) -t.test('pkg has foo script, with args', t => runScriptPkg({ - event: 'foo', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - pkg: { - _id: 'foo@1.2.3', - scripts: { - foo: 'bar', - }, - }, - args: ['a', 'b', 'c'], - binPaths: false, -}).then(res => t.strictSame(res, ['sh', ['-c', 'bar'], { - stdioString: undefined, - event: 'foo', - path: 'path', - scriptShell: 'sh', - args: ['a', 'b', 'c'], - binPaths: false, - env: { - environ: 'value', - }, - stdio: 'pipe', - cmd: 'bar', -}, { - event: 'foo', - script: 'bar', - pkgid: 'foo@1.2.3', - path: 'path', -}]))) + await t.test('do not show banner when stdio is inherited, if suppressed', async t => { + const logs = mockConsole(t) + spawk.spawn('sh', a => a.includes('bar')) + await runScript({ + event: 'foo', + path: emptyDir, + scriptShell: 'sh', + env: { + environ: 'value', + }, + stdio: 'inherit', + cmd: 'bar', + pkg: { + _id: 'foo@1.2.3', + scripts: {}, + }, + banner: false, + }) + t.strictSame(logs, []) + t.ok(spawk.done()) + }) -t.test('pkg has no install or preinstall script, but node-gyp files are present', async t => { - fakeIsNodeGypPackage = true + await t.test('do the banner with no pkgid', async t => { + const logs = mockConsole(t) + spawk.spawn('sh', a => a.includes('bar baz buzz')) + await runScript({ + event: 'foo', + path: emptyDir, + scriptShell: 'sh', + env: { + environ: 'value', + }, + stdio: 'inherit', + cmd: 'bar', + args: ['baz', 'buzz'], + pkg: { + scripts: {}, + }, + }) + t.strictSame(logs, [['\n> foo\n> bar baz buzz\n']]) + t.ok(spawk.done()) + }) - const res = await runScriptPkg({ - event: 'install', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - pkg: { - _id: 'foo@1.2.3', - scripts: { + await t.test('pkg has foo script', async t => { + const logs = mockConsole(t) + spawk.spawn('sh', a => a.includes('bar')) + await runScript({ + event: 'foo', + path: emptyDir, + scriptShell: 'sh', + env: { + environ: 'value', }, - }, + stdio: 'pipe', + pkg: { + _id: 'foo@1.2.3', + scripts: { + foo: 'bar', + }, + }, + }) + t.strictSame(logs, []) + t.ok(spawk.done()) }) - t.strictSame(res, [ - 'sh', - ['-c', 'node-gyp rebuild'], - { - event: 'install', + await t.test('pkg has foo script, with args', async t => { + const logs = mockConsole(t) + spawk.spawn('sh', a => a.includes('bar a b c')) + await runScript({ + event: 'foo', path: 'path', scriptShell: 'sh', - args: [], - binPaths: false, - env: { environ: 'value' }, + env: { + environ: 'value', + }, stdio: 'pipe', - cmd: 'node-gyp rebuild', - stdioString: undefined, - }, - { - event: 'install', - script: 'node-gyp rebuild', - pkgid: 'foo@1.2.3', - path: 'path', - }, - ]) -}) + pkg: { + _id: 'foo@1.2.3', + scripts: { + foo: 'bar', + }, + }, + args: ['a', 'b', 'c'], + binPaths: false, + }) + t.strictSame(logs, []) + t.ok(spawk.done()) + }) -t.test('pkg has no install or preinstall script, but gypfile:false', async t => { - fakeIsNodeGypPackage = true + await t.test('pkg has no install or preinstall script, node-gyp files present', async t => { + const testdir = t.testdir({ + 'binding.gyp': 'exists', + }) - const res = await runScriptPkg({ - event: 'install', - path: 'path', - scriptShell: 'sh', - env: { - environ: 'value', - }, - stdio: 'pipe', - pkg: { - _id: 'foo@1.2.3', - gypfile: false, - scripts: { + const logs = mockConsole(t) + spawk.spawn('sh', a => a.includes('node-gyp rebuild')) + await runScript({ + event: 'install', + path: testdir, + scriptShell: 'sh', + env: { + environ: 'value', }, - }, + stdio: 'pipe', + pkg: { + _id: 'foo@1.2.3', + scripts: {}, + }, + }) + t.strictSame(logs, []) + t.ok(spawk.done()) }) - t.strictSame(res, { code: 0, signal: null }) -}) + t.test('pkg has no install or preinstall script, but gypfile:false', async t => { + const testdir = t.testdir({ + 'binding.gyp': 'exists', + }) -t.test('end stdin if present', async t => { - let stdinEnded = false - const rspWithEndedStdin = requireInject('../lib/run-script-pkg.js', { - '../lib/make-spawn-args.js': options => ['sh', ['-c', options.cmd], options], - '@npmcli/promise-spawn': (...args) => { - const p = Promise.resolve(args) - p.stdin = { end: () => stdinEnded = true } - p.process = new EventEmitter() - return p - }, - }) - await t.resolveMatch(rspWithEndedStdin({ - event: 'cat', - path: 'path', - stdin: { end: () => t.end() }, - pkg: { - _id: 'kitty@1.2.3', - scripts: { - cat: 'cat', + const res = await runScript({ + event: 'install', + path: testdir, + scriptShell: 'sh', + env: { + environ: 'value', }, - }, - }), ['sh', ['-c', 'cat'], { - event: 'cat', - path: 'path', - scriptShell: undefined, - env: {}, - stdio: 'pipe', - cmd: 'cat', - stdioString: undefined, - }, { - event: 'cat', - script: 'cat', - pkgid: 'kitty@1.2.3', - path: 'path', - }]) - t.equal(stdinEnded, true, 'stdin was ended properly') -}) + stdio: 'pipe', + pkg: { + _id: 'foo@1.2.3', + gypfile: false, + scripts: { + }, + }, + }) + t.strictSame(res, { code: 0, signal: null }) + }) -t.test('kill process when foreground process ends with signal', t => { - const { kill } = process.kill - t.teardown(() => { - process.kill = kill - SIGNAL = null + t.test('end stdin if present', async t => { + const interceptor = spawk.spawn('sh', a => a.includes('cat')) + await runScript({ + event: 'cat', + path: emptyDir, + scriptShell: 'sh', + pkg: { + _id: 'kitty@1.2.3', + scripts: { + cat: 'cat', + }, + }, + }) + t.ok(spawk.done()) + t.ok(interceptor.calledWith.stdio[0].writableEnded, 'stdin was ended properly') }) - process.kill = (pid, signal) => { - t.equal(process.pid, pid, 'got expected pid') - t.equal(signal, 'SIGFOO', 'got expected signal') - } - SIGNAL = 'SIGFOO' - const p = runScriptPkg({ - event: 'sleep', - path: 'path', - stdio: 'inherit', - banner: false, - signalTimeout: 1, - pkg: { - _id: 'husky@1.2.3', - name: 'husky', - version: '1.2.3', - scripts: { - sleep: 'sleep 1000000', + + await t.test('kill process when foreground process ends with signal', async t => { + t.teardown(() => { + process.kill = pkill + }) + let pid + let signal + process.kill = (p, s) => { + pid = p + signal = s + // make the process.kill actually stop things + throw new Error('process killed') + } + spawk.spawn('sh', a => a.includes('sleep 1000000')).signal('SIGFOO') + await t.rejects(runScript({ + event: 'sleep', + path: emptyDir, + scriptShell: 'sh', + stdio: 'inherit', + banner: false, + signalTimeout: 1, + pkg: { + _id: 'husky@1.2.3', + name: 'husky', + version: '1.2.3', + scripts: { + sleep: 'sleep 1000000', + }, }, - }, + })) + t.ok(spawk.done()) + if (!isWindows) { + t.equal(signal, 'SIGFOO', 'process.kill got expected signal') + t.equal(pid, process.pid, 'process.kill got expected pid') + } }) - p.catch(er => { - t.equal(er.signal, 'SIGFOO') - t.end() + + await t.test('kill process when foreground process ends with signal', async t => { + t.teardown(() => { + process.kill = pkill + }) + let pid + let signal + process.kill = (p, s) => { + pid = p + signal = s + // make the process.kill actually stop things + throw new Error('process killed') + } + spawk.spawn('sh', a => a.includes('sleep 1000000')).signal('SIGFOO') + await t.rejects(runScript({ + event: 'sleep', + path: emptyDir, + scriptShell: 'sh', + stdio: 'inherit', + banner: false, + signalTimeout: 1, + pkg: { + _id: 'husky@1.2.3', + name: 'husky', + version: '1.2.3', + scripts: { + sleep: 'sleep 1000000', + }, + }, + })) + t.ok(spawk.done()) + if (!isWindows) { + t.equal(signal, 'SIGFOO', 'process.kill got expected signal') + t.equal(pid, process.pid, 'process.kill got expected pid') + } }) -}) -t.test('fail promise when background process ends with signal', t => { - t.teardown(() => SIGNAL = null) - SIGNAL = 'SIGBAR' - const p = runScriptPkg({ - event: 'sleep', - path: 'path', - pkg: { - _id: 'husky@1.2.3', - name: 'husky', - version: '1.2.3', - scripts: { - sleep: 'sleep 1000000', + t.test('rejects if process.kill fails to end process', async t => { + t.teardown(() => { + process.kill = pkill + }) + let pid + let signal + process.kill = (p, s) => { + pid = p + signal = s + // do nothing here to emulate process.kill not killing the process + } + spawk.spawn('sh', a => a.includes('sleep 1000000')).signal('SIGFOO') + await t.rejects(runScript({ + event: 'sleep', + path: emptyDir, + stdio: 'inherit', + scriptShell: 'sh', + banner: false, + signalTimeout: 1, + pkg: { + _id: 'husky@1.2.3', + name: 'husky', + version: '1.2.3', + scripts: { + sleep: 'sleep 1000000', + }, }, - }, + })) + t.ok(spawk.done()) + if (!isWindows) { + t.equal(signal, 'SIGFOO', 'process.kill got expected signal') + t.equal(pid, process.pid, 'process.kill got expected pid') + } }) - p.catch(er => { - t.equal(er.signal, 'SIGBAR') - t.end() + + t.test('rejects if stdio is not inherit', async t => { + spawk.spawn('sh', a => a.includes('sleep 1000000')).signal('SIGFOO') + await t.rejects(runScript({ + event: 'sleep', + path: emptyDir, + banner: false, + scriptShell: 'sh', + signalTimeout: 1, + pkg: { + _id: 'husky@1.2.3', + name: 'husky', + version: '1.2.3', + scripts: { + sleep: 'sleep 1000000', + }, + }, + })) + t.ok(spawk.done()) }) }) diff --git a/test/run-script.js b/test/run-script.js index 5fcc438..c63a4cb 100644 --- a/test/run-script.js +++ b/test/run-script.js @@ -1,23 +1,132 @@ -const requireInject = require('require-inject') +const t = require('tap') +const spawk = require('spawk') +const runScript = require('..') -const runScript = requireInject('../lib/run-script.js', { - '../lib/run-script-pkg.js': async x => x, - '../lib/validate-options.js': () => {}, - 'read-package-json-fast': async x => x, -}) +t.test('run-script', async t => { + const emptyDir = t.testdir({}) + await t.test('no package provided, local package read', async t => { + spawk.spawn(/.*/, a => a.includes('echo test')) + const testdir = t.testdir({ + 'package.json': JSON.stringify({ + name: '@npmcli/run-script-test-package', + scripts: { + test: 'echo test', + }, + }), + }) + await t.resolves(() => runScript({ + path: testdir, + event: 'test', + })) + t.ok(spawk.done()) + }) -const t = require('tap') + await t.test('package provided, skip look up', async t => { + spawk.spawn(/.*/, a => a.includes('echo test')) + await t.resolves(() => runScript({ + pkg: { + name: '@npmcli/run-script-test-package', + scripts: { + test: 'echo test', + }, + }, + path: emptyDir, + event: 'test', + })) + t.ok(spawk.done()) + }) + + await t.test('non-install event, pkg has no scripts, early exit', async t => { + const res = await runScript({ + event: 'foo', + path: emptyDir, + pkg: {}, + }) + t.strictSame(res, { code: 0, signal: null }) + }) + + await t.test('non-install event, pkg does not have requested script', async t => { + const res = await runScript({ + event: 'foo', + path: emptyDir, + pkg: { + scripts: {}, + }, + }) + t.strictSame(res, { code: 0, signal: null }) + }) -t.test('no package provided, look up the package', t => - runScript({ path: 'foo' }).then(res => t.strictSame(res, { - path: 'foo', - pkg: 'foo/package.json', - }))) + await t.test('install event, pkg has no scripts, early exit', async t => { + const res = await runScript({ + event: 'install', + path: emptyDir, + pkg: {}, + }) + t.strictSame(res, { code: 0, signal: null }) + }) -t.test('package provided, skip look up', t => - runScript({ path: 'foo', pkg: 'bar' }).then(res => t.strictSame(res, { - path: 'foo', - pkg: 'bar', - }))) + await t.test('start event, pkg has no scripts, no server.js', async t => { + const res = await runScript({ + event: 'start', + path: emptyDir, + pkg: {}, + }) + t.strictSame(res, { code: 0, signal: null }) + }) -t.type(runScript.isServerPackage, 'function', 'export isServerPackage fn') + await t.test('start event, pkg has server.js but no start script', async t => { + const path = t.testdir({ 'server.js': '' }) + spawk.spawn(/.*/, a => a.includes('node server.js')) + const res = await runScript({ + event: 'start', + path, + pkg: { + _id: '@npmcli/run-script-test@1.2.3', + scripts: {}, + }, + }) + t.match(res, { + event: 'start', + script: 'node server.js', + pkgid: '@npmcli/run-script-test@1.2.3', + }) + }) + + await t.test('pkg does not have requested script, with custom cmd', async t => { + spawk.spawn(/.*/, a => a.includes('testcmd')) + const res = await runScript({ + event: 'foo', + cmd: 'testcmd', + path: emptyDir, + pkg: { + scripts: {}, + }, + }) + t.match(res, { + event: 'foo', + script: 'testcmd', + code: 0, + signal: null, + }) + t.ok(spawk.done()) + }) +}) + +t.test('isServerPackage', async t => { + await t.test('is server package', async t => { + const testdir = t.testdir({ + 'server.js': '', + }) + await t.resolves(runScript.isServerPackage(testdir), true) + }) + await t.test('is not server package - no server.js', async t => { + const testdir = t.testdir({}) + await t.resolves(runScript.isServerPackage(testdir), false) + }) + await t.test('is not server package - invalid server.js', async t => { + const testdir = t.testdir({ + 'server.js': {}, + }) + await t.resolves(runScript.isServerPackage(testdir), false) + }) +}) diff --git a/test/set-path.js b/test/set-path.js deleted file mode 100644 index 6af1339..0000000 --- a/test/set-path.js +++ /dev/null @@ -1,53 +0,0 @@ -const t = require('tap') -const { resolve, delimiter } = require('path').posix - -const setPATH = t.mock('../lib/set-path.js', { - // Always use posix path functions so tests are consistent - path: require('path').posix, -}) - -const paths = [ - '/x/y/z/node_modules/a/node_modules/b/node_modules/.bin', - '/x/y/z/node_modules/a/node_modules/node_modules/.bin', - '/x/y/z/node_modules/a/node_modules/.bin', - '/x/y/z/node_modules/node_modules/.bin', - '/x/y/z/node_modules/.bin', - '/x/y/node_modules/.bin', - '/x/node_modules/.bin', - '/node_modules/.bin', - resolve(__dirname, '../lib/node-gyp-bin'), - '/usr/local/bin', - '/usr/local/sbin', - '/usr/bin', - '/usr/sbin', - '/bin', - '/sbin', -] -t.test('no binPaths', async t => { - const projectPath = '/x/y/z/node_modules/a/node_modules/b' - t.strictSame(setPATH(projectPath, false, { - foo: 'bar', - PATH: '/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin', - }), { - foo: 'bar', - PATH: paths.join(delimiter), - }) -}) - -t.test('binPaths end up at beginning of PATH', async t => { - const projectPath = '/x/y/z/node_modules/a/node_modules/b' - const binPaths = [ - '/q/r/s/node_modules/.bin', - '/t/u/v/node_modules/.bin', - ] - t.strictSame(setPATH(projectPath, binPaths, { - foo: 'bar', - PATH: '/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin', - }), { - foo: 'bar', - PATH: [ - ...binPaths, - ...paths, - ].join(delimiter), - }) -}) diff --git a/test/validate-options.js b/test/validate-options.js index 6f2660f..d541931 100644 --- a/test/validate-options.js +++ b/test/validate-options.js @@ -1,26 +1,26 @@ +/* eslint-disable max-len */ const t = require('tap') -const validateOptions = require('../lib/validate-options.js') +const runScript = require('..') const cases = [ - ['xyz', 'invalid options object provided to runScript'], - [{}, 'valid event not provided to runScript'], - [{ event: false }, 'valid event not provided to runScript'], - [{ event: 'x' }, 'valid path not provided to runScript'], - [{ event: 'x', path: true }, 'valid path not provided to runScript'], - [{ event: 'x', path: 'x', scriptShell: 1 }, 'invalid scriptShell option provided to runScript'], - [{ event: 'x', path: 'x', env: null }, 'invalid env option provided to runScript'], - [{ event: 'x', path: 'x', stdio: null }, 'invalid stdio option provided to runScript'], - [{ event: 'x', path: 'x', args: null }, 'invalid args option provided to runScript'], - [{ event: 'x', path: 'x', args: [null] }, 'invalid args option provided to runScript'], - [{ event: 'x', path: 'x', args: ['x'], cmd: 7 }, 'invalid cmd option provided to runScript'], - [{ event: 'x', path: 'x', args: ['x'] }, null], + ['no options', false, 'invalid options object provided to runScript'], + ['not an object', 'xyz', 'invalid options object provided to runScript'], + ['no event', {}, 'valid event not provided to runScript'], + ['invalid event', { event: false }, 'valid event not provided to runScript'], + ['no path', { event: 'x' }, 'valid path not provided to runScript'], + ['invalid path', { event: 'x', path: true }, 'valid path not provided to runScript'], + ['invalid scriptShell', { event: 'x', path: 'x', scriptShell: 1 }, 'invalid scriptShell option provided to runScript'], + ['invalid env', { event: 'x', path: 'x', env: null }, 'invalid env option provided to runScript'], + ['invalid stdio', { event: 'x', path: 'x', stdio: null }, 'invalid stdio option provided to runScript'], + ['invalid args', { event: 'x', path: 'x', args: null }, 'invalid args option provided to runScript'], + ['invalid single arg', { event: 'x', path: 'x', args: [null] }, 'invalid args option provided to runScript'], + ['invalid cmd', { event: 'x', path: 'x', args: ['x'], cmd: 7 }, 'invalid cmd option provided to runScript'], ] -t.plan(cases.length) -cases.forEach(([options, message]) => { - if (message) { - t.throws(() => validateOptions(options), new TypeError(message)) - } else { - t.doesNotThrow(() => validateOptions(options)) +t.test('validate options error cases', async t => { + for (const [name, options, message] of cases) { + await t.test(name, async t => { + await t.rejects(runScript(options), { name: 'TypeError', message }) + }) } })