From 7b6db904e066114e65a8f68eadebf1c64b92ebda Mon Sep 17 00:00:00 2001 From: Gar Date: Fri, 28 May 2021 09:23:49 -0700 Subject: [PATCH] fix(libnpmexec): don't detach output from npm The npm output function refers to this.log. libnpmexec has that passed in, which decoupled the function from the npm object. This fixes it, and sets the tests up in a way where if the output function ever becomes detached from this.npm in the same way, tests will fail. PR-URL: https://github.com/npm/cli/pull/3329 Credit: @wraithgar Close: #3329 Reviewed-by: @ruyadorno --- lib/exec.js | 2 +- lib/init.js | 7 ++- tap-snapshots/test/lib/init.js.test.cjs | 21 ++++++-- test/fixtures/mock-npm.js | 65 ++++++++++++++++--------- test/lib/exec.js | 22 ++++----- test/lib/init.js | 20 +++++--- 6 files changed, 89 insertions(+), 48 deletions(-) diff --git a/lib/exec.js b/lib/exec.js index f30746b8c50ed..8a87615d9749e 100644 --- a/lib/exec.js +++ b/lib/exec.js @@ -76,8 +76,8 @@ class Exec extends BaseCommand { localBin, log, globalBin, - output, } = this.npm + const output = (...outputArgs) => this.npm.output(...outputArgs) const scriptShell = this.npm.config.get('script-shell') || undefined const packages = this.npm.config.get('package') const yes = this.npm.config.get('yes') diff --git a/lib/init.js b/lib/init.js index bc809a15e49a9..4dd091601e191 100644 --- a/lib/init.js +++ b/lib/init.js @@ -113,8 +113,13 @@ class Init extends BaseCommand { localBin, log, globalBin, - output, } = this.npm + // this function is definitely called. But because of coverage map stuff + // it ends up both uncovered, and the coverage report doesn't even mention. + // the tests do assert that some output happens, so we know this line is + // being hit. + /* istanbul ignore next */ + const output = (...outputArgs) => this.npm.output(...outputArgs) const locationMsg = await getLocationMsg({ color, path }) const runPath = path const scriptShell = this.npm.config.get('script-shell') || undefined diff --git a/tap-snapshots/test/lib/init.js.test.cjs b/tap-snapshots/test/lib/init.js.test.cjs index 043d8b641dcce..95abbe6c1d830 100644 --- a/tap-snapshots/test/lib/init.js.test.cjs +++ b/tap-snapshots/test/lib/init.js.test.cjs @@ -6,13 +6,28 @@ */ 'use strict' exports[`test/lib/init.js TAP workspaces no args > should print helper info 1`] = ` - +Array [ + Array [ + String( + This utility will walk you through creating a package.json file. + It only covers the most common items, and tries to guess sensible defaults. + + See \`npm help init\` for definitive documentation on these fields + and exactly what they do. + + Use \`npm install \` afterwards to install a package and + save it as a dependency in the package.json file. + + Press ^C at any time to quit. + ), + ], +] ` exports[`test/lib/init.js TAP workspaces no args, existing folder > should print helper info 1`] = ` - +Array [] ` exports[`test/lib/init.js TAP workspaces with arg but missing workspace folder > should print helper info 1`] = ` - +Array [] ` diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index aa8d44020ee36..c972c35b31861 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -4,35 +4,54 @@ const realConfig = require('../../lib/utils/config') -const mockLog = { - clearProgress: () => {}, - disableProgress: () => {}, - enableProgress: () => {}, - http: () => {}, - info: () => {}, - levels: [], - notice: () => {}, - pause: () => {}, - silly: () => {}, - verbose: () => {}, - warn: () => {}, -} -const mockNpm = (base = {}) => { - const config = base.config || {} - const flatOptions = base.flatOptions || {} - return { - log: mockLog, - ...base, - flatOptions, - config: { +class MockNpm { + constructor (base = {}) { + this._mockOutputs = [] + this.isMockNpm = true + this.base = base + + const config = base.config || {} + + for (const attr in base) { + if (attr !== 'config') { + this[attr] = base[attr] + } + } + + this.flatOptions = base.flatOptions || {} + this.config = { // for now just set `find` to what config.find should return // this works cause `find` is not an existing config entry find: (k) => ({...realConfig.defaults, ...config})[k], get: (k) => ({...realConfig.defaults, ...config})[k], set: (k, v) => config[k] = v, list: [{ ...realConfig.defaults, ...config}] - }, + } + if (!this.log) { + this.log = { + clearProgress: () => {}, + disableProgress: () => {}, + enableProgress: () => {}, + http: () => {}, + info: () => {}, + levels: [], + notice: () => {}, + pause: () => {}, + silly: () => {}, + verbose: () => {}, + warn: () => {}, + } + } + } + + output(...msg) { + if (this.base.output) + return this.base.output(msg) + this._mockOutputs.push(msg) } } -module.exports = mockNpm +// TODO export MockNpm, and change tests to use new MockNpm() +module.exports = (base = {}) => { + return new MockNpm(base) +} diff --git a/test/lib/exec.js b/test/lib/exec.js index 33e30e24f84e0..4f8cc02fce7bd 100644 --- a/test/lib/exec.js +++ b/test/lib/exec.js @@ -1,8 +1,6 @@ const t = require('tap') const mockNpm = require('../fixtures/mock-npm') const { resolve, delimiter } = require('path') -const OUTPUT = [] -const output = (...msg) => OUTPUT.push(msg) const ARB_CTOR = [] const ARB_ACTUAL_TREE = {} @@ -36,6 +34,7 @@ const config = { package: [], 'script-shell': 'shell-cmd', } + const npm = mockNpm({ flatOptions, config, @@ -53,7 +52,6 @@ const npm = mockNpm({ LOG_WARN.push(args) }, }, - output, }) const RUN_SCRIPTS = [] @@ -225,7 +223,7 @@ t.test('npm exec , run interactive shell', t => { ARB_CTOR.length = 0 MKDIRPS.length = 0 ARB_REIFY.length = 0 - OUTPUT.length = 0 + npm._mockOutputs.length = 0 exec.exec([], er => { if (er) throw er @@ -256,7 +254,7 @@ t.test('npm exec , run interactive shell', t => { process.stdin.isTTY = true run(t, true, () => { t.strictSame(LOG_WARN, []) - t.strictSame(OUTPUT, [ + t.strictSame(npm._mockOutputs, [ [`\nEntering npm script environment at location:\n${process.cwd()}\nType 'exit' or ^D when finished\n`], ], 'printed message about interactive shell') t.end() @@ -270,7 +268,7 @@ t.test('npm exec , run interactive shell', t => { run(t, true, () => { t.strictSame(LOG_WARN, []) - t.strictSame(OUTPUT, [ + t.strictSame(npm._mockOutputs, [ [`\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m at location:\u001b[0m\n\u001b[0m\u001b[2m${process.cwd()}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`], ], 'printed message about interactive shell') t.end() @@ -282,7 +280,7 @@ t.test('npm exec , run interactive shell', t => { process.stdin.isTTY = false run(t, true, () => { t.strictSame(LOG_WARN, []) - t.strictSame(OUTPUT, [], 'no message about interactive shell') + t.strictSame(npm._mockOutputs, [], 'no message about interactive shell') t.end() }) }) @@ -294,7 +292,7 @@ t.test('npm exec , run interactive shell', t => { t.strictSame(LOG_WARN, [ ['exec', 'Interactive mode disabled in CI environment'], ]) - t.strictSame(OUTPUT, [], 'no message about interactive shell') + t.strictSame(npm._mockOutputs, [], 'no message about interactive shell') t.end() }) }) @@ -316,7 +314,7 @@ t.test('npm exec , run interactive shell', t => { ARB_CTOR.length = 0 MKDIRPS.length = 0 ARB_REIFY.length = 0 - OUTPUT.length = 0 + npm._mockOutputs.length = 0 RUN_SCRIPTS.length = 0 t.end() }) @@ -1195,7 +1193,7 @@ t.test('workspaces', t => { return rej(er) t.strictSame(LOG_WARN, []) - t.strictSame(OUTPUT, [ + t.strictSame(npm._mockOutputs, [ [`\nEntering npm script environment in workspace a@1.0.0 at location:\n${resolve(npm.localPrefix, 'packages/a')}\nType 'exit' or ^D when finished\n`], ], 'printed message about interactive shell') res() @@ -1203,14 +1201,14 @@ t.test('workspaces', t => { }) config.color = true - OUTPUT.length = 0 + npm._mockOutputs.length = 0 await new Promise((res, rej) => { exec.execWorkspaces([], ['a'], er => { if (er) return rej(er) t.strictSame(LOG_WARN, []) - t.strictSame(OUTPUT, [ + t.strictSame(npm._mockOutputs, [ [`\u001b[0m\u001b[0m\n\u001b[0mEntering npm script environment\u001b[0m\u001b[0m in workspace \u001b[32ma@1.0.0\u001b[39m at location:\u001b[0m\n\u001b[0m\u001b[2m${resolve(npm.localPrefix, 'packages/a')}\u001b[22m\u001b[0m\u001b[1m\u001b[22m\n\u001b[1mType 'exit' or ^D when finished\u001b[22m\n\u001b[1m\u001b[22m`], ], 'printed message about interactive shell') res() diff --git a/test/lib/init.js b/test/lib/init.js index 0964bb5cedde6..268b170cb4839 100644 --- a/test/lib/init.js +++ b/test/lib/init.js @@ -3,7 +3,6 @@ const { resolve } = require('path') const t = require('tap') const mockNpm = require('../fixtures/mock-npm') -let result = '' const npmLog = { disableProgress: () => null, enableProgress: () => null, @@ -19,9 +18,6 @@ const config = { const npm = mockNpm({ config, log: npmLog, - output: (...msg) => { - result += msg.join('\n') - }, }) const mocks = { '../../lib/utils/usage.js': () => 'usage instructions', @@ -33,7 +29,6 @@ const _consolelog = console.log const noop = () => {} t.afterEach(() => { - result = '' config.yes = true config.package = undefined npm.log = npmLog @@ -322,6 +317,9 @@ t.test('npm init error', t => { t.test('workspaces', t => { t.test('no args', t => { + t.teardown(() => { + npm._mockOutputs.length = 0 + }) npm.localPrefix = t.testdir({ 'package.json': JSON.stringify({ name: 'top-level', @@ -340,12 +338,15 @@ t.test('workspaces', t => { if (err) throw err - t.matchSnapshot(result, 'should print helper info') + t.matchSnapshot(npm._mockOutputs, 'should print helper info') t.end() }) }) t.test('no args, existing folder', t => { + t.teardown(() => { + npm._mockOutputs.length = 0 + }) // init-package-json prints directly to console.log // this avoids poluting test output with those logs console.log = noop @@ -369,12 +370,15 @@ t.test('workspaces', t => { if (err) throw err - t.matchSnapshot(result, 'should print helper info') + t.matchSnapshot(npm._mockOutputs, 'should print helper info') t.end() }) }) t.test('with arg but missing workspace folder', t => { + t.teardown(() => { + npm._mockOutputs.length = 0 + }) // init-package-json prints directly to console.log // this avoids poluting test output with those logs console.log = noop @@ -401,7 +405,7 @@ t.test('workspaces', t => { if (err) throw err - t.matchSnapshot(result, 'should print helper info') + t.matchSnapshot(npm._mockOutputs, 'should print helper info') t.end() }) })