diff --git a/lib/set.js b/lib/set.js index 74a002cd638b..a9f16f3b3451 100644 --- a/lib/set.js +++ b/lib/set.js @@ -22,7 +22,7 @@ class Set extends BaseCommand { exec (args, cb) { if (!args.length) - return cb(this.usage) + return cb(this.usageError()) this.npm.commands.config(['set'].concat(args), cb) } } diff --git a/lib/test.js b/lib/test.js index e78fdf0c786c..8ab1e8582340 100644 --- a/lib/test.js +++ b/lib/test.js @@ -19,15 +19,5 @@ class Test extends LifecycleCmd { 'script-shell', ] } - - exec (args, cb) { - super.exec(args, er => { - if (er && er.code === 'ELIFECYCLE') { - /* eslint-disable standard/no-callback-literal */ - cb('Test failed. See above for more details.') - } else - cb(er) - }) - } } module.exports = Test diff --git a/node_modules/.gitignore b/node_modules/.gitignore index 2363ccfda45e..154c341e2b2c 100644 --- a/node_modules/.gitignore +++ b/node_modules/.gitignore @@ -325,6 +325,7 @@ readme* /source-map /source-map-support /space-separated-tokens +/spawk /spawn-wrap /spdx-compare /spdx-expression-validate diff --git a/package-lock.json b/package-lock.json index 01f2e5b3b315..22e6a9f8ca7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -163,6 +163,7 @@ "eslint-plugin-promise": "^5.1.0", "eslint-plugin-standard": "^5.0.0", "licensee": "^8.2.0", + "spawk": "^1.7.1", "tap": "^15.0.9" }, "engines": { @@ -7106,6 +7107,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/spawk": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/spawk/-/spawk-1.7.1.tgz", + "integrity": "sha512-qkPqVdPp5ICEeSYKB/qCkwIBB0IWQuouEvYenQvpTq15fqSQgutpH453NjEImrpCWTwQwj2bQjGp8YGHapEiWw==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -15722,6 +15732,12 @@ "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", "dev": true }, + "spawk": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/spawk/-/spawk-1.7.1.tgz", + "integrity": "sha512-qkPqVdPp5ICEeSYKB/qCkwIBB0IWQuouEvYenQvpTq15fqSQgutpH453NjEImrpCWTwQwj2bQjGp8YGHapEiWw==", + "dev": true + }, "spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", diff --git a/package.json b/package.json index b728f9f88d1e..ec0d9c1c60f7 100644 --- a/package.json +++ b/package.json @@ -199,6 +199,7 @@ "eslint-plugin-promise": "^5.1.0", "eslint-plugin-standard": "^5.0.0", "licensee": "^8.2.0", + "spawk": "^1.7.1", "tap": "^15.0.9" }, "scripts": { diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index e3be10b4b9aa..3faf8d5cf4f6 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -9,6 +9,10 @@ for (const level in npmlog.levels) const { title, execPath } = process const RealMockNpm = (t, otherMocks = {}) => { + t.afterEach(() => { + outputs.length = 0 + logs.length = 0 + }) t.teardown(() => { npm.perfStop() npmlog.record.length = 0 @@ -22,6 +26,9 @@ const RealMockNpm = (t, otherMocks = {}) => { }) const logs = [] const outputs = [] + const joinedOutput = () => { + return outputs.map(o => o.join(' ')).join('\n') + } const npm = t.mock('../../lib/npm.js', otherMocks) const command = async (command, args = []) => { return new Promise((resolve, reject) => { @@ -43,7 +50,7 @@ const RealMockNpm = (t, otherMocks = {}) => { } } npm.output = (...msg) => outputs.push(msg) - return { npm, logs, outputs, command } + return { npm, logs, outputs, command, joinedOutput } } const realConfig = require('../../lib/utils/config') diff --git a/test/lib/find-dupes.js b/test/lib/find-dupes.js index c7b33ceb6ed3..17940764b94a 100644 --- a/test/lib/find-dupes.js +++ b/test/lib/find-dupes.js @@ -1,24 +1,26 @@ const t = require('tap') -const FindDupes = require('../../lib/find-dupes.js') +const { real: mockNpm } = require('../fixtures/mock-npm') -t.test('should run dedupe in dryRun mode', (t) => { - t.plan(3) - const findDupesTest = new FindDupes({ - config: { - set: (k, v) => { - t.match(k, 'dry-run') - t.match(v, true) - }, +t.test('should run dedupe in dryRun mode', async (t) => { + t.plan(5) + const { npm, command } = mockNpm(t, { + '@npmcli/arborist': function (args) { + t.ok(args, 'gets options object') + t.ok(args.path, 'gets path option') + t.ok(args.dryRun, 'is called in dryRun mode') + this.dedupe = () => { + t.ok(true, 'dedupe is called') + } }, - commands: { - dedupe: (args, cb) => { - t.match(args, []) - cb() - }, + '../../lib/utils/reify-finish.js': (npm, arb) => { + t.ok(arb, 'gets arborist tree') }, }) - findDupesTest.exec({}, () => { - t.end() - }) + await npm.load() + // explicitly set to false so we can be 100% sure it's always true when it + // hits arborist + npm.config.set('dry-run', false) + npm.config.set('prefix', 'foo') + await command('find-dupes') }) diff --git a/test/lib/get.js b/test/lib/get.js index 9b77fbba3e6f..30e26b7453fe 100644 --- a/test/lib/get.js +++ b/test/lib/get.js @@ -1,16 +1,16 @@ const t = require('tap') +const { real: mockNpm } = require('../fixtures/mock-npm') -t.test('should retrieve values from npm.commands.config', (t) => { - const Get = t.mock('../../lib/get.js') - const get = new Get({ - commands: { - config: ([action, arg]) => { - t.equal(action, 'get', 'should use config get action') - t.equal(arg, 'foo', 'should use expected key') - t.end() - }, - }, - }) - - get.exec(['foo']) +t.test('should retrieve values from config', async t => { + const { joinedOutput, command, npm } = mockNpm(t) + const name = 'editor' + const value = 'vigor' + await npm.load() + npm.config.set(name, value) + await command('get', [name]) + t.equal( + joinedOutput(), + value, + 'outputs config item' + ) }) diff --git a/test/lib/load-all.js b/test/lib/load-all.js index e6e407805346..c38c244934ec 100644 --- a/test/lib/load-all.js +++ b/test/lib/load-all.js @@ -1,15 +1,24 @@ const t = require('tap') const glob = require('glob') const { resolve } = require('path') +const { real: mockNpm } = require('../fixtures/mock-npm') const full = process.env.npm_lifecycle_event === 'check-coverage' if (!full) t.pass('nothing to do here, not checking for full coverage') else { - // some files do config.get() on load, so have to load npm first - const npm = require('../../lib/npm.js') - t.test('load npm first', t => npm.load(t.end)) + const { npm } = mockNpm(t) + + t.teardown(() => { + const exitHandler = require('../../lib/utils/exit-handler.js') + exitHandler.setNpm(npm) + exitHandler() + }) + + t.test('load npm first', async t => { + await npm.load() + }) t.test('load all the files', t => { // just load all the files so we measure coverage for the missing tests @@ -21,11 +30,4 @@ else { t.pass('loaded all files') t.end() }) - - t.test('call the exit handler so we dont freak out', t => { - const exitHandler = require('../../lib/utils/exit-handler.js') - exitHandler.setNpm(npm) - exitHandler() - t.end() - }) } diff --git a/test/lib/prefix.js b/test/lib/prefix.js index 526631388e74..18a37f3ccd1e 100644 --- a/test/lib/prefix.js +++ b/test/lib/prefix.js @@ -1,19 +1,13 @@ const t = require('tap') +const { real: mockNpm } = require('../fixtures/mock-npm') -t.test('prefix', (t) => { - t.plan(3) - const dir = '/prefix/dir' - - const Prefix = require('../../lib/prefix.js') - const prefix = new Prefix({ - prefix: dir, - output: (output) => { - t.equal(output, dir, 'prints the correct directory') - }, - }) - - prefix.exec([], (err) => { - t.error(err, 'npm prefix') - t.ok('should have printed directory') - }) +t.test('prefix', async (t) => { + const { joinedOutput, command, npm } = mockNpm(t) + await npm.load() + await command('prefix') + t.equal( + joinedOutput(), + npm.prefix, + 'outputs npm.prefix' + ) }) diff --git a/test/lib/prune.js b/test/lib/prune.js index 87bb1370f3a1..3e47feb46139 100644 --- a/test/lib/prune.js +++ b/test/lib/prune.js @@ -1,7 +1,9 @@ const t = require('tap') +const { real: mockNpm } = require('../fixtures/mock-npm') -t.test('should prune using Arborist', (t) => { - const Prune = t.mock('../../lib/prune.js', { +t.test('should prune using Arborist', async (t) => { + t.plan(4) + const { command, npm } = mockNpm(t, { '@npmcli/arborist': function (args) { t.ok(args, 'gets options object') t.ok(args.path, 'gets path option') @@ -13,16 +15,6 @@ t.test('should prune using Arborist', (t) => { t.ok(arb, 'gets arborist tree') }, }) - const prune = new Prune({ - prefix: 'foo', - flatOptions: { - foo: 'bar', - }, - }) - prune.exec(null, er => { - if (er) - throw er - t.ok(true, 'callback is called') - t.end() - }) + await npm.load() + await command('prune') }) diff --git a/test/lib/restart.js b/test/lib/restart.js index 9719476c4180..153c31447282 100644 --- a/test/lib/restart.js +++ b/test/lib/restart.js @@ -1,16 +1,36 @@ const t = require('tap') -let runArgs -const npm = { - commands: { - 'run-script': (args, cb) => { - runArgs = args - cb() - }, - }, -} -const Restart = require('../../lib/restart.js') -const restart = new Restart(npm) -restart.exec(['foo'], () => { - t.match(runArgs, ['restart', 'foo']) - t.end() +const spawk = require('spawk') +const { real: mockNpm } = require('../fixtures/mock-npm') + +spawk.preventUnmatched() +t.teardown(() => { + spawk.unload() +}) + +// TODO this ... smells. npm "script-shell" config mentions defaults but those +// are handled by run-script, not npm. So for now we have to tie tests to some +// pretty specific internals of runScript +const makeSpawnArgs = require('@npmcli/run-script/lib/make-spawn-args.js') + +t.test('should run stop script from package.json', async t => { + const prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + restart: 'node ./test-restart.js', + }, + }), + }) + const { command, npm } = mockNpm(t) + await npm.load() + npm.log.level = 'silent' + npm.localPrefix = prefix + const [scriptShell] = makeSpawnArgs({ path: prefix }) + const script = spawk.spawn(scriptShell, (args) => { + t.ok(args.includes('node ./test-restart.js "foo"'), 'ran stop script with extra args') + return true + }) + await command('restart', ['foo']) + t.ok(script.called, 'script ran') }) diff --git a/test/lib/root.js b/test/lib/root.js index 5460f3d4985c..7b91654c6c98 100644 --- a/test/lib/root.js +++ b/test/lib/root.js @@ -1,19 +1,13 @@ const t = require('tap') +const { real: mockNpm } = require('../fixtures/mock-npm') -t.test('root', (t) => { - t.plan(3) - const dir = '/root/dir' - - const Root = require('../../lib/root.js') - const root = new Root({ - dir, - output: (output) => { - t.equal(output, dir, 'prints the correct directory') - }, - }) - - root.exec([], (err) => { - t.error(err, 'npm root') - t.ok('should have printed directory') - }) +t.test('prefix', async (t) => { + const { joinedOutput, command, npm } = mockNpm(t) + await npm.load() + await command('root') + t.equal( + joinedOutput(), + npm.dir, + 'outputs npm.dir' + ) }) diff --git a/test/lib/set.js b/test/lib/set.js index f51065a4b293..14d094001b44 100644 --- a/test/lib/set.js +++ b/test/lib/set.js @@ -1,5 +1,32 @@ const t = require('tap') +// can't run this until npm set can save to npm.localPrefix +t.skip('npm set', async t => { + const { real: mockNpm } = require('../fixtures/mock-npm') + const { joinedOutput, command, npm } = mockNpm(t) + await npm.load() + + t.test('no args', async t => { + t.rejects( + command('set'), + /Usage:/, + 'prints usage' + ) + }) + + t.test('test-config-item', async t => { + npm.localPrefix = t.testdir({}) + t.not(npm.config.get('test-config-item', 'project'), 'test config value', 'config is not already new value') + // This will write to ~/.npmrc! + // Don't unskip until we can write to project level + await command('set', ['test-config-item=test config value']) + t.equal(joinedOutput(), '', 'outputs nothing') + t.equal(npm.config.get('test-config-item', 'project'), 'test config value', 'config is set to new value') + }) +}) + +// Everything after this can go away once the above test is unskipped + let configArgs = null const npm = { commands: { diff --git a/test/lib/start.js b/test/lib/start.js index 4e77f9691b81..5c38c71a9a64 100644 --- a/test/lib/start.js +++ b/test/lib/start.js @@ -1,16 +1,36 @@ const t = require('tap') -let runArgs -const npm = { - commands: { - 'run-script': (args, cb) => { - runArgs = args - cb() - }, - }, -} -const Start = require('../../lib/start.js') -const start = new Start(npm) -start.exec(['foo'], () => { - t.match(runArgs, ['start', 'foo']) - t.end() +const spawk = require('spawk') +const { real: mockNpm } = require('../fixtures/mock-npm') + +spawk.preventUnmatched() +t.teardown(() => { + spawk.unload() +}) + +// TODO this ... smells. npm "script-shell" config mentions defaults but those +// are handled by run-script, not npm. So for now we have to tie tests to some +// pretty specific internals of runScript +const makeSpawnArgs = require('@npmcli/run-script/lib/make-spawn-args.js') + +t.test('should run stop script from package.json', async t => { + const prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + start: 'node ./test-start.js', + }, + }), + }) + const { command, npm } = mockNpm(t) + await npm.load() + npm.log.level = 'silent' + npm.localPrefix = prefix + const [scriptShell] = makeSpawnArgs({ path: prefix }) + const script = spawk.spawn(scriptShell, (args) => { + t.ok(args.includes('node ./test-start.js "foo"'), 'ran start script with extra args') + return true + }) + await command('start', ['foo']) + t.ok(script.called, 'script ran') }) diff --git a/test/lib/stop.js b/test/lib/stop.js index 92ca84bd8741..04cdb4e5e751 100644 --- a/test/lib/stop.js +++ b/test/lib/stop.js @@ -1,16 +1,36 @@ const t = require('tap') -let runArgs -const npm = { - commands: { - 'run-script': (args, cb) => { - runArgs = args - cb() - }, - }, -} -const Stop = require('../../lib/stop.js') -const stop = new Stop(npm) -stop.exec(['foo'], () => { - t.match(runArgs, ['stop', 'foo']) - t.end() +const spawk = require('spawk') +const { real: mockNpm } = require('../fixtures/mock-npm') + +spawk.preventUnmatched() +t.teardown(() => { + spawk.unload() +}) + +// TODO this ... smells. npm "script-shell" config mentions defaults but those +// are handled by run-script, not npm. So for now we have to tie tests to some +// pretty specific internals of runScript +const makeSpawnArgs = require('@npmcli/run-script/lib/make-spawn-args.js') + +t.test('should run stop script from package.json', async t => { + const prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + stop: 'node ./test-stop.js', + }, + }), + }) + const { command, npm } = mockNpm(t) + await npm.load() + npm.log.level = 'silent' + npm.localPrefix = prefix + const [scriptShell] = makeSpawnArgs({ path: prefix }) + const script = spawk.spawn(scriptShell, (args) => { + t.ok(args.includes('node ./test-stop.js "foo"'), 'ran stop script with extra args') + return true + }) + await command('stop', ['foo']) + t.ok(script.called, 'script ran') }) diff --git a/test/lib/test.js b/test/lib/test.js index c151b1e82534..d597ba274383 100644 --- a/test/lib/test.js +++ b/test/lib/test.js @@ -1,38 +1,36 @@ const t = require('tap') -let RUN_ARGS = null -const npm = { - commands: { - 'run-script': (args, cb) => { - RUN_ARGS = args - cb() - }, - }, -} -const Test = require('../../lib/test.js') -const test = new Test(npm) +const spawk = require('spawk') +const { real: mockNpm } = require('../fixtures/mock-npm') -t.test('run a test', t => { - test.exec([], (er) => { - t.strictSame(RUN_ARGS, ['test'], 'added "test" to the args') - }) - test.exec(['hello', 'world'], (er) => { - t.strictSame(RUN_ARGS, ['test', 'hello', 'world'], 'added positional args') - }) +spawk.preventUnmatched() +t.teardown(() => { + spawk.unload() +}) - const lcErr = Object.assign(new Error('should not see this'), { - code: 'ELIFECYCLE', - }) - const otherErr = new Error('should see this') +// TODO this ... smells. npm "script-shell" config mentions defaults but those +// are handled by run-script, not npm. So for now we have to tie tests to some +// pretty specific internals of runScript +const makeSpawnArgs = require('@npmcli/run-script/lib/make-spawn-args.js') - npm.commands['run-script'] = (args, cb) => cb(lcErr) - test.exec([], (er) => { - t.equal(er, 'Test failed. See above for more details.') +t.test('should run stop script from package.json', async t => { + const prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + test: 'node ./test-test.js', + }, + }), }) - - npm.commands['run-script'] = (args, cb) => cb(otherErr) - test.exec([], (er) => { - t.match(er, { message: 'should see this' }) + const { command, npm } = mockNpm(t) + await npm.load() + npm.log.level = 'silent' + npm.localPrefix = prefix + const [scriptShell] = makeSpawnArgs({ path: prefix }) + const script = spawk.spawn(scriptShell, (args) => { + t.ok(args.includes('node ./test-test.js "foo"'), 'ran test script with extra args') + return true }) - - t.end() + await command('test', ['foo']) + t.ok(script.called, 'script ran') }) diff --git a/test/lib/whoami.js b/test/lib/whoami.js index 9190e3858b13..c54ee2a5a2be 100644 --- a/test/lib/whoami.js +++ b/test/lib/whoami.js @@ -1,41 +1,25 @@ const t = require('tap') -const { fake: mockNpm } = require('../fixtures/mock-npm') +const { real: mockNpm } = require('../fixtures/mock-npm') -t.test('whoami', (t) => { - t.plan(3) - const Whoami = t.mock('../../lib/whoami.js', { - '../../lib/utils/get-identity.js': () => Promise.resolve('foo'), - }) - const npm = mockNpm({ - config: { json: false }, - output: (output) => { - t.equal(output, 'foo', 'should output the username') - }, - }) - - const whoami = new Whoami(npm) +const username = 'foo' +const { joinedOutput, command, npm } = mockNpm(t, { + '../../lib/utils/get-identity.js': () => Promise.resolve(username), +}) - whoami.exec([], (err) => { - t.error(err, 'npm whoami') - t.ok('should successfully print username') - }) +t.before(async () => { + await npm.load() }) -t.test('whoami json', (t) => { - t.plan(3) - const Whoami = t.mock('../../lib/whoami.js', { - '../../lib/utils/get-identity.js': () => Promise.resolve('foo'), - }) - const npm = mockNpm({ - config: { json: true }, - output: (output) => { - t.equal(output, '"foo"', 'should output the username') - }, - }) - const whoami = new Whoami(npm) +t.test('npm whoami', async (t) => { + await command('whoami') + t.equal(joinedOutput(), username, 'should print username') +}) - whoami.exec([], (err) => { - t.error(err, 'npm whoami') - t.ok('should successfully print username as json') +t.test('npm whoami --json', async (t) => { + t.teardown(() => { + npm.config.set('json', false) }) + npm.config.set('json', true) + await command('whoami') + t.equal(JSON.parse(joinedOutput()), username, 'should print username') })