From b9c3857b9f8ffcf56500e8ec8a395434400db3ef Mon Sep 17 00:00:00 2001 From: Luke Karrys Date: Sat, 27 Nov 2021 08:08:06 -0700 Subject: [PATCH] feat: streaming debug logfile This decouples the log file writing from the terminal logging. We now open an append only file at the start of the process and stream logs to it. We still only display the log file message in timing mode or if there is an error, but the file is still written regardless. All logging now goes through `proc-log` and his is the first step to removing `npmlog`. For now `npmlog` is still used for the terminal logging but with a shim in front of it to make it easier to test and use in conjunction with `proc-log`. Ref: npm/statusboard#366 This also refactors many of the tests to always use an explicit `t.testdir` for their cache since the file is opened on each `new Npm()`. Tests are also refactored to use more of `MockNpm` with behavior to add config items and load `npm` if necessary. A new fixture `mockGlobals` was also added to make much of this for ergonomic. Ref: npm/statusboard#410 Closes npm/statusboard#411 Closes npm/statusboard#367 --- .eslintrc.json | 14 +- lib/auth/legacy.js | 9 +- lib/auth/sso.js | 3 +- lib/cli.js | 33 +- lib/commands/adduser.js | 3 +- lib/commands/bin.js | 1 + lib/commands/bugs.js | 2 +- lib/commands/cache.js | 4 +- lib/commands/ci.js | 5 +- lib/commands/config.js | 11 +- lib/commands/dedupe.js | 3 +- lib/commands/diff.js | 16 +- lib/commands/dist-tag.js | 3 +- lib/commands/docs.js | 3 +- lib/commands/doctor.js | 17 +- lib/commands/exec.js | 4 +- lib/commands/explore.js | 7 +- lib/commands/fund.js | 3 +- lib/commands/init.js | 17 +- lib/commands/install.js | 6 +- lib/commands/link.js | 11 +- lib/commands/logout.js | 2 +- lib/commands/owner.js | 3 +- lib/commands/pack.js | 9 +- lib/commands/ping.js | 2 +- lib/commands/profile.js | 2 +- lib/commands/prune.js | 3 +- lib/commands/publish.js | 6 +- lib/commands/repo.js | 3 +- lib/commands/run-script.js | 2 +- lib/commands/search.js | 2 +- lib/commands/set-script.js | 2 +- lib/commands/shrinkwrap.js | 3 +- lib/commands/star.js | 3 +- lib/commands/stars.js | 3 +- lib/commands/token.js | 2 +- lib/commands/uninstall.js | 3 +- lib/commands/unpublish.js | 8 +- lib/commands/update.js | 4 +- lib/commands/view.js | 4 +- lib/npm.js | 153 +++-- lib/utils/audit-error.js | 4 +- lib/utils/cleanup-log-files.js | 35 - lib/utils/config/definitions.js | 5 +- lib/utils/deref-command.js | 2 +- lib/utils/display.js | 124 ++++ lib/utils/error-message.js | 9 +- lib/utils/exit-handler.js | 207 +++--- lib/utils/log-file.js | 245 +++++++ lib/utils/log-shim.js | 59 ++ lib/utils/proc-log-listener.js | 22 - lib/utils/pulse-till-done.js | 2 +- lib/utils/read-user-info.js | 14 +- lib/utils/reify-output.js | 2 +- lib/utils/setup-log.js | 66 -- lib/utils/tar.js | 4 +- lib/utils/timers.js | 111 ++++ lib/utils/unsupported.js | 16 +- lib/utils/update-notifier.js | 3 +- lib/utils/usage.js | 2 +- lib/utils/with-chown-sync.js | 13 + package-lock.json | 2 + package.json | 2 + .../test/lib/commands/config.js.test.cjs | 6 +- .../test/lib/commands/shrinkwrap.js.test.cjs | 70 +- .../test/lib/commands/view.js.test.cjs | 8 +- .../test/lib/utils/error-message.js.test.cjs | 83 ++- .../test/lib/utils/exit-handler.js.test.cjs | 62 +- .../test/lib/utils/log-file.js.test.cjs | 68 ++ test/coverage-map.js | 3 + test/fixtures/clean-snapshot.js | 19 + test/fixtures/mock-globals.js | 172 +++++ test/fixtures/mock-logs.js | 71 ++ test/fixtures/mock-npm.js | 190 +++--- test/fixtures/sandbox.js | 46 +- test/index.js | 4 +- test/lib/auth/legacy.js | 2 +- test/lib/auth/sso.js | 2 +- test/lib/cli.js | 182 +++--- test/lib/commands/access.js | 307 +++++---- test/lib/commands/adduser.js | 9 + test/lib/commands/audit.js | 143 ++-- test/lib/commands/birthday.js | 13 +- test/lib/commands/cache.js | 19 +- test/lib/commands/ci.js | 2 +- test/lib/commands/completion.js | 249 +++---- test/lib/commands/dedupe.js | 61 +- test/lib/commands/diff.js | 2 +- test/lib/commands/dist-tag.js | 2 +- test/lib/commands/doctor.js | 83 ++- test/lib/commands/exec.js | 30 +- test/lib/commands/explore.js | 11 +- test/lib/commands/find-dupes.js | 35 +- test/lib/commands/get.js | 6 +- test/lib/commands/init.js | 32 +- test/lib/commands/install.js | 62 +- test/lib/commands/logout.js | 131 ++-- test/lib/commands/owner.js | 22 +- test/lib/commands/pack.js | 146 ++--- test/lib/commands/ping.js | 6 +- test/lib/commands/prefix.js | 5 +- test/lib/commands/profile.js | 32 +- test/lib/commands/prune.js | 26 +- test/lib/commands/publish.js | 31 +- test/lib/commands/repo.js | 91 ++- test/lib/commands/restart.js | 34 +- test/lib/commands/root.js | 5 +- test/lib/commands/run-script.js | 12 +- test/lib/commands/set-script.js | 2 +- test/lib/commands/set.js | 1 + test/lib/commands/shrinkwrap.js | 34 +- test/lib/commands/star.js | 10 +- test/lib/commands/stars.js | 12 +- test/lib/commands/start.js | 33 +- test/lib/commands/stop.js | 30 +- test/lib/commands/test.js | 32 +- test/lib/commands/token.js | 24 +- test/lib/commands/unpublish.js | 9 +- test/lib/commands/update.js | 28 +- test/lib/commands/version.js | 504 +++++++------- test/lib/commands/view.js | 18 +- test/lib/commands/whoami.js | 20 +- test/lib/fixtures/mock-globals.js | 253 +++++++ test/lib/load-all-commands.js | 13 +- test/lib/load-all.js | 39 +- test/lib/npm.js | 528 ++++++++------- test/lib/utils/audit-error.js | 9 +- test/lib/utils/cleanup-log-files.js | 79 --- test/lib/utils/config/definitions.js | 81 +-- test/lib/utils/did-you-mean.js | 6 +- test/lib/utils/display.js | 85 +++ test/lib/utils/error-message.js | 264 +++----- test/lib/utils/exit-handler.js | 618 ++++++++++-------- test/lib/utils/is-windows-bash.js | 34 +- test/lib/utils/log-file.js | 333 ++++++++++ test/lib/utils/log-shim.js | 100 +++ test/lib/utils/npm-usage.js | 6 +- test/lib/utils/proc-log-listener.js | 41 -- test/lib/utils/pulse-till-done.js | 19 +- test/lib/utils/read-user-info.js | 30 +- test/lib/utils/reify-output.js | 7 +- test/lib/utils/setup-log.js | 296 --------- test/lib/utils/tar.js | 52 +- test/lib/utils/timers.js | 82 +++ test/lib/utils/unsupported.js | 52 +- test/lib/utils/update-notifier.js | 70 +- 146 files changed, 4504 insertions(+), 3288 deletions(-) delete mode 100644 lib/utils/cleanup-log-files.js create mode 100644 lib/utils/display.js create mode 100644 lib/utils/log-file.js create mode 100644 lib/utils/log-shim.js delete mode 100644 lib/utils/proc-log-listener.js delete mode 100644 lib/utils/setup-log.js create mode 100644 lib/utils/timers.js create mode 100644 lib/utils/with-chown-sync.js create mode 100644 tap-snapshots/test/lib/utils/log-file.js.test.cjs create mode 100644 test/fixtures/clean-snapshot.js create mode 100644 test/fixtures/mock-globals.js create mode 100644 test/fixtures/mock-logs.js create mode 100644 test/lib/fixtures/mock-globals.js delete mode 100644 test/lib/utils/cleanup-log-files.js create mode 100644 test/lib/utils/display.js create mode 100644 test/lib/utils/log-file.js create mode 100644 test/lib/utils/log-shim.js delete mode 100644 test/lib/utils/proc-log-listener.js delete mode 100644 test/lib/utils/setup-log.js create mode 100644 test/lib/utils/timers.js diff --git a/.eslintrc.json b/.eslintrc.json index b39431d2cb074..2968a2ea31901 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,15 @@ { - "extends": ["@npmcli"] + "extends": ["@npmcli"], + "overrides": [{ + "files": "test/**", + "rules": { + "no-extend-native": "off", + "no-global-assign": "off" + } + }, { + "files": ["lib/**"], + "rules": { + "no-console": "warn" + } + }] } diff --git a/lib/auth/legacy.js b/lib/auth/legacy.js index 2da82e361db40..7929ccc64d7cc 100644 --- a/lib/auth/legacy.js +++ b/lib/auth/legacy.js @@ -1,15 +1,12 @@ -const log = require('npmlog') const profile = require('npm-profile') - +const log = require('../utils/log-shim') const openUrl = require('../utils/open-url.js') const read = require('../utils/read-user-info.js') const loginPrompter = async (creds) => { - const opts = { log: log } - - creds.username = await read.username('Username:', creds.username, opts) + creds.username = await read.username('Username:', creds.username) creds.password = await read.password('Password:', creds.password) - creds.email = await read.email('Email: (this IS public) ', creds.email, opts) + creds.email = await read.email('Email: (this IS public) ', creds.email) return creds } diff --git a/lib/auth/sso.js b/lib/auth/sso.js index 6fcfc30e5d3a8..795eb8972a223 100644 --- a/lib/auth/sso.js +++ b/lib/auth/sso.js @@ -7,10 +7,9 @@ // CLI, we can remove this, and fold the lib/auth/legacy.js back into // lib/adduser.js -const log = require('npmlog') const profile = require('npm-profile') const npmFetch = require('npm-registry-fetch') - +const log = require('../utils/log-shim') const openUrl = require('../utils/open-url.js') const otplease = require('../utils/otplease.js') diff --git a/lib/cli.js b/lib/cli.js index 9dcd9d04d2ff2..3d0c32d4beda3 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -4,20 +4,23 @@ module.exports = async process => { // leak any private CLI configs to other programs process.title = 'npm' - const { checkForBrokenNode, checkForUnsupportedNode } = require('../lib/utils/unsupported.js') - + // We used to differentiate between known broken and unsupported + // versions of node and attempt to only log unsupported but still run. + // After we dropped node 10 support, we can use new features + // (like static, private, etc) which will only give vague syntax errors, + // so now both broken and unsupported use console, but only broken + // will process.exit. It is important to now perform *both* of these + // checks as early as possible so the user gets the error message. + const { checkForBrokenNode, checkForUnsupportedNode } = require('./utils/unsupported.js') checkForBrokenNode() - - const log = require('npmlog') - // pause it here so it can unpause when we've loaded the configs - // and know what loglevel we should be printing. - log.pause() - checkForUnsupportedNode() - const Npm = require('../lib/npm.js') + const exitHandler = require('./utils/exit-handler.js') + process.on('uncaughtException', exitHandler) + process.on('unhandledRejection', exitHandler) + + const Npm = require('./npm.js') const npm = new Npm() - const exitHandler = require('../lib/utils/exit-handler.js') exitHandler.setNpm(npm) // if npm is called as "npmg" or "npm_g", then @@ -26,16 +29,14 @@ module.exports = async process => { process.argv.splice(1, 1, 'npm', '-g') } - const replaceInfo = require('../lib/utils/replace-info.js') + const log = require('./utils/log-shim.js') + const replaceInfo = require('./utils/replace-info.js') log.verbose('cli', replaceInfo(process.argv)) log.info('using', 'npm@%s', npm.version) log.info('using', 'node@%s', process.version) - process.on('uncaughtException', exitHandler) - process.on('unhandledRejection', exitHandler) - - const updateNotifier = require('../lib/utils/update-notifier.js') + const updateNotifier = require('./utils/update-notifier.js') let cmd // now actually fire up npm and run the command. @@ -63,7 +64,7 @@ module.exports = async process => { } await npm.exec(cmd, npm.argv) - exitHandler() + return exitHandler() } catch (err) { if (err.code === 'EUNKNOWNCOMMAND') { const didYouMean = require('./utils/did-you-mean.js') diff --git a/lib/commands/adduser.js b/lib/commands/adduser.js index 6cd6d3001c0e1..1cf70fffbf541 100644 --- a/lib/commands/adduser.js +++ b/lib/commands/adduser.js @@ -1,4 +1,4 @@ -const log = require('npmlog') +const log = require('../utils/log-shim.js') const replaceInfo = require('../utils/replace-info.js') const BaseCommand = require('../base-command.js') const authTypes = { @@ -31,6 +31,7 @@ class AddUser extends BaseCommand { creds, registry, scope, + log, }) await this.updateConfig({ diff --git a/lib/commands/bin.js b/lib/commands/bin.js index 8f5ae0cc524e3..bb700d45a8f1b 100644 --- a/lib/commands/bin.js +++ b/lib/commands/bin.js @@ -10,6 +10,7 @@ class Bin extends BaseCommand { const b = this.npm.bin this.npm.output(b) if (this.npm.config.get('global') && !envPath.includes(b)) { + // XXX: does this need to be console? console.error('(not in PATH env variable)') } } diff --git a/lib/commands/bugs.js b/lib/commands/bugs.js index 8ca8188ccd793..5dfd1eb918959 100644 --- a/lib/commands/bugs.js +++ b/lib/commands/bugs.js @@ -1,5 +1,5 @@ -const log = require('npmlog') const pacote = require('pacote') +const log = require('../utils/log-shim') const openUrl = require('../utils/open-url.js') const hostedFromMani = require('../utils/hosted-git-info-from-manifest.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/cache.js b/lib/commands/cache.js index b1c045bbfae7f..ecb34cb8916c4 100644 --- a/lib/commands/cache.js +++ b/lib/commands/cache.js @@ -1,6 +1,5 @@ const cacache = require('cacache') const { promisify } = require('util') -const log = require('npmlog') const pacote = require('pacote') const path = require('path') const rimraf = promisify(require('rimraf')) @@ -9,6 +8,7 @@ const BaseCommand = require('../base-command.js') const npa = require('npm-package-arg') const jsonParse = require('json-parse-even-better-errors') const localeCompare = require('@isaacs/string-locale-compare')('en') +const log = require('../utils/log-shim') const searchCachePackage = async (path, spec, cacheKeys) => { const parsed = npa(spec) @@ -141,7 +141,7 @@ class Cache extends BaseCommand { try { entry = await cacache.get(cachePath, key) } catch (err) { - this.npm.log.warn(`Not Found: ${key}`) + log.warn(`Not Found: ${key}`) break } this.npm.output(`Deleted: ${key}`) diff --git a/lib/commands/ci.js b/lib/commands/ci.js index e928a01d15b54..2c2f8da866653 100644 --- a/lib/commands/ci.js +++ b/lib/commands/ci.js @@ -5,8 +5,7 @@ const reifyFinish = require('../utils/reify-finish.js') const runScript = require('@npmcli/run-script') const fs = require('fs') const readdir = util.promisify(fs.readdir) - -const log = require('npmlog') +const log = require('../utils/log-shim.js') const removeNodeModules = async where => { const rimrafOpts = { glob: false } @@ -39,7 +38,7 @@ class CI extends ArboristWorkspaceCmd { const opts = { ...this.npm.flatOptions, path: where, - log: this.npm.log, + log, save: false, // npm ci should never modify the lockfile or package.json workspaces: this.workspaceNames, } diff --git a/lib/commands/config.js b/lib/commands/config.js index 0cdcd576f527a..eb1d570c6ea25 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -11,6 +11,7 @@ const { spawn } = require('child_process') const { EOL } = require('os') const ini = require('ini') const localeCompare = require('@isaacs/string-locale-compare')('en') +const log = require('../utils/log-shim.js') // take an array of `[key, value, k2=v2, k3, v3, ...]` and turn into // { key: value, k2: v2, k3: v3 } @@ -87,12 +88,12 @@ class Config extends BaseCommand { } async execWorkspaces (args, filters) { - this.npm.log.warn('config', 'This command does not support workspaces.') + log.warn('config', 'This command does not support workspaces.') return this.exec(args) } async exec ([action, ...args]) { - this.npm.log.disableProgress() + log.disableProgress() try { switch (action) { case 'set': @@ -117,7 +118,7 @@ class Config extends BaseCommand { throw this.usageError() } } finally { - this.npm.log.enableProgress() + log.enableProgress() } } @@ -128,10 +129,10 @@ class Config extends BaseCommand { const where = this.npm.flatOptions.location for (const [key, val] of Object.entries(keyValues(args))) { - this.npm.log.info('config', 'set %j %j', key, val) + log.info('config', 'set %j %j', key, val) this.npm.config.set(key, val || '', where) if (!this.npm.config.validate(where)) { - this.npm.log.warn('config', 'omitting invalid config values') + log.warn('config', 'omitting invalid config values') } } diff --git a/lib/commands/dedupe.js b/lib/commands/dedupe.js index e1eafbe3bc58d..cc4b119d09d2a 100644 --- a/lib/commands/dedupe.js +++ b/lib/commands/dedupe.js @@ -1,6 +1,7 @@ // dedupe duplicated packages, or find them in the tree const Arborist = require('@npmcli/arborist') const reifyFinish = require('../utils/reify-finish.js') +const log = require('../utils/log-shim.js') const ArboristWorkspaceCmd = require('../arborist-cmd.js') @@ -32,7 +33,7 @@ class Dedupe extends ArboristWorkspaceCmd { const where = this.npm.prefix const opts = { ...this.npm.flatOptions, - log: this.npm.log, + log, path: where, dryRun, workspaces: this.workspaceNames, diff --git a/lib/commands/diff.js b/lib/commands/diff.js index 3134f502ea151..d737a58dc43d8 100644 --- a/lib/commands/diff.js +++ b/lib/commands/diff.js @@ -1,13 +1,11 @@ const { resolve } = require('path') - const semver = require('semver') const libnpmdiff = require('libnpmdiff') const npa = require('npm-package-arg') const Arborist = require('@npmcli/arborist') -const npmlog = require('npmlog') const pacote = require('pacote') const pickManifest = require('npm-pick-manifest') - +const log = require('../utils/log-shim') const readPackageName = require('../utils/read-package-name.js') const BaseCommand = require('../base-command.js') @@ -57,7 +55,7 @@ class Diff extends BaseCommand { } const [a, b] = await this.retrieveSpecs(specs) - npmlog.info('diff', { src: a, dst: b }) + log.info('diff', { src: a, dst: b }) const res = await libnpmdiff([a, b], { ...this.npm.flatOptions, @@ -83,7 +81,7 @@ class Diff extends BaseCommand { try { name = await readPackageName(this.prefix) } catch (e) { - npmlog.verbose('diff', 'could not read project dir package.json') + log.verbose('diff', 'could not read project dir package.json') } if (!name) { @@ -116,7 +114,7 @@ class Diff extends BaseCommand { try { pkgName = await readPackageName(this.prefix) } catch (e) { - npmlog.verbose('diff', 'could not read project dir package.json') + log.verbose('diff', 'could not read project dir package.json') noPackageJson = true } @@ -154,7 +152,7 @@ class Diff extends BaseCommand { actualTree.inventory.query('name', spec.name) .values().next().value } catch (e) { - npmlog.verbose('diff', 'failed to load actual install tree') + log.verbose('diff', 'failed to load actual install tree') } if (!node || !node.name || !node.package || !node.package.version) { @@ -227,7 +225,7 @@ class Diff extends BaseCommand { try { pkgName = await readPackageName(this.prefix) } catch (e) { - npmlog.verbose('diff', 'could not read project dir package.json') + log.verbose('diff', 'could not read project dir package.json') } if (!pkgName) { @@ -261,7 +259,7 @@ class Diff extends BaseCommand { const arb = new Arborist(opts) actualTree = await arb.loadActual(opts) } catch (e) { - npmlog.verbose('diff', 'failed to load actual install tree') + log.verbose('diff', 'failed to load actual install tree') } return specs.map(i => { diff --git a/lib/commands/dist-tag.js b/lib/commands/dist-tag.js index fa79b293c50d0..bf2dffe912030 100644 --- a/lib/commands/dist-tag.js +++ b/lib/commands/dist-tag.js @@ -1,8 +1,7 @@ -const log = require('npmlog') const npa = require('npm-package-arg') const regFetch = require('npm-registry-fetch') const semver = require('semver') - +const log = require('../utils/log-shim') const otplease = require('../utils/otplease.js') const readPackageName = require('../utils/read-package-name.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/docs.js b/lib/commands/docs.js index 9aba242057178..19cd735642262 100644 --- a/lib/commands/docs.js +++ b/lib/commands/docs.js @@ -1,8 +1,7 @@ -const log = require('npmlog') const pacote = require('pacote') const openUrl = require('../utils/open-url.js') const hostedFromMani = require('../utils/hosted-git-info-from-manifest.js') - +const log = require('../utils/log-shim') const BaseCommand = require('../base-command.js') class Docs extends BaseCommand { static description = 'Open documentation for a package in a web browser' diff --git a/lib/commands/doctor.js b/lib/commands/doctor.js index 6b8878b6f4f12..47a522eb676d0 100644 --- a/lib/commands/doctor.js +++ b/lib/commands/doctor.js @@ -8,6 +8,7 @@ const pacote = require('pacote') const { resolve } = require('path') const semver = require('semver') const { promisify } = require('util') +const log = require('../utils/log-shim.js') const ansiTrim = require('../utils/ansi-trim.js') const isWindows = require('../utils/is-windows.js') const ping = require('../utils/ping.js') @@ -42,7 +43,7 @@ class Doctor extends BaseCommand { static params = ['registry'] async exec (args) { - this.npm.log.info('Running checkup') + log.info('Running checkup') // each message is [title, ok, message] const messages = [] @@ -124,7 +125,7 @@ class Doctor extends BaseCommand { stringLength: s => ansiTrim(s).length, } - const silent = this.npm.log.levels[this.npm.log.level] > this.npm.log.levels.error + const silent = log.levels[log.level] > log.levels.error if (!silent) { this.npm.output(table(outTable, tableOpts)) if (!allOk) { @@ -137,7 +138,7 @@ class Doctor extends BaseCommand { } async checkPing () { - const tracker = this.npm.log.newItem('checkPing', 1) + const tracker = log.newItem('checkPing', 1) tracker.info('checkPing', 'Pinging registry') try { await ping(this.npm.flatOptions) @@ -154,7 +155,7 @@ class Doctor extends BaseCommand { } async getLatestNpmVersion () { - const tracker = this.npm.log.newItem('getLatestNpmVersion', 1) + const tracker = log.newItem('getLatestNpmVersion', 1) tracker.info('getLatestNpmVersion', 'Getting npm package information') try { const latest = (await pacote.manifest('npm@latest', this.npm.flatOptions)).version @@ -173,7 +174,7 @@ class Doctor extends BaseCommand { const current = process.version const currentRange = `^${current}` const url = 'https://nodejs.org/dist/index.json' - const tracker = this.npm.log.newItem('getLatestNodejsVersion', 1) + const tracker = log.newItem('getLatestNodejsVersion', 1) tracker.info('getLatestNodejsVersion', 'Getting Node.js release information') try { const res = await fetch(url, { method: 'GET', ...this.npm.flatOptions }) @@ -207,7 +208,7 @@ class Doctor extends BaseCommand { let ok = true - const tracker = this.npm.log.newItem(root, 1) + const tracker = log.newItem(root, 1) try { const uid = process.getuid() @@ -269,7 +270,7 @@ class Doctor extends BaseCommand { } async getGitPath () { - const tracker = this.npm.log.newItem('getGitPath', 1) + const tracker = log.newItem('getGitPath', 1) tracker.info('getGitPath', 'Finding git in your PATH') try { return await which('git').catch(er => { @@ -282,7 +283,7 @@ class Doctor extends BaseCommand { } async verifyCachedFiles () { - const tracker = this.npm.log.newItem('verifyCachedFiles', 1) + const tracker = log.newItem('verifyCachedFiles', 1) tracker.info('verifyCachedFiles', 'Verifying the npm cache') try { const stats = await cacache.verify(this.npm.flatOptions.cache) diff --git a/lib/commands/exec.js b/lib/commands/exec.js index 515ac910f80e4..61fb496a35086 100644 --- a/lib/commands/exec.js +++ b/lib/commands/exec.js @@ -1,6 +1,7 @@ const libexec = require('libnpmexec') const BaseCommand = require('../base-command.js') const getLocationMsg = require('../exec/get-workspace-location-msg.js') +const log = require('../utils/log-shim') // it's like this: // @@ -59,7 +60,6 @@ class Exec extends BaseCommand { const { flatOptions, localBin, - log, globalBin, } = this.npm const output = (...outputArgs) => this.npm.output(...outputArgs) @@ -77,7 +77,6 @@ class Exec extends BaseCommand { call, localBin, locationMsg, - log, globalBin, output, packages, @@ -85,6 +84,7 @@ class Exec extends BaseCommand { runPath, scriptShell, yes, + log, }) } diff --git a/lib/commands/explore.js b/lib/commands/explore.js index f94fff01c42eb..90e6af69fe57c 100644 --- a/lib/commands/explore.js +++ b/lib/commands/explore.js @@ -4,6 +4,7 @@ const rpj = require('read-package-json-fast') const runScript = require('@npmcli/run-script') const { join, resolve, relative } = require('path') +const log = require('../utils/log-shim.js') const completion = require('../utils/completion/installed-shallow.js') const BaseCommand = require('../base-command.js') @@ -37,7 +38,7 @@ class Explore extends BaseCommand { // handle all the escaping and PATH setup stuff. const pkg = await rpj(resolve(path, 'package.json')).catch(er => { - this.npm.log.error('explore', `It doesn't look like ${pkgname} is installed.`) + log.error('explore', `It doesn't look like ${pkgname} is installed.`) throw er }) @@ -50,7 +51,7 @@ class Explore extends BaseCommand { if (!args.length) { this.npm.output(`\nExploring ${path}\nType 'exit' or ^D when finished\n`) } - this.npm.log.disableProgress() + log.disableProgress() try { return await runScript({ ...this.npm.flatOptions, @@ -71,7 +72,7 @@ class Explore extends BaseCommand { } }) } finally { - this.npm.log.enableProgress() + log.enableProgress() } } } diff --git a/lib/commands/fund.js b/lib/commands/fund.js index 81c6d9a1b0726..47a51c33a6841 100644 --- a/lib/commands/fund.js +++ b/lib/commands/fund.js @@ -5,6 +5,7 @@ const pacote = require('pacote') const semver = require('semver') const npa = require('npm-package-arg') const { depth } = require('treeverse') +const log = require('../utils/log-shim.js') const { readTree: getFundingInfo, normalizeFunding, isValidFunding } = require('libnpmfund') const completion = require('../utils/completion/installed-deep.js') @@ -68,7 +69,7 @@ class Fund extends ArboristWorkspaceCmd { // TODO: add !workspacesEnabled option handling to libnpmfund const fundingInfo = getFundingInfo(tree, { ...this.flatOptions, - log: this.npm.log, + log, workspaces: this.workspaceNames, }) diff --git a/lib/commands/init.js b/lib/commands/init.js index eaca2716ee112..367533f8259f5 100644 --- a/lib/commands/init.js +++ b/lib/commands/init.js @@ -7,6 +7,7 @@ const rpj = require('read-package-json-fast') const libexec = require('libnpmexec') const mapWorkspaces = require('@npmcli/map-workspaces') const PackageJson = require('@npmcli/package-json') +const log = require('../utils/log-shim.js') const getLocationMsg = require('../exec/get-workspace-location-msg.js') const BaseCommand = require('../base-command.js') @@ -94,7 +95,6 @@ class Init extends BaseCommand { const { flatOptions, localBin, - log, globalBin, } = this.npm // this function is definitely called. But because of coverage map stuff @@ -114,7 +114,6 @@ class Init extends BaseCommand { color, localBin, locationMsg, - log, globalBin, output, path, @@ -125,8 +124,8 @@ class Init extends BaseCommand { } async template (path = process.cwd()) { - this.npm.log.pause() - this.npm.log.disableProgress() + log.pause() + log.disableProgress() const initFile = this.npm.config.get('init-module') if (!this.npm.config.get('yes') && !this.npm.config.get('force')) { @@ -147,17 +146,17 @@ class Init extends BaseCommand { // XXX promisify init-package-json await new Promise((res, rej) => { initJson(path, initFile, this.npm.config, (er, data) => { - this.npm.log.resume() - this.npm.log.enableProgress() - this.npm.log.silly('package data', data) + log.resume() + log.enableProgress() + log.silly('package data', data) if (er && er.message === 'canceled') { - this.npm.log.warn('init', 'canceled') + log.warn('init', 'canceled') return res() } if (er) { rej(er) } else { - this.npm.log.info('init', 'written successfully') + log.info('init', 'written successfully') res(data) } }) diff --git a/lib/commands/install.js b/lib/commands/install.js index 02ccb57248341..a92a5edc5ebb7 100644 --- a/lib/commands/install.js +++ b/lib/commands/install.js @@ -3,7 +3,7 @@ const fs = require('fs') const util = require('util') const readdir = util.promisify(fs.readdir) const reifyFinish = require('../utils/reify-finish.js') -const log = require('npmlog') +const log = require('../utils/log-shim.js') const { resolve, join } = require('path') const Arborist = require('@npmcli/arborist') const runScript = require('@npmcli/run-script') @@ -118,7 +118,7 @@ class Install extends ArboristWorkspaceCmd { checks.checkEngine(npmManifest, npmManifest.version, process.version) } catch (e) { if (forced) { - this.npm.log.warn( + log.warn( 'install', /* eslint-disable-next-line max-len */ `Forcing global npm install with incompatible version ${npmManifest.version} into node ${process.version}` @@ -147,7 +147,7 @@ class Install extends ArboristWorkspaceCmd { const opts = { ...this.npm.flatOptions, - log: this.npm.log, + log, auditLevel: null, path: where, add: args, diff --git a/lib/commands/link.js b/lib/commands/link.js index 8755af6f68266..e8e2c6b349aa9 100644 --- a/lib/commands/link.js +++ b/lib/commands/link.js @@ -7,6 +7,7 @@ const Arborist = require('@npmcli/arborist') const npa = require('npm-package-arg') const rpj = require('read-package-json-fast') const semver = require('semver') +const log = require('../utils/log-shim.js') const reifyFinish = require('../utils/reify-finish.js') @@ -68,7 +69,7 @@ class Link extends ArboristWorkspaceCmd { const globalOpts = { ...this.npm.flatOptions, path: globalTop, - log: this.npm.log, + log, global: true, prune: false, } @@ -117,7 +118,7 @@ class Link extends ArboristWorkspaceCmd { const localArb = new Arborist({ ...this.npm.flatOptions, prune: false, - log: this.npm.log, + log, path: this.npm.prefix, save, }) @@ -125,7 +126,7 @@ class Link extends ArboristWorkspaceCmd { ...this.npm.flatOptions, prune: false, path: this.npm.prefix, - log: this.npm.log, + log, add: names.map(l => `file:${resolve(globalTop, 'node_modules', l)}`), save, workspaces: this.workspaceNames, @@ -142,12 +143,12 @@ class Link extends ArboristWorkspaceCmd { const arb = new Arborist({ ...this.npm.flatOptions, path: globalTop, - log: this.npm.log, + log, global: true, }) await arb.reify({ add, - log: this.npm.log, + log, }) await reifyFinish(this.npm, arb) } diff --git a/lib/commands/logout.js b/lib/commands/logout.js index e17b2b879002c..4e6bab9859551 100644 --- a/lib/commands/logout.js +++ b/lib/commands/logout.js @@ -1,6 +1,6 @@ -const log = require('npmlog') const getAuth = require('npm-registry-fetch/auth.js') const npmFetch = require('npm-registry-fetch') +const log = require('../utils/log-shim') const BaseCommand = require('../base-command.js') class Logout extends BaseCommand { diff --git a/lib/commands/owner.js b/lib/commands/owner.js index 8f0b1f1eff686..c027ad6464557 100644 --- a/lib/commands/owner.js +++ b/lib/commands/owner.js @@ -1,8 +1,7 @@ -const log = require('npmlog') const npa = require('npm-package-arg') const npmFetch = require('npm-registry-fetch') const pacote = require('pacote') - +const log = require('../utils/log-shim') const otplease = require('../utils/otplease.js') const readLocalPkgName = require('../utils/read-package-name.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/pack.js b/lib/commands/pack.js index d84dde86e83ec..0719fa3b85828 100644 --- a/lib/commands/pack.js +++ b/lib/commands/pack.js @@ -1,14 +1,11 @@ const util = require('util') -const log = require('npmlog') const pacote = require('pacote') const libpack = require('libnpmpack') const npa = require('npm-package-arg') const path = require('path') - +const log = require('../utils/log-shim') const { getContents, logTar } = require('../utils/tar.js') - const writeFile = util.promisify(require('fs').writeFile) - const BaseCommand = require('../base-command.js') class Pack extends BaseCommand { @@ -70,7 +67,7 @@ class Pack extends BaseCommand { } for (const tar of tarballs) { - logTar(tar, { log, unicode }) + logTar(tar, { unicode }) this.npm.output(tar.filename.replace(/^@/, '').replace(/\//, '-')) } } @@ -82,7 +79,7 @@ class Pack extends BaseCommand { const useWorkspaces = args.length === 0 || args.includes('.') if (!useWorkspaces) { - this.npm.log.warn('Ignoring workspaces for specified package(s)') + log.warn('Ignoring workspaces for specified package(s)') return this.exec(args) } diff --git a/lib/commands/ping.js b/lib/commands/ping.js index a049d24127daf..993e029d45651 100644 --- a/lib/commands/ping.js +++ b/lib/commands/ping.js @@ -1,4 +1,4 @@ -const log = require('npmlog') +const log = require('../utils/log-shim') const pingUtil = require('../utils/ping.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/profile.js b/lib/commands/profile.js index 0939013cc2c61..e1102696e69d1 100644 --- a/lib/commands/profile.js +++ b/lib/commands/profile.js @@ -1,7 +1,7 @@ const inspect = require('util').inspect const { URL } = require('url') const ansistyles = require('ansistyles') -const log = require('npmlog') +const log = require('../utils/log-shim.js') const npmProfile = require('npm-profile') const qrcodeTerminal = require('qrcode-terminal') const Table = require('cli-table3') diff --git a/lib/commands/prune.js b/lib/commands/prune.js index 403575e024b27..5831df62859c2 100644 --- a/lib/commands/prune.js +++ b/lib/commands/prune.js @@ -1,5 +1,6 @@ // prune extraneous packages const Arborist = require('@npmcli/arborist') +const log = require('../utils/log-shim.js') const reifyFinish = require('../utils/reify-finish.js') const ArboristWorkspaceCmd = require('../arborist-cmd.js') @@ -14,7 +15,7 @@ class Prune extends ArboristWorkspaceCmd { const opts = { ...this.npm.flatOptions, path: where, - log: this.npm.log, + log, workspaces: this.workspaceNames, } const arb = new Arborist(opts) diff --git a/lib/commands/publish.js b/lib/commands/publish.js index 88ddcae7bbdf2..ad538668b63a3 100644 --- a/lib/commands/publish.js +++ b/lib/commands/publish.js @@ -1,5 +1,5 @@ const util = require('util') -const log = require('npmlog') +const log = require('../utils/log-shim.js') const semver = require('semver') const pack = require('libnpmpack') const libpub = require('libnpmpublish').publish @@ -94,10 +94,10 @@ class Publish extends BaseCommand { flatten(manifest.publishConfig, opts) } - // note that logTar calls npmlog.notice(), so if we ARE in silent mode, + // note that logTar calls log.notice(), so if we ARE in silent mode, // this will do nothing, but we still want it in the debuglog if it fails. if (!json) { - logTar(pkgContents, { log, unicode }) + logTar(pkgContents, { unicode }) } if (!dryRun) { diff --git a/lib/commands/repo.js b/lib/commands/repo.js index cc68e85650235..8ac4178f261ee 100644 --- a/lib/commands/repo.js +++ b/lib/commands/repo.js @@ -1,7 +1,6 @@ -const log = require('npmlog') const pacote = require('pacote') const { URL } = require('url') - +const log = require('../utils/log-shim') const hostedFromMani = require('../utils/hosted-git-info-from-manifest.js') const openUrl = require('../utils/open-url.js') diff --git a/lib/commands/run-script.js b/lib/commands/run-script.js index 37140c8c539c6..cd877e0b3dfa4 100644 --- a/lib/commands/run-script.js +++ b/lib/commands/run-script.js @@ -3,7 +3,7 @@ const chalk = require('chalk') const runScript = require('@npmcli/run-script') const { isServerPackage } = runScript const rpj = require('read-package-json-fast') -const log = require('npmlog') +const log = require('../utils/log-shim.js') const didYouMean = require('../utils/did-you-mean.js') const isWindowsShell = require('../utils/is-windows-shell.js') diff --git a/lib/commands/search.js b/lib/commands/search.js index ff533ebbd1c1b..bdeeffe816980 100644 --- a/lib/commands/search.js +++ b/lib/commands/search.js @@ -1,7 +1,7 @@ const Minipass = require('minipass') const Pipeline = require('minipass-pipeline') const libSearch = require('libnpmsearch') -const log = require('npmlog') +const log = require('../utils/log-shim.js') const formatPackageStream = require('../search/format-package-stream.js') const packageFilter = require('../search/package-filter.js') diff --git a/lib/commands/set-script.js b/lib/commands/set-script.js index 58fd2726db601..7c73ff01b9396 100644 --- a/lib/commands/set-script.js +++ b/lib/commands/set-script.js @@ -1,7 +1,7 @@ const { resolve } = require('path') -const log = require('npmlog') const rpj = require('read-package-json-fast') const PackageJson = require('@npmcli/package-json') +const log = require('../utils/log-shim') const BaseCommand = require('../base-command.js') class SetScript extends BaseCommand { diff --git a/lib/commands/shrinkwrap.js b/lib/commands/shrinkwrap.js index dfb3c8e381597..05e3f6d278618 100644 --- a/lib/commands/shrinkwrap.js +++ b/lib/commands/shrinkwrap.js @@ -1,8 +1,7 @@ const { resolve, basename } = require('path') const { unlink } = require('fs').promises const Arborist = require('@npmcli/arborist') -const log = require('npmlog') - +const log = require('../utils/log-shim') const BaseCommand = require('../base-command.js') class Shrinkwrap extends BaseCommand { static description = 'Lock down dependency versions for publication' diff --git a/lib/commands/star.js b/lib/commands/star.js index 1bbd25efdafb8..ec11605899437 100644 --- a/lib/commands/star.js +++ b/lib/commands/star.js @@ -1,7 +1,6 @@ const fetch = require('npm-registry-fetch') -const log = require('npmlog') const npa = require('npm-package-arg') - +const log = require('../utils/log-shim') const getIdentity = require('../utils/get-identity') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/stars.js b/lib/commands/stars.js index 1260655d07323..f45ec846dc7fe 100644 --- a/lib/commands/stars.js +++ b/lib/commands/stars.js @@ -1,6 +1,5 @@ -const log = require('npmlog') const fetch = require('npm-registry-fetch') - +const log = require('../utils/log-shim') const getIdentity = require('../utils/get-identity.js') const BaseCommand = require('../base-command.js') diff --git a/lib/commands/token.js b/lib/commands/token.js index db2374203831c..df80f1afec44e 100644 --- a/lib/commands/token.js +++ b/lib/commands/token.js @@ -1,7 +1,7 @@ const Table = require('cli-table3') const ansistyles = require('ansistyles') const { v4: isCidrV4, v6: isCidrV6 } = require('is-cidr') -const log = require('npmlog') +const log = require('../utils/log-shim.js') const profile = require('npm-profile') const otplease = require('../utils/otplease.js') diff --git a/lib/commands/uninstall.js b/lib/commands/uninstall.js index dba45e127a7a7..b40c59bda4419 100644 --- a/lib/commands/uninstall.js +++ b/lib/commands/uninstall.js @@ -1,4 +1,5 @@ const { resolve } = require('path') +const log = require('../utils/log-shim.js') const Arborist = require('@npmcli/arborist') const rpj = require('read-package-json-fast') @@ -48,7 +49,7 @@ class Uninstall extends ArboristWorkspaceCmd { const opts = { ...this.npm.flatOptions, path, - log: this.npm.log, + log, rm: args, workspaces: this.workspaceNames, } diff --git a/lib/commands/unpublish.js b/lib/commands/unpublish.js index 3636dc58a6948..578890025d224 100644 --- a/lib/commands/unpublish.js +++ b/lib/commands/unpublish.js @@ -5,7 +5,7 @@ const libaccess = require('libnpmaccess') const npmFetch = require('npm-registry-fetch') const libunpub = require('libnpmpublish').unpublish const readJson = util.promisify(require('read-package-json')) - +const log = require('../utils/log-shim') const otplease = require('../utils/otplease.js') const getIdentity = require('../utils/get-identity.js') @@ -66,8 +66,8 @@ class Unpublish extends BaseCommand { let pkgName let pkgVersion - this.npm.log.silly('unpublish', 'args[0]', args[0]) - this.npm.log.silly('unpublish', 'spec', spec) + log.silly('unpublish', 'args[0]', args[0]) + log.silly('unpublish', 'spec', spec) if ((!spec || !spec.rawSpec) && !force) { throw this.usageError( @@ -92,7 +92,7 @@ class Unpublish extends BaseCommand { } } - this.npm.log.verbose('unpublish', manifest) + log.verbose('unpublish', manifest) const { name, version, publishConfig } = manifest const pkgJsonSpec = npa.resolve(name, version) diff --git a/lib/commands/update.js b/lib/commands/update.js index 4bb74990bea20..a8bbc4c969251 100644 --- a/lib/commands/update.js +++ b/lib/commands/update.js @@ -1,7 +1,7 @@ const path = require('path') const Arborist = require('@npmcli/arborist') -const log = require('npmlog') +const log = require('../utils/log-shim.js') const reifyFinish = require('../utils/reify-finish.js') const completion = require('../utils/completion/installed-deep.js') @@ -47,7 +47,7 @@ class Update extends ArboristWorkspaceCmd { const arb = new Arborist({ ...this.npm.flatOptions, - log: this.npm.log, + log, path: where, workspaces: this.workspaceNames, }) diff --git a/lib/commands/view.js b/lib/commands/view.js index 105ebc16dfc69..4f7464ddd76c1 100644 --- a/lib/commands/view.js +++ b/lib/commands/view.js @@ -4,7 +4,7 @@ const color = require('ansicolors') const columns = require('cli-columns') const fs = require('fs') const jsonParse = require('json-parse-even-better-errors') -const log = require('npmlog') +const log = require('../utils/log-shim.js') const npa = require('npm-package-arg') const { resolve } = require('path') const formatBytes = require('../utils/format-bytes.js') @@ -139,7 +139,7 @@ class View extends BaseCommand { const local = /^\.@/.test(pkg) || pkg === '.' if (!local) { - this.npm.log.warn('Ignoring workspaces for specified package(s)') + log.warn('Ignoring workspaces for specified package(s)') return this.exec([pkg, ...args]) } let wholePackument = false diff --git a/lib/npm.js b/lib/npm.js index 0096e0ac59e06..639aa109e2974 100644 --- a/lib/npm.js +++ b/lib/npm.js @@ -1,34 +1,10 @@ const EventEmitter = require('events') const { resolve, dirname } = require('path') const Config = require('@npmcli/config') -const log = require('npmlog') // Patch the global fs module here at the app level require('graceful-fs').gracefulify(require('fs')) -// TODO make this only ever load once (or unload) in tests -const procLogListener = require('./utils/proc-log-listener.js') - -// Timers in progress -const timers = new Map() -// Finished timers -const timings = {} - -const processOnTimeHandler = name => { - timers.set(name, Date.now()) -} - -const processOnTimeEndHandler = name => { - if (timers.has(name)) { - const ms = Date.now() - timers.get(name) - log.timing(name, `Completed in ${ms}ms`) - timings[name] = ms - timers.delete(name) - } else { - log.silly('timing', "Tried to end timer that doesn't exist:", name) - } -} - const { definitions, flatten, shorthands } = require('./utils/config/index.js') const { shellouts } = require('./utils/cmd-list.js') const usage = require('./utils/npm-usage.js') @@ -36,8 +12,11 @@ const usage = require('./utils/npm-usage.js') const which = require('which') const deref = require('./utils/deref-command.js') -const setupLog = require('./utils/setup-log.js') -const cleanUpLogFiles = require('./utils/cleanup-log-files.js') +const LogFile = require('./utils/log-file.js') +const Timers = require('./utils/timers.js') +const Display = require('./utils/display.js') +const log = require('./utils/log-shim') +const replaceInfo = require('./utils/replace-info.js') let warnedNonDashArg = false const _load = Symbol('_load') @@ -50,21 +29,30 @@ class Npm extends EventEmitter { return pkg.version } + #unloaded = false + #timers = null + #logFile = null + #display = null + constructor () { super() - this.started = Date.now() this.command = null - this.timings = timings - this.timers = timers - process.on('time', processOnTimeHandler) - process.on('timeEnd', processOnTimeEndHandler) - procLogListener() - process.emit('time', 'npm') + this.#logFile = new LogFile() + this.#display = new Display() + this.#timers = new Timers({ + start: 'npm', + listener: (name, ms) => { + const args = ['timing', name, `Completed in ${ms}ms`] + this.#logFile.log(...args) + this.#display.log(...args) + }, + }) this.config = new Config({ npmPath: dirname(__dirname), definitions, flatten, shorthands, + log, }) this[_title] = process.title this.updateNotification = null @@ -117,7 +105,7 @@ class Npm extends EventEmitter { .filter(arg => /^[\u2010-\u2015\u2212\uFE58\uFE63\uFF0D]/.test(arg)) .forEach(arg => { warnedNonDashArg = true - this.log.error( + log.error( 'arg', 'Argument starts with non-ascii dash, this is probably invalid:', arg @@ -164,14 +152,13 @@ class Npm extends EventEmitter { async load () { if (!this.loadPromise) { process.emit('time', 'npm:load') - this.log.pause() this.loadPromise = new Promise((resolve, reject) => { this[_load]() .catch(er => er) .then(er => { this.loadErr = er if (!er && this.config.get('force')) { - this.log.warn('using --force', 'Recommended protections disabled.') + log.warn('using --force', 'Recommended protections disabled.') } process.emit('timeEnd', 'npm:load') @@ -189,6 +176,34 @@ class Npm extends EventEmitter { return this.config.loaded } + // This gets called at the end of the exit handler and + // during any tests to cleanup all of our listeners + // Everything in here should be synchronous + unload () { + // Track if we've already unloaded so we dont + // write multiple timing files. This is only an + // issue in tests right now since we unload + // in both tap teardowns and the exit handler + if (this.#unloaded) { + return + } + this.#timers.off() + this.#display.off() + this.#logFile.off() + if (this.loaded && this.config.get('timing')) { + this.#timers.writeFile({ + command: process.argv.slice(2), + // We used to only ever report a single log file + // so to be backwards compatible report the last logfile + // XXX: remove this in npm 9 or just keep it forever + logfile: this.logFiles[this.logFiles.length - 1], + logfiles: this.logFiles, + version: this.version, + }) + } + this.#unloaded = true + } + get title () { return this[_title] } @@ -203,12 +218,12 @@ class Npm extends EventEmitter { let node try { node = which.sync(process.argv[0]) - } catch (_) { + } catch { // TODO should we throw here? } process.emit('timeEnd', 'npm:load:whichnode') if (node && node.toUpperCase() !== process.execPath.toUpperCase()) { - this.log.verbose('node symlink', node) + log.verbose('node symlink', node) process.execPath = node this.config.execPath = node } @@ -228,19 +243,35 @@ class Npm extends EventEmitter { const tokrev = deref(this.argv[0]) === 'token' && this.argv[1] === 'revoke' this.title = tokrev ? 'npm token revoke' + (this.argv[2] ? ' ***' : '') - : ['npm', ...this.argv].join(' ') + : replaceInfo(['npm', ...this.argv].join(' ')) process.emit('timeEnd', 'npm:load:setTitle') - process.emit('time', 'npm:load:setupLog') - setupLog(this.config) - process.emit('timeEnd', 'npm:load:setupLog') + process.emit('time', 'npm:load:display') + this.#display.load({ + // Use logColor since that is based on stderr + color: this.logColor, + timing: this.config.get('timing'), + loglevel: this.config.get('loglevel'), + unicode: this.config.get('unicode'), + progress: this.config.get('progress'), + heading: this.config.get('heading'), + }) + process.emit('timeEnd', 'npm:load:display') process.env.COLOR = this.color ? '1' : '0' - process.emit('time', 'npm:load:cleanupLog') - cleanUpLogFiles(this.cache, this.config.get('logs-max'), this.log.warn) - process.emit('timeEnd', 'npm:load:cleanupLog') + process.emit('time', 'npm:load:logFile') + this.#logFile.load({ + dir: resolve(this.cache, '_logs'), + logsMax: this.config.get('logs-max'), + }) + log.verbose('logfile', this.#logFile.files[0]) + process.emit('timeEnd', 'npm:load:logFile') - this.log.resume() + process.emit('time', 'npm:load:timers') + this.#timers.load({ + dir: this.cache, + }) + process.emit('timeEnd', 'npm:load:timers') process.emit('time', 'npm:load:configScope') const configScope = this.config.get('scope') @@ -258,18 +289,35 @@ class Npm extends EventEmitter { return flat } + // color and logColor are a special derived values that takes into + // consideration not only the config, but whether or not we are operating + // in a tty with the associated output (stdout/stderr) get color () { - // This is a special derived value that takes into consideration not only - // the config, but whether or not we are operating in a tty. return this.flatOptions.color } + get logColor () { + return this.flatOptions.logColor + } + get lockfileVersion () { return 2 } - get log () { - return log + get unfinishedTimers () { + return this.#timers.unfinished + } + + get finishedTimers () { + return this.#timers.finished + } + + get started () { + return this.#timers.started + } + + get logFiles () { + return this.#logFile.files } get cache () { @@ -347,9 +395,10 @@ class Npm extends EventEmitter { // output to stdout in a progress bar compatible way output (...msg) { - this.log.clearProgress() + log.clearProgress() + // eslint-disable-next-line no-console console.log(...msg) - this.log.showProgress() + log.showProgress() } } module.exports = Npm diff --git a/lib/utils/audit-error.js b/lib/utils/audit-error.js index b4ab26fd0c697..7feccc739b6a9 100644 --- a/lib/utils/audit-error.js +++ b/lib/utils/audit-error.js @@ -1,3 +1,5 @@ +const log = require('./log-shim') + // print an error or just nothing if the audit report has an error // this is called by the audit command, and by the reify-output util // prints a JSON version of the error if it's --json @@ -15,7 +17,7 @@ const auditError = (npm, report) => { const { error } = report // ok, we care about it, then - npm.log.warn('audit', error.message) + log.warn('audit', error.message) const { body: errBody } = error const body = Buffer.isBuffer(errBody) ? errBody.toString() : errBody if (npm.flatOptions.json) { diff --git a/lib/utils/cleanup-log-files.js b/lib/utils/cleanup-log-files.js deleted file mode 100644 index 8fb0fa1550281..0000000000000 --- a/lib/utils/cleanup-log-files.js +++ /dev/null @@ -1,35 +0,0 @@ -// module to clean out the old log files in cache/_logs -// this is a best-effort attempt. if a rm fails, we just -// log a message about it and move on. We do return a -// Promise that succeeds when we've tried to delete everything, -// just for the benefit of testing this function properly. - -const { resolve } = require('path') -const rimraf = require('rimraf') -const glob = require('glob') -module.exports = (cache, max, warn) => { - return new Promise(done => { - glob(resolve(cache, '_logs', '*-debug.log'), (er, files) => { - if (er) { - return done() - } - - let pending = files.length - max - if (pending <= 0) { - return done() - } - - for (let i = 0; i < files.length - max; i++) { - rimraf(files[i], er => { - if (er) { - warn('log', 'failed to remove log file', files[i]) - } - - if (--pending === 0) { - done() - } - }) - } - }) - }) -} diff --git a/lib/utils/config/definitions.js b/lib/utils/config/definitions.js index e941362861fbd..4ba801eef5450 100644 --- a/lib/utils/config/definitions.js +++ b/lib/utils/config/definitions.js @@ -472,7 +472,10 @@ define('color', { flatten (key, obj, flatOptions) { flatOptions.color = !obj.color ? false : obj.color === 'always' ? true - : process.stdout.isTTY + : !!process.stdout.isTTY + flatOptions.logColor = !obj.color ? false + : obj.color === 'always' ? true + : !!process.stderr.isTTY }, }) diff --git a/lib/utils/deref-command.js b/lib/utils/deref-command.js index dd89fb5a4f2b2..0a3c8c90bc903 100644 --- a/lib/utils/deref-command.js +++ b/lib/utils/deref-command.js @@ -1,6 +1,6 @@ // de-reference abbreviations and shorthands into canonical command name -const { aliases, cmdList, plumbing } = require('../utils/cmd-list.js') +const { aliases, cmdList, plumbing } = require('./cmd-list.js') const aliasNames = Object.keys(aliases) const fullList = cmdList.concat(aliasNames).filter(c => !plumbing.includes(c)) const abbrev = require('abbrev') diff --git a/lib/utils/display.js b/lib/utils/display.js new file mode 100644 index 0000000000000..83071a6ea4aa2 --- /dev/null +++ b/lib/utils/display.js @@ -0,0 +1,124 @@ +const { inspect } = require('util') +const npmlog = require('npmlog') +const log = require('./log-shim.js') +const { explain } = require('./explain-eresolve.js') + +const _logHandler = Symbol('logHandler') +const _eresolveWarn = Symbol('eresolveWarn') +const _log = Symbol('log') +const _npmlog = Symbol('npmlog') + +class Display { + constructor () { + // pause by default until config is loaded + this.on() + log.pause() + } + + on () { + process.on('log', this[_logHandler]) + } + + off () { + process.off('log', this[_logHandler]) + // Unbalanced calls to enable/disable progress + // will leave change listeners on the tracker + // This pretty much only happens in tests but + // this removes the event emitter listener warnings + log.tracker.removeAllListeners() + } + + load (config) { + const { + color, + timing, + loglevel, + unicode, + progress, + heading = 'npm', + } = config + + // XXX: decouple timing from loglevel + if (timing && loglevel === 'notice') { + log.level = 'timing' + } else { + log.level = loglevel + } + + log.heading = heading + + if (color) { + log.enableColor() + } else { + log.disableColor() + } + + if (unicode) { + log.enableUnicode() + } else { + log.disableUnicode() + } + + // Progress is on stderr so see if term supports that + const stderrTTY = process.stderr.isTTY + const dumbTerm = process.env.TERM === 'dumb' + const stderrNotDumb = stderrTTY && !dumbTerm + // if it's more than error, don't show progress + const quiet = log.levels[log.level] > log.levels.error + + if (progress && stderrNotDumb && !quiet) { + log.enableProgress() + } else { + log.disableProgress() + } + + // Resume displaying logs now that we have config + log.resume() + } + + log (...args) { + this[_logHandler](...args) + } + + [_logHandler] = (level, ...args) => { + try { + this[_log](level, ...args) + } catch (ex) { + try { + // if it crashed once, it might again! + this[_npmlog]('verbose', `attempt to log ${inspect(args)} crashed`, ex) + } catch (ex2) { + // eslint-disable-next-line no-console + console.error(`attempt to log ${inspect(args)} crashed`, ex, ex2) + } + } + } + + [_log] (...args) { + return this[_eresolveWarn](...args) || this[_npmlog](...args) + } + + // Explicitly call these on npmlog and not log shim + // This is the final place we should call npmlog before removing it. + [_npmlog] (level, ...args) { + npmlog[level](...args) + } + + // Also (and this is a really inexcusable kludge), we patch the + // log.warn() method so that when we see a peerDep override + // explanation from Arborist, we can replace the object with a + // highly abbreviated explanation of what's being overridden. + [_eresolveWarn] (level, heading, message, expl) { + if (level === 'warn' && + heading === 'ERESOLVE' && + expl && typeof expl === 'object' + ) { + this[_npmlog](level, heading, message) + this[_npmlog](level, '', explain(expl, log.useColor(), 2)) + // Return true to short circuit other log in chain + return true + } + } +} + +module.exports = Display diff --git a/lib/utils/error-message.js b/lib/utils/error-message.js index 48ad4676f471e..4d584346d0f3b 100644 --- a/lib/utils/error-message.js +++ b/lib/utils/error-message.js @@ -1,9 +1,9 @@ const { format } = require('util') const { resolve } = require('path') const nameValidator = require('validate-npm-package-name') -const npmlog = require('npmlog') const replaceInfo = require('./replace-info.js') const { report } = require('./explain-eresolve.js') +const log = require('./log-shim') module.exports = (er, npm) => { const short = [] @@ -20,7 +20,10 @@ module.exports = (er, npm) => { case 'ERESOLVE': short.push(['ERESOLVE', er.message]) detail.push(['', '']) - detail.push(['', report(er, npm.color, resolve(npm.cache, 'eresolve-report.txt'))]) + // XXX(display): error messages are logged so we use the logColor since that is based + // on stderr. This should be handled solely by the display layer so it could also be + // printed to stdout if necessary. + detail.push(['', report(er, !!npm.logColor, resolve(npm.cache, 'eresolve-report.txt'))]) break case 'ENOLOCK': { @@ -61,7 +64,7 @@ module.exports = (er, npm) => { if (!isWindows && (isCachePath || isCacheDest)) { // user probably doesn't need this, but still add it to the debug log - npmlog.verbose(er.stack) + log.verbose(er.stack) short.push([ '', [ diff --git a/lib/utils/exit-handler.js b/lib/utils/exit-handler.js index 5b2811468eca3..32434662422ae 100644 --- a/lib/utils/exit-handler.js +++ b/lib/utils/exit-handler.js @@ -1,119 +1,108 @@ const os = require('os') -const path = require('path') -const writeFileAtomic = require('write-file-atomic') -const mkdirp = require('mkdirp-infer-owner') -const fs = require('graceful-fs') +const log = require('./log-shim.js') const errorMessage = require('./error-message.js') const replaceInfo = require('./replace-info.js') -let exitHandlerCalled = false -let logFileName -let npm // set by the cli -let wroteLogFile = false - -const getLogFile = () => { - // we call this multiple times, so we need to treat it as a singleton because - // the date is part of the name - if (!logFileName) { - logFileName = path.resolve( - npm.config.get('cache'), - '_logs', - new Date().toISOString().replace(/[.:]/g, '_') + '-debug.log' - ) - } +const messageText = msg => msg.map(line => line.slice(1).join(' ')).join('\n') - return logFileName -} +let npm = null // set by the cli +let exitHandlerCalled = false +let showLogFileMessage = false process.on('exit', code => { + log.disableProgress() + // process.emit is synchronous, so the timeEnd handler will run before the // unfinished timer check below process.emit('timeEnd', 'npm') - npm.log.disableProgress() - for (const [name, timers] of npm.timers) { - npm.log.verbose('unfinished npm timer', name, timers) - } - if (npm.config.loaded && npm.config.get('timing')) { - try { - const file = path.resolve(npm.config.get('cache'), '_timing.json') - const dir = path.dirname(npm.config.get('cache')) - mkdirp.sync(dir) - - fs.appendFileSync( - file, - JSON.stringify({ - command: process.argv.slice(2), - logfile: getLogFile(), - version: npm.version, - ...npm.timings, - }) + '\n' - ) - - const st = fs.lstatSync(path.dirname(npm.config.get('cache'))) - fs.chownSync(dir, st.uid, st.gid) - fs.chownSync(file, st.uid, st.gid) - } catch (ex) { - // ignore + const hasNpm = !!npm + const hasLoadedNpm = hasNpm && npm.config.loaded + + // Unfinished timers can be read before config load + if (hasNpm) { + for (const [name, timer] of npm.unfinishedTimers) { + log.verbose('unfinished npm timer', name, timer) } } if (!code) { - npm.log.info('ok') + log.info('ok') } else { - npm.log.verbose('code', code) + log.verbose('code', code) } if (!exitHandlerCalled) { process.exitCode = code || 1 - npm.log.error('', 'Exit handler never called!') + log.error('', 'Exit handler never called!') console.error('') - npm.log.error('', 'This is an error with npm itself. Please report this error at:') - npm.log.error('', ' ') - // TODO this doesn't have an npm.config.loaded guard - writeLogFile() + log.error('', 'This is an error with npm itself. Please report this error at:') + log.error('', ' ') + showLogFileMessage = true } - // In timing mode we always write the log file - if (npm.config.loaded && npm.config.get('timing') && !wroteLogFile) { - writeLogFile() + + // In timing mode we always show the log file message + if (hasLoadedNpm && npm.config.get('timing')) { + showLogFileMessage = true } - if (wroteLogFile) { + + // npm must be loaded to know where the log file was written + if (showLogFileMessage && hasLoadedNpm) { // just a line break - if (npm.log.levels[npm.log.level] <= npm.log.levels.error) { + if (log.levels[log.level] <= log.levels.error) { console.error('') } - npm.log.error( + log.error( '', - ['A complete log of this run can be found in:', ' ' + getLogFile()].join('\n') + [ + 'A complete log of this run can be found in:', + ...npm.logFiles.map(f => ' ' + f), + ].join('\n') ) } + // This removes any listeners npm setup and writes files if necessary + // This is mostly used for tests to avoid max listener warnings + if (hasLoadedNpm) { + npm.unload() + } + // these are needed for the tests to have a clean slate in each test case exitHandlerCalled = false - wroteLogFile = false + showLogFileMessage = false }) const exitHandler = err => { - npm.log.disableProgress() - if (!npm.config.loaded) { + exitHandlerCalled = true + + log.disableProgress() + + const hasNpm = !!npm + const hasLoadedNpm = hasNpm && npm.config.loaded + + if (!hasNpm) { + err = err || new Error('Exit prior to setting npm in exit handler') + console.error(err.stack || err.message) + return process.exit(1) + } + + if (!hasLoadedNpm) { err = err || new Error('Exit prior to config file resolving.') console.error(err.stack || err.message) } // only show the notification if it finished. if (typeof npm.updateNotification === 'string') { - const { level } = npm.log - npm.log.level = 'notice' - npm.log.notice('', npm.updateNotification) - npm.log.level = level + const { level } = log + log.level = 'notice' + log.notice('', npm.updateNotification) + log.level = level } - exitHandlerCalled = true - let exitCode - let noLog + let noLogMessage if (err) { exitCode = 1 @@ -125,13 +114,13 @@ const exitHandler = err => { const quietShellout = isShellout && typeof err.code === 'number' && err.code if (quietShellout) { exitCode = err.code - noLog = true + noLogMessage = true } else if (typeof err === 'string') { - noLog = true - npm.log.error('', err) + log.error('', err) + noLogMessage = true } else if (!(err instanceof Error)) { - noLog = true - npm.log.error('weird error', err) + log.error('weird error', err) + noLogMessage = true } else { if (!err.code) { const matchErrorCode = err.message.match(/^(?:Error: )?(E[A-Z]+)/) @@ -141,31 +130,30 @@ const exitHandler = err => { for (const k of ['type', 'stack', 'statusCode', 'pkgid']) { const v = err[k] if (v) { - npm.log.verbose(k, replaceInfo(v)) + log.verbose(k, replaceInfo(v)) } } - npm.log.verbose('cwd', process.cwd()) - const args = replaceInfo(process.argv) - npm.log.verbose('', os.type() + ' ' + os.release()) - npm.log.verbose('argv', args.map(JSON.stringify).join(' ')) - npm.log.verbose('node', process.version) - npm.log.verbose('npm ', 'v' + npm.version) + log.verbose('cwd', process.cwd()) + log.verbose('', os.type() + ' ' + os.release()) + log.verbose('argv', args.map(JSON.stringify).join(' ')) + log.verbose('node', process.version) + log.verbose('npm ', 'v' + npm.version) for (const k of ['code', 'syscall', 'file', 'path', 'dest', 'errno']) { const v = err[k] if (v) { - npm.log.error(k, v) + log.error(k, v) } } const msg = errorMessage(err, npm) for (const errline of [...msg.summary, ...msg.detail]) { - npm.log.error(...errline) + log.error(...errline) } - if (npm.config.loaded && npm.config.get('json')) { + if (hasLoadedNpm && npm.config.get('json')) { const error = { error: { code: err.code, @@ -183,17 +171,12 @@ const exitHandler = err => { } } } - npm.log.verbose('exit', exitCode || 0) - if (npm.log.level === 'silent') { - noLog = true - } + log.verbose('exit', exitCode || 0) - // noLog is true if there was an error, including if config wasn't loaded, so - // this doesn't need a config.loaded guard - if (exitCode && !noLog) { - writeLogFile() - } + showLogFileMessage = log.level === 'silent' || noLogMessage + ? false + : !!exitCode // explicitly call process.exit now so we don't hang on things like the // update notifier, also flush stdout beforehand because process.exit doesn't @@ -201,42 +184,6 @@ const exitHandler = err => { process.stdout.write('', () => process.exit(exitCode)) } -const messageText = msg => msg.map(line => line.slice(1).join(' ')).join('\n') - -const writeLogFile = () => { - try { - let logOutput = '' - npm.log.record.forEach(m => { - const p = [m.id, m.level] - if (m.prefix) { - p.push(m.prefix) - } - const pref = p.join(' ') - - m.message - .trim() - .split(/\r?\n/) - .map(line => (pref + ' ' + line).trim()) - .forEach(line => { - logOutput += line + os.EOL - }) - }) - - const file = getLogFile() - const dir = path.dirname(file) - mkdirp.sync(dir) - writeFileAtomic.sync(file, logOutput) - - const st = fs.lstatSync(path.dirname(npm.config.get('cache'))) - fs.chownSync(dir, st.uid, st.gid) - fs.chownSync(file, st.uid, st.gid) - - // truncate once it's been written. - npm.log.record.length = 0 - wroteLogFile = true - } catch (ex) {} -} - module.exports = exitHandler module.exports.setNpm = n => { npm = n diff --git a/lib/utils/log-file.js b/lib/utils/log-file.js new file mode 100644 index 0000000000000..b37fd23e079c0 --- /dev/null +++ b/lib/utils/log-file.js @@ -0,0 +1,245 @@ +const os = require('os') +const path = require('path') +const { format, promisify } = require('util') +const rimraf = promisify(require('rimraf')) +const glob = promisify(require('glob')) +const MiniPass = require('minipass') +const fsMiniPass = require('fs-minipass') +const log = require('./log-shim') +const withChownSync = require('./with-chown-sync') + +const _logHandler = Symbol('logHandler') +const _formatLogItem = Symbol('formatLogItem') +const _getLogFilePath = Symbol('getLogFilePath') +const _openLogFile = Symbol('openLogFile') +const _cleanLogs = Symbol('cleanlogs') +const _endStream = Symbol('endStream') +const _isBuffered = Symbol('isBuffered') + +class LogFiles { + // If we write multiple log files we want them all to have the same + // identifier for sorting and matching purposes + #logId = null + + // Default to a plain minipass stream so we can buffer + // initial writes before we know the cache location + #logStream = null + + // We cap log files at a certain number of log events per file. + // Note that each log event can write more than one line to the + // file. Then we rotate log files once this number of events is reached + #MAX_LOGS_PER_FILE = null + + // Now that we write logs continuously we need to have a backstop + // here for infinite loops that still log. This is also partially handled + // by the config.get('max-files') option, but this is a failsafe to + // prevent runaway log file creation + #MAX_LOG_FILES_PER_PROCESS = null + + #fileLogCount = 0 + #totalLogCount = 0 + #dir = null + #logsMax = null + #files = [] + + constructor ({ + maxLogsPerFile = 50_000, + maxFilesPerProcess = 5, + } = {}) { + this.#logId = LogFiles.logId(new Date()) + this.#MAX_LOGS_PER_FILE = maxLogsPerFile + this.#MAX_LOG_FILES_PER_PROCESS = maxFilesPerProcess + this.on() + } + + static logId (d) { + return d.toISOString().replace(/[.:]/g, '_') + } + + static fileName (prefix, suffix) { + return `${prefix}-debug-${suffix}.log` + } + + static format (count, level, title, ...args) { + let prefix = `${count} ${level}` + if (title) { + prefix += ` ${title}` + } + + return format(...args) + .split(/\r?\n/) + .reduce((lines, line) => + lines += prefix + (line ? ' ' : '') + line + os.EOL, + '' + ) + } + + on () { + this.#logStream = new MiniPass() + process.on('log', this[_logHandler]) + } + + off () { + process.off('log', this[_logHandler]) + this[_endStream]() + } + + load ({ dir, logsMax } = {}) { + this.#dir = dir + this.#logsMax = logsMax + + // Log stream has already ended + if (!this.#logStream) { + return + } + // Pipe our initial stream to our new file stream and + // set that as the new log logstream for future writes + const initialFile = this[_openLogFile]() + if (initialFile) { + this.#logStream = this.#logStream.pipe(initialFile) + } + + // Kickoff cleaning process. This is async but it wont delete + // our next log file since it deletes oldest first. Return the + // result so it can be awaited in tests + return this[_cleanLogs]() + } + + log (...args) { + this[_logHandler](...args) + } + + get files () { + return this.#files + } + + get [_isBuffered] () { + return this.#logStream instanceof MiniPass + } + + [_endStream] (output) { + if (this.#logStream) { + this.#logStream.end(output) + this.#logStream = null + } + } + + [_logHandler] = (level, ...args) => { + // Ignore pause and resume events since we + // write everything to the log file + if (level === 'pause' || level === 'resume') { + return + } + + // If the stream is ended then do nothing + if (!this.#logStream) { + return + } + + const logOutput = this[_formatLogItem](level, ...args) + + if (this[_isBuffered]) { + // Cant do anything but buffer the output if we dont + // have a file stream yet + this.#logStream.write(logOutput) + return + } + + // Open a new log file if we've written too many logs to this one + if (this.#fileLogCount >= this.#MAX_LOGS_PER_FILE) { + // Write last chunk to the file and close it + this[_endStream](logOutput) + if (this.#files.length >= this.#MAX_LOG_FILES_PER_PROCESS) { + // but if its way too many then we just stop listening + this.off() + } else { + // otherwise we are ready for a new file for the next event + this.#logStream = this[_openLogFile]() + } + } else { + this.#logStream.write(logOutput) + } + } + + [_formatLogItem] (...args) { + this.#fileLogCount += 1 + return LogFiles.format(this.#totalLogCount++, ...args) + } + + [_getLogFilePath] (prefix, suffix) { + return path.resolve(this.#dir, LogFiles.fileName(prefix, suffix)) + } + + [_openLogFile] () { + // Count in filename will be 0 indexed + const count = this.#files.length + + // Pad with zeros so that our log files are always sorted properly + // We never want to write files ending in `-9.log` and `-10.log` because + // log file cleaning is done by deleting the oldest so in this example + // `-10.log` would be deleted next + const countDigits = this.#MAX_LOG_FILES_PER_PROCESS.toString().length + + try { + const logStream = withChownSync( + this[_getLogFilePath](this.#logId, count.toString().padStart(countDigits, '0')), + // Some effort was made to make the async, but we need to write logs + // during process.on('exit') which has to be synchronous. So in order + // to never drop log messages, it is easiest to make it sync all the time + // and this was measured to be about 1.5% slower for 40k lines of output + (f) => new fsMiniPass.WriteStreamSync(f, { flags: 'a' }) + ) + if (count > 0) { + // Reset file log count if we are opening + // after our first file + this.#fileLogCount = 0 + } + this.#files.push(logStream.path) + return logStream + } catch (e) { + // XXX: do something here for errors? + // log to display only? + return null + } + } + + async [_cleanLogs] () { + // module to clean out the old log files + // this is a best-effort attempt. if a rm fails, we just + // log a message about it and move on. We do return a + // Promise that succeeds when we've tried to delete everything, + // just for the benefit of testing this function properly. + + if (typeof this.#logsMax !== 'number') { + return + } + + // Add 1 to account for the current log file and make + // minimum config 0 so current log file is never deleted + // XXX: we should make a separate documented option to + // disable log file writing + const max = Math.max(this.#logsMax, 0) + 1 + try { + const files = await glob(this[_getLogFilePath]('*', '*')) + const toDelete = files.length - max + + if (toDelete <= 0) { + return + } + + log.silly('logfile', `start cleaning logs, removing ${toDelete} files`) + + for (const file of files.slice(0, toDelete)) { + try { + await rimraf(file) + } catch (e) { + log.warn('logfile', 'error removing log file', file, e) + } + } + } catch (e) { + log.warn('logfile', 'error cleaning log files', e) + } + } +} + +module.exports = LogFiles diff --git a/lib/utils/log-shim.js b/lib/utils/log-shim.js new file mode 100644 index 0000000000000..9d5a36d967413 --- /dev/null +++ b/lib/utils/log-shim.js @@ -0,0 +1,59 @@ +const NPMLOG = require('npmlog') +const PROCLOG = require('proc-log') + +// Sets getter and optionally a setter +// otherwise setting should throw +const accessors = (obj, set) => (k) => ({ + get: () => obj[k], + set: set ? (v) => (obj[k] = v) : () => { + throw new Error(`Cant set ${k}`) + }, +}) + +// Set the value to a bound function on the object +const value = (obj) => (k) => ({ + value: (...args) => obj[k].apply(obj, args), +}) + +const properties = { + // npmlog getters/setters + level: accessors(NPMLOG, true), + heading: accessors(NPMLOG, true), + levels: accessors(NPMLOG), + gauge: accessors(NPMLOG), + stream: accessors(NPMLOG), + tracker: accessors(NPMLOG), + progressEnabled: accessors(NPMLOG), + // npmlog methods + useColor: value(NPMLOG), + enableColor: value(NPMLOG), + disableColor: value(NPMLOG), + enableUnicode: value(NPMLOG), + disableUnicode: value(NPMLOG), + enableProgress: value(NPMLOG), + disableProgress: value(NPMLOG), + clearProgress: value(NPMLOG), + showProgress: value(NPMLOG), + newItem: value(NPMLOG), + newGroup: value(NPMLOG), + // proclog methods + notice: value(PROCLOG), + error: value(PROCLOG), + warn: value(PROCLOG), + info: value(PROCLOG), + verbose: value(PROCLOG), + http: value(PROCLOG), + silly: value(PROCLOG), + pause: value(PROCLOG), + resume: value(PROCLOG), +} + +const descriptors = Object.entries(properties).reduce((acc, [k, v]) => { + acc[k] = { enumerable: true, ...v(k) } + return acc +}, {}) + +// Create an object with the allowed properties rom npm log and all +// the logging methods from proc log +// XXX: this should go away and requires of this should be replaced with proc-log + new display +module.exports = Object.freeze(Object.defineProperties({}, descriptors)) diff --git a/lib/utils/proc-log-listener.js b/lib/utils/proc-log-listener.js deleted file mode 100644 index 2cfe94ecb0cf2..0000000000000 --- a/lib/utils/proc-log-listener.js +++ /dev/null @@ -1,22 +0,0 @@ -const log = require('npmlog') -const { inspect } = require('util') -module.exports = () => { - process.on('log', (level, ...args) => { - try { - log[level](...args) - } catch (ex) { - try { - // if it crashed once, it might again! - log.verbose(`attempt to log ${inspect([level, ...args])} crashed`, ex) - } catch (ex2) { - console.error(`attempt to log ${inspect([level, ...args])} crashed`, ex) - } - } - }) -} - -// for tests -/* istanbul ignore next */ -module.exports.reset = () => { - process.removeAllListeners('log') -} diff --git a/lib/utils/pulse-till-done.js b/lib/utils/pulse-till-done.js index a88b8aacd862b..2229414147483 100644 --- a/lib/utils/pulse-till-done.js +++ b/lib/utils/pulse-till-done.js @@ -1,4 +1,4 @@ -const log = require('npmlog') +const log = require('./log-shim.js') let pulseTimer = null const withPromise = async (promise) => { diff --git a/lib/utils/read-user-info.js b/lib/utils/read-user-info.js index 993aa886f6b4c..ac24396c6abb9 100644 --- a/lib/utils/read-user-info.js +++ b/lib/utils/read-user-info.js @@ -1,7 +1,7 @@ const { promisify } = require('util') const readAsync = promisify(require('read')) const userValidate = require('npm-user-validate') -const log = require('npmlog') +const log = require('./log-shim.js') exports.otp = readOTP exports.password = readPassword @@ -40,30 +40,30 @@ function readPassword (msg = passwordPrompt, password, isRetry) { .then((password) => readPassword(msg, password, true)) } -function readUsername (msg = usernamePrompt, username, opts = {}, isRetry) { +function readUsername (msg = usernamePrompt, username, isRetry) { if (isRetry && username) { const error = userValidate.username(username) if (error) { - opts.log && opts.log.warn(error.message) + log.warn(error.message) } else { return Promise.resolve(username.trim()) } } return read({ prompt: msg, default: username || '' }) - .then((username) => readUsername(msg, username, opts, true)) + .then((username) => readUsername(msg, username, true)) } -function readEmail (msg = emailPrompt, email, opts = {}, isRetry) { +function readEmail (msg = emailPrompt, email, isRetry) { if (isRetry && email) { const error = userValidate.email(email) if (error) { - opts.log && opts.log.warn(error.message) + log.warn(error.message) } else { return email.trim() } } return read({ prompt: msg, default: email || '' }) - .then((username) => readEmail(msg, username, opts, true)) + .then((username) => readEmail(msg, username, true)) } diff --git a/lib/utils/reify-output.js b/lib/utils/reify-output.js index 7741b72200dd8..b4114c1b2fe04 100644 --- a/lib/utils/reify-output.js +++ b/lib/utils/reify-output.js @@ -9,7 +9,7 @@ // found 37 vulnerabilities (5 low, 7 moderate, 25 high) // run `npm audit fix` to fix them, or `npm audit` for details -const log = require('npmlog') +const log = require('./log-shim.js') const { depth } = require('treeverse') const ms = require('ms') const auditReport = require('npm-audit-report') diff --git a/lib/utils/setup-log.js b/lib/utils/setup-log.js deleted file mode 100644 index 05ca38c828240..0000000000000 --- a/lib/utils/setup-log.js +++ /dev/null @@ -1,66 +0,0 @@ -// module to set the appropriate log settings based on configs -// returns a boolean to say whether we should enable color on -// stdout or not. -// -// Also (and this is a really inexcusable kludge), we patch the -// log.warn() method so that when we see a peerDep override -// explanation from Arborist, we can replace the object with a -// highly abbreviated explanation of what's being overridden. -const log = require('npmlog') -const { explain } = require('./explain-eresolve.js') - -module.exports = (config) => { - const color = config.get('color') - - const { warn } = log - - const stdoutTTY = process.stdout.isTTY - const stderrTTY = process.stderr.isTTY - const dumbTerm = process.env.TERM === 'dumb' - const stderrNotDumb = stderrTTY && !dumbTerm - // this logic is duplicated in the config 'color' flattener - const enableColorStderr = color === 'always' ? true - : color === false ? false - : stderrTTY - - const enableColorStdout = color === 'always' ? true - : color === false ? false - : stdoutTTY - - log.warn = (heading, ...args) => { - if (heading === 'ERESOLVE' && args[1] && typeof args[1] === 'object') { - warn(heading, args[0]) - return warn('', explain(args[1], enableColorStdout, 2)) - } - return warn(heading, ...args) - } - - if (config.get('timing') && config.get('loglevel') === 'notice') { - log.level = 'timing' - } else { - log.level = config.get('loglevel') - } - - log.heading = config.get('heading') || 'npm' - - if (enableColorStderr) { - log.enableColor() - } else { - log.disableColor() - } - - if (config.get('unicode')) { - log.enableUnicode() - } else { - log.disableUnicode() - } - - // if it's more than error, don't show progress - const quiet = log.levels[log.level] > log.levels.error - - if (config.get('progress') && stderrNotDumb && !quiet) { - log.enableProgress() - } else { - log.disableProgress() - } -} diff --git a/lib/utils/tar.js b/lib/utils/tar.js index 26e7a98df6b49..2f2773c6d49bc 100644 --- a/lib/utils/tar.js +++ b/lib/utils/tar.js @@ -1,6 +1,6 @@ const tar = require('tar') const ssri = require('ssri') -const npmlog = require('npmlog') +const log = require('./log-shim') const formatBytes = require('./format-bytes.js') const columnify = require('columnify') const localeCompare = require('@isaacs/string-locale-compare')('en', { @@ -9,7 +9,7 @@ const localeCompare = require('@isaacs/string-locale-compare')('en', { }) const logTar = (tarball, opts = {}) => { - const { unicode = false, log = npmlog } = opts + const { unicode = false } = opts log.notice('') log.notice('', `${unicode ? '📦 ' : 'package:'} ${tarball.name}@${tarball.version}`) log.notice('=== Tarball Contents ===') diff --git a/lib/utils/timers.js b/lib/utils/timers.js new file mode 100644 index 0000000000000..acff29eb0521b --- /dev/null +++ b/lib/utils/timers.js @@ -0,0 +1,111 @@ +const EE = require('events') +const path = require('path') +const fs = require('graceful-fs') +const log = require('./log-shim') +const withChownSync = require('./with-chown-sync.js') + +const _timeListener = Symbol('timeListener') +const _timeEndListener = Symbol('timeEndListener') +const _init = Symbol('init') + +// This is an event emiiter but on/off +// only listen on a single internal event that gets +// emitted whenever a timer ends +class Timers extends EE { + #unfinished = new Map() + #finished = {} + #onTimeEnd = Symbol('onTimeEnd') + #dir = null + #initialListener = null + #initialTimer = null + + constructor ({ listener = null, start = 'npm' } = {}) { + super() + this.#initialListener = listener + this.#initialTimer = start + this[_init]() + } + + get unfinished () { + return this.#unfinished + } + + get finished () { + return this.#finished + } + + [_init] () { + this.on() + if (this.#initialListener) { + this.on(this.#initialListener) + } + process.emit('time', this.#initialTimer) + this.started = this.#unfinished.get(this.#initialTimer) + } + + on (listener) { + if (listener) { + super.on(this.#onTimeEnd, listener) + } else { + process.on('time', this[_timeListener]) + process.on('timeEnd', this[_timeEndListener]) + } + } + + off (listener) { + if (listener) { + super.off(this.#onTimeEnd, listener) + } else { + this.removeAllListeners(this.#onTimeEnd) + process.off('time', this[_timeListener]) + process.off('timeEnd', this[_timeEndListener]) + } + } + + load ({ dir }) { + this.#dir = dir + } + + writeFile (fileData) { + try { + const globalStart = this.started + const globalEnd = this.#finished.npm || Date.now() + const content = { + ...fileData, + ...this.#finished, + // add any unfinished timers with their relative start/end + unfinished: [...this.#unfinished.entries()].reduce((acc, [name, start]) => { + acc[name] = [start - globalStart, globalEnd - globalStart] + return acc + }, {}), + } + withChownSync( + path.resolve(this.#dir, '_timing.json'), + (f) => + // we append line delimited json to this file...forever + // XXX: should we also write a process specific timing file? + // with similar rules to the debug log (max files, etc) + fs.appendFileSync(f, JSON.stringify(content) + '\n') + ) + } catch (e) { + log.warn('timing', 'could not write timing file', e) + } + } + + [_timeListener] = (name) => { + this.#unfinished.set(name, Date.now()) + } + + [_timeEndListener] = (name) => { + if (this.#unfinished.has(name)) { + const ms = Date.now() - this.#unfinished.get(name) + this.#finished[name] = ms + this.#unfinished.delete(name) + this.emit(this.#onTimeEnd, name, ms) + } else { + log.silly('timing', "Tried to end timer that doesn't exist:", name) + } + } +} + +module.exports = Timers diff --git a/lib/utils/unsupported.js b/lib/utils/unsupported.js index 5f6a341a83d20..75aad5e780ec4 100644 --- a/lib/utils/unsupported.js +++ b/lib/utils/unsupported.js @@ -1,7 +1,14 @@ +/* eslint-disable no-console */ const semver = require('semver') const supported = require('../../package.json').engines.node const knownBroken = '<6.2.0 || 9 <9.3.0' +// Keep this file compatible with all practical versions of node +// so we dont get syntax errors when trying to give the users +// a nice error message. Don't use our log handler because +// if we encounter a syntax error early on, that will never +// get displayed to the user. + const checkVersion = exports.checkVersion = version => { const versionNoPrerelease = version.replace(/-.*$/, '') return { @@ -24,10 +31,9 @@ exports.checkForBrokenNode = () => { exports.checkForUnsupportedNode = () => { const nodejs = checkVersion(process.version) if (nodejs.unsupported) { - const log = require('npmlog') - log.warn('npm', 'npm does not support Node.js ' + process.version) - log.warn('npm', 'You should probably upgrade to a newer version of node as we') - log.warn('npm', "can't make any promises that npm will work with this version.") - log.warn('npm', 'You can find the latest version at https://nodejs.org/') + console.error('npm does not support Node.js ' + process.version) + console.error('You should probably upgrade to a newer version of node as we') + console.error("can't make any promises that npm will work with this version.") + console.error('You can find the latest version at https://nodejs.org/') } } diff --git a/lib/utils/update-notifier.js b/lib/utils/update-notifier.js index 2b45d54c815e0..44b6a5433c09a 100644 --- a/lib/utils/update-notifier.js +++ b/lib/utils/update-notifier.js @@ -10,6 +10,7 @@ const { promisify } = require('util') const stat = promisify(require('fs').stat) const writeFile = promisify(require('fs').writeFile) const { resolve } = require('path') +const log = require('./log-shim.js') const isGlobalNpmUpdate = npm => { return npm.flatOptions.global && @@ -61,7 +62,7 @@ const updateNotifier = async (npm, spec = 'latest') => { // if they're currently using a prerelease, nudge to the next prerelease // otherwise, nudge to latest. - const useColor = npm.log.useColor() + const useColor = log.useColor() const mani = await pacote.manifest(`npm@${spec}`, { // always prefer latest, even if doing --tag=whatever on the cmd diff --git a/lib/utils/usage.js b/lib/utils/usage.js index e23e50c51c42a..39eaa45e4101e 100644 --- a/lib/utils/usage.js +++ b/lib/utils/usage.js @@ -1,4 +1,4 @@ -const aliases = require('../utils/cmd-list').aliases +const aliases = require('./cmd-list').aliases module.exports = function usage (cmd, txt, opt) { const post = Object.keys(aliases).reduce(function (p, c) { diff --git a/lib/utils/with-chown-sync.js b/lib/utils/with-chown-sync.js new file mode 100644 index 0000000000000..481b5696ddabf --- /dev/null +++ b/lib/utils/with-chown-sync.js @@ -0,0 +1,13 @@ +const mkdirp = require('mkdirp-infer-owner') +const fs = require('graceful-fs') +const path = require('path') + +module.exports = (file, method) => { + const dir = path.dirname(file) + mkdirp.sync(dir) + const result = method(file) + const st = fs.lstatSync(dir) + fs.chownSync(dir, st.uid, st.gid) + fs.chownSync(file, st.uid, st.gid) + return result +} diff --git a/package-lock.json b/package-lock.json index 7d0386f2d7ea4..e815a8e459b05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -63,6 +63,7 @@ "opener", "pacote", "parse-conflict-json", + "proc-log", "qrcode-terminal", "read", "read-package-json", @@ -140,6 +141,7 @@ "opener": "^1.5.2", "pacote": "^12.0.2", "parse-conflict-json": "^1.1.1", + "proc-log": "^1.0.0", "qrcode-terminal": "^0.12.0", "read": "~1.0.7", "read-package-json": "^4.1.1", diff --git a/package.json b/package.json index 43456fef9a2ac..7bb5ff18e8112 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "opener": "^1.5.2", "pacote": "^12.0.2", "parse-conflict-json": "^1.1.1", + "proc-log": "^1.0.0", "qrcode-terminal": "^0.12.0", "read": "~1.0.7", "read-package-json": "^4.1.1", @@ -181,6 +182,7 @@ "opener", "pacote", "parse-conflict-json", + "proc-log", "qrcode-terminal", "read", "read-package-json", diff --git a/tap-snapshots/test/lib/commands/config.js.test.cjs b/tap-snapshots/test/lib/commands/config.js.test.cjs index 814f6de7c7b6e..da7a89baede40 100644 --- a/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -9,6 +9,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna { "prefix": "{LOCALPREFIX}", "userconfig": "{HOME}/.npmrc", + "cache": "{NPMDIR}/test/lib/commands/tap-testdir-config-config-list---json-sandbox/cache", "json": true, "projectloaded": "yes", "userloaded": "yes", @@ -24,7 +25,6 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "bin-links": true, "browser": null, "ca": null, - "cache": "{CACHE}", "cache-max": null, "cache-min": 0, "cafile": null, @@ -175,7 +175,7 @@ before = null bin-links = true browser = null ca = null -cache = "{CACHE}" +; cache = "{CACHE}" ; overridden by cli cache-max = null cache-min = 0 cafile = null @@ -324,6 +324,7 @@ projectloaded = "yes" ; "cli" config from command line options +cache = "{NPMDIR}/test/lib/commands/tap-testdir-config-config-list---long-sandbox/cache" long = true prefix = "{LOCALPREFIX}" userconfig = "{HOME}/.npmrc" @@ -332,6 +333,7 @@ userconfig = "{HOME}/.npmrc" exports[`test/lib/commands/config.js TAP config list > output matches snapshot 1`] = ` ; "cli" config from command line options +cache = "{NPMDIR}/test/lib/commands/tap-testdir-config-config-list-sandbox/cache" prefix = "{LOCALPREFIX}" userconfig = "{HOME}/.npmrc" diff --git a/tap-snapshots/test/lib/commands/shrinkwrap.js.test.cjs b/tap-snapshots/test/lib/commands/shrinkwrap.js.test.cjs index a0d5795776d6f..ddc80a9350f0a 100644 --- a/tap-snapshots/test/lib/commands/shrinkwrap.js.test.cjs +++ b/tap-snapshots/test/lib/commands/shrinkwrap.js.test.cjs @@ -16,7 +16,7 @@ exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile ancient > must }, "config": {}, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-ancient", + "name": "root", "lockfileVersion": 1, "requires": true }, @@ -36,10 +36,10 @@ exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile ancient upgrad } }, "config": { - "lockfileVersion": 3 + "lockfile-version": 3 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-ancient-upgrade", + "name": "root", "lockfileVersion": 3, "requires": true, "packages": {} @@ -61,7 +61,7 @@ exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile existing > mus }, "config": {}, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing", + "name": "root", "lockfileVersion": 2, "requires": true, "packages": {} @@ -82,10 +82,10 @@ exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile existing downg } }, "config": { - "lockfileVersion": 1 + "lockfile-version": 1 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing-downgrade", + "name": "root", "lockfileVersion": 1, "requires": true }, @@ -105,10 +105,10 @@ exports[`test/lib/commands/shrinkwrap.js TAP with hidden lockfile existing upgra } }, "config": { - "lockfileVersion": 3 + "lockfile-version": 3 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-hidden-lockfile-existing-upgrade", + "name": "root", "lockfileVersion": 3, "requires": true, "packages": {} @@ -124,7 +124,7 @@ exports[`test/lib/commands/shrinkwrap.js TAP with nothing ancient > must match s "localPrefix": {}, "config": {}, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-nothing-ancient", + "name": "root", "lockfileVersion": 2, "requires": true, "packages": {} @@ -139,10 +139,10 @@ exports[`test/lib/commands/shrinkwrap.js TAP with nothing ancient upgrade > must { "localPrefix": {}, "config": { - "lockfileVersion": 3 + "lockfile-version": 3 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-nothing-ancient-upgrade", + "name": "root", "lockfileVersion": 3, "requires": true, "packages": {} @@ -162,12 +162,12 @@ exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json ancient > }, "config": {}, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient", + "name": "root", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient" + "name": "root" } } }, @@ -185,15 +185,15 @@ exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json ancient up } }, "config": { - "lockfileVersion": 3 + "lockfile-version": 3 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient-upgrade", + "name": "root", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-ancient-upgrade" + "name": "root" } } }, @@ -212,12 +212,12 @@ exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json existing > }, "config": {}, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing", + "name": "root", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing" + "name": "root" } } }, @@ -235,10 +235,10 @@ exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json existing d } }, "config": { - "lockfileVersion": 1 + "lockfile-version": 1 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-downgrade", + "name": "root", "lockfileVersion": 1, "requires": true }, @@ -256,15 +256,15 @@ exports[`test/lib/commands/shrinkwrap.js TAP with npm-shrinkwrap.json existing u } }, "config": { - "lockfileVersion": 3 + "lockfile-version": 3 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-upgrade", + "name": "root", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tap-testdir-shrinkwrap-with-npm-shrinkwrap.json-existing-upgrade" + "name": "root" } } }, @@ -283,12 +283,12 @@ exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json ancient > mu }, "config": {}, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient", + "name": "root", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient" + "name": "root" } } }, @@ -306,15 +306,15 @@ exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json ancient upgr } }, "config": { - "lockfileVersion": 3 + "lockfile-version": 3 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient-upgrade", + "name": "root", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-ancient-upgrade" + "name": "root" } } }, @@ -333,12 +333,12 @@ exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json existing > m }, "config": {}, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing", + "name": "root", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing" + "name": "root" } } }, @@ -356,10 +356,10 @@ exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json existing dow } }, "config": { - "lockfileVersion": 1 + "lockfile-version": 1 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-downgrade", + "name": "root", "lockfileVersion": 1, "requires": true }, @@ -377,15 +377,15 @@ exports[`test/lib/commands/shrinkwrap.js TAP with package-lock.json existing upg } }, "config": { - "lockfileVersion": 3 + "lockfile-version": 3 }, "shrinkwrap": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-upgrade", + "name": "root", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "tap-testdir-shrinkwrap-with-package-lock.json-existing-upgrade" + "name": "root" } } }, diff --git a/tap-snapshots/test/lib/commands/view.js.test.cjs b/tap-snapshots/test/lib/commands/view.js.test.cjs index 10d38cb3f8c06..72d09b44e2620 100644 --- a/tap-snapshots/test/lib/commands/view.js.test.cjs +++ b/tap-snapshots/test/lib/commands/view.js.test.cjs @@ -82,7 +82,7 @@ dist dist-tags: latest: 1.0.0 -published yesterday +published {TIME} ago ` exports[`test/lib/commands/view.js TAP should log info of package in current working dir specific version > must match snapshot 1`] = ` @@ -99,7 +99,7 @@ dist dist-tags: latest: 1.0.0 -published yesterday +published {TIME} ago ` exports[`test/lib/commands/view.js TAP should log package info package from git > must match snapshot 1`] = ` @@ -302,7 +302,7 @@ dist dist-tags: latest: 1.0.0 -published yesterday +published {TIME} ago ` exports[`test/lib/commands/view.js TAP should log package info package with semver range > must match snapshot 1`] = ` @@ -319,7 +319,7 @@ dist dist-tags: latest: 1.0.0 -published yesterday +published {TIME} ago blue@1.0.1 | Proprietary | deps: none | versions: 2 diff --git a/tap-snapshots/test/lib/utils/error-message.js.test.cjs b/tap-snapshots/test/lib/utils/error-message.js.test.cjs index e4efb0eb9eef0..3b82e3c055de6 100644 --- a/tap-snapshots/test/lib/utils/error-message.js.test.cjs +++ b/tap-snapshots/test/lib/utils/error-message.js.test.cjs @@ -255,7 +255,7 @@ Object { "summary": Array [ Array [ "notsup", - "Unsupported platform for lodash@1.0.0: wanted {\\"os\\":\\"!yours,mine\\",\\"arch\\":\\"x867,x5309\\"} (current: {\\"os\\":\\"posix\\",\\"arch\\":\\"x64\\"})", + "Unsupported platform for lodash@1.0.0: wanted {/"os/":/"!yours,mine/",/"arch/":/"x867,x5309/"} (current: {/"os/":/"posix/",/"arch/":/"x64/"})", ], ], } @@ -277,7 +277,7 @@ Object { "summary": Array [ Array [ "notsup", - "Unsupported platform for lodash@1.0.0: wanted {\\"os\\":\\"!yours\\",\\"arch\\":\\"x420\\"} (current: {\\"os\\":\\"posix\\",\\"arch\\":\\"x64\\"})", + "Unsupported platform for lodash@1.0.0: wanted {/"os/":/"!yours/",/"arch/":/"x420/"} (current: {/"os/":/"posix/",/"arch/":/"x64/"})", ], ], } @@ -394,7 +394,7 @@ Object { "", Error: whoopsie { "code": "EACCES", - "dest": "/some/cache/dir/dest", + "dest": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-false-cachePath-false-cacheDest-true-/cache/dest", "path": "/not/cache/dir/path", }, ], @@ -428,7 +428,7 @@ Object { Error: whoopsie { "code": "EACCES", "dest": "/not/cache/dir/dest", - "path": "/some/cache/dir/path", + "path": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-false-cachePath-true-cacheDest-false-/cache/path", }, ], ], @@ -460,8 +460,8 @@ Object { "", Error: whoopsie { "code": "EACCES", - "dest": "/some/cache/dir/dest", - "path": "/some/cache/dir/path", + "dest": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-false-cachePath-true-cacheDest-true-/cache/dest", + "path": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-false-cachePath-true-cacheDest-true-/cache/path", }, ], ], @@ -502,7 +502,12 @@ Object { ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":false,"loaded":true,"cachePath":false,"cacheDest":false} > must match snapshot 2`] = ` -Array [] +Array [ + Array [ + "logfile", + "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-true-cachePath-false-cacheDest-false-/cache/_logs/{DATE}-debug-0.log", + ], +] ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":false,"loaded":true,"cachePath":false,"cacheDest":true} > must match snapshot 1`] = ` @@ -517,7 +522,7 @@ Object { previous versions of npm which has since been addressed. To permanently fix this problem, please run: - sudo chown -R 867:5309 "/some/cache/dir" + sudo chown -R 867:5309 "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-true-cachePath-false-cacheDest-true-/cache" ), ], ], @@ -526,6 +531,10 @@ Object { exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":false,"loaded":true,"cachePath":false,"cacheDest":true} > must match snapshot 2`] = ` Array [ + Array [ + "logfile", + "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-true-cachePath-false-cacheDest-true-/cache/_logs/{DATE}-debug-0.log", + ], Array [ "dummy stack trace", ], @@ -544,7 +553,7 @@ Object { previous versions of npm which has since been addressed. To permanently fix this problem, please run: - sudo chown -R 867:5309 "/some/cache/dir" + sudo chown -R 867:5309 "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-true-cachePath-true-cacheDest-false-/cache" ), ], ], @@ -553,6 +562,10 @@ Object { exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":false,"loaded":true,"cachePath":true,"cacheDest":false} > must match snapshot 2`] = ` Array [ + Array [ + "logfile", + "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-true-cachePath-true-cacheDest-false-/cache/_logs/{DATE}-debug-0.log", + ], Array [ "dummy stack trace", ], @@ -571,7 +584,7 @@ Object { previous versions of npm which has since been addressed. To permanently fix this problem, please run: - sudo chown -R 867:5309 "/some/cache/dir" + sudo chown -R 867:5309 "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-true-cachePath-true-cacheDest-true-/cache" ), ], ], @@ -580,6 +593,10 @@ Object { exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":false,"loaded":true,"cachePath":true,"cacheDest":true} > must match snapshot 2`] = ` Array [ + Array [ + "logfile", + "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-false-loaded-true-cachePath-true-cacheDest-true-/cache/_logs/{DATE}-debug-0.log", + ], Array [ "dummy stack trace", ], @@ -642,7 +659,7 @@ Object { "", Error: whoopsie { "code": "EACCES", - "dest": "/some/cache/dir/dest", + "dest": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-false-cachePath-false-cacheDest-true-/cache/dest", "path": "/not/cache/dir/path", }, ], @@ -677,7 +694,7 @@ Object { Error: whoopsie { "code": "EACCES", "dest": "/not/cache/dir/dest", - "path": "/some/cache/dir/path", + "path": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-false-cachePath-true-cacheDest-false-/cache/path", }, ], ], @@ -710,8 +727,8 @@ Object { "", Error: whoopsie { "code": "EACCES", - "dest": "/some/cache/dir/dest", - "path": "/some/cache/dir/path", + "dest": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-false-cachePath-true-cacheDest-true-/cache/dest", + "path": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-false-cachePath-true-cacheDest-true-/cache/path", }, ], ], @@ -753,7 +770,12 @@ Object { ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":true,"loaded":true,"cachePath":false,"cacheDest":false} > must match snapshot 2`] = ` -Array [] +Array [ + Array [ + "logfile", + "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-true-cachePath-false-cacheDest-false-/cache/_logs/{DATE}-debug-0.log", + ], +] ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":true,"loaded":true,"cachePath":false,"cacheDest":true} > must match snapshot 1`] = ` @@ -778,7 +800,7 @@ Object { "", Error: whoopsie { "code": "EACCES", - "dest": "/some/cache/dir/dest", + "dest": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-true-cachePath-false-cacheDest-true-/cache/dest", "path": "/not/cache/dir/path", }, ], @@ -787,7 +809,12 @@ Object { ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":true,"loaded":true,"cachePath":false,"cacheDest":true} > must match snapshot 2`] = ` -Array [] +Array [ + Array [ + "logfile", + "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-true-cachePath-false-cacheDest-true-/cache/_logs/{DATE}-debug-0.log", + ], +] ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":true,"loaded":true,"cachePath":true,"cacheDest":false} > must match snapshot 1`] = ` @@ -813,7 +840,7 @@ Object { Error: whoopsie { "code": "EACCES", "dest": "/not/cache/dir/dest", - "path": "/some/cache/dir/path", + "path": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-true-cachePath-true-cacheDest-false-/cache/path", }, ], ], @@ -821,7 +848,12 @@ Object { ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":true,"loaded":true,"cachePath":true,"cacheDest":false} > must match snapshot 2`] = ` -Array [] +Array [ + Array [ + "logfile", + "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-true-cachePath-true-cacheDest-false-/cache/_logs/{DATE}-debug-0.log", + ], +] ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":true,"loaded":true,"cachePath":true,"cacheDest":true} > must match snapshot 1`] = ` @@ -846,8 +878,8 @@ Object { "", Error: whoopsie { "code": "EACCES", - "dest": "/some/cache/dir/dest", - "path": "/some/cache/dir/path", + "dest": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-true-cachePath-true-cacheDest-true-/cache/dest", + "path": "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-true-cachePath-true-cacheDest-true-/cache/path", }, ], ], @@ -855,7 +887,12 @@ Object { ` exports[`test/lib/utils/error-message.js TAP eacces/eperm {"windows":true,"loaded":true,"cachePath":true,"cacheDest":true} > must match snapshot 2`] = ` -Array [] +Array [ + Array [ + "logfile", + "{CWD}/test/lib/utils/tap-testdir-error-message-eacces-eperm--windows-true-loaded-true-cachePath-true-cacheDest-true-/cache/_logs/{DATE}-debug-0.log", + ], +] ` exports[`test/lib/utils/error-message.js TAP enoent without a file > must match snapshot 1`] = ` @@ -863,7 +900,7 @@ Object { "detail": Array [ Array [ "enoent", - "This is related to npm not being able to find a file.\\n", + "This is related to npm not being able to find a file./n", ], ], "summary": Array [ diff --git a/tap-snapshots/test/lib/utils/exit-handler.js.test.cjs b/tap-snapshots/test/lib/utils/exit-handler.js.test.cjs index eb383c104a674..523aabca29b50 100644 --- a/tap-snapshots/test/lib/utils/exit-handler.js.test.cjs +++ b/tap-snapshots/test/lib/utils/exit-handler.js.test.cjs @@ -5,16 +5,56 @@ * Make sure to inspect the output below. Do not ignore changes! */ 'use strict' -exports[`test/lib/utils/exit-handler.js TAP handles unknown error > should have expected log contents for unknown error 1`] = ` -24 verbose stack Error: ERROR -25 verbose cwd {CWD} -26 verbose Foo 1.0.0 -27 verbose argv "/node" "{CWD}/test/lib/utils/exit-handler.js" -28 verbose node v1.0.0 -29 verbose npm v1.0.0 -30 error code ERROR -31 error ERR ERROR -32 error ERR ERROR -33 verbose exit 1 +exports[`test/lib/utils/exit-handler.js TAP handles unknown error with logs and debug file > debug file contents 1`] = ` +0 timing npm:load:whichnode Completed in {TIME}ms +15 timing config:load Completed in {TIME}ms +16 timing npm:load:configload Completed in {TIME}ms +17 timing npm:load:setTitle Completed in {TIME}ms +19 timing npm:load:display Completed in {TIME}ms +20 verbose logfile {CWD}/test/lib/utils/tap-testdir-exit-handler-handles-unknown-error-with-logs-and-debug-file/cache/_logs/{DATE}-debug-0.log +21 timing npm:load:logFile Completed in {TIME}ms +22 timing npm:load:timers Completed in {TIME}ms +23 timing npm:load:configScope Completed in {TIME}ms +24 timing npm:load Completed in {TIME}ms +25 verbose stack Error: Unknown error +26 verbose cwd {CWD} +27 verbose Foo 1.0.0 +28 verbose argv "/node" "{CWD}/test/lib/utils/exit-handler.js" +29 verbose node v1.0.0 +30 verbose npm v1.0.0 +31 error code ECODE +32 error ERR SUMMARY Unknown error +33 error ERR DETAIL Unknown error +34 verbose exit 1 +35 timing npm Completed in {TIME}ms +36 verbose code 1 +37 error A complete log of this run can be found in: +37 error {CWD}/test/lib/utils/tap-testdir-exit-handler-handles-unknown-error-with-logs-and-debug-file/cache/_logs/{DATE}-debug-0.log +` +exports[`test/lib/utils/exit-handler.js TAP handles unknown error with logs and debug file > logs 1`] = ` +timing npm:load:whichnode Completed in {TIME}ms +timing config:load Completed in {TIME}ms +timing npm:load:configload Completed in {TIME}ms +timing npm:load:setTitle Completed in {TIME}ms +timing npm:load:display Completed in {TIME}ms +verbose logfile {CWD}/test/lib/utils/tap-testdir-exit-handler-handles-unknown-error-with-logs-and-debug-file/cache/_logs/{DATE}-debug-0.log +timing npm:load:logFile Completed in {TIME}ms +timing npm:load:timers Completed in {TIME}ms +timing npm:load:configScope Completed in {TIME}ms +timing npm:load Completed in {TIME}ms +verbose stack Error: Unknown error +verbose cwd {CWD} +verbose Foo 1.0.0 +verbose argv "/node" "{CWD}/test/lib/utils/exit-handler.js" +verbose node v1.0.0 +verbose npm v1.0.0 +error code ECODE +error ERR SUMMARY Unknown error +error ERR DETAIL Unknown error +verbose exit 1 +timing npm Completed in {TIME}ms +verbose code 1 +error A complete log of this run can be found in: + {CWD}/test/lib/utils/tap-testdir-exit-handler-handles-unknown-error-with-logs-and-debug-file/cache/_logs/{DATE}-debug-0.log ` diff --git a/tap-snapshots/test/lib/utils/log-file.js.test.cjs b/tap-snapshots/test/lib/utils/log-file.js.test.cjs new file mode 100644 index 0000000000000..ecce9eafcc925 --- /dev/null +++ b/tap-snapshots/test/lib/utils/log-file.js.test.cjs @@ -0,0 +1,68 @@ +/* IMPORTANT + * This snapshot file is auto-generated, but designed for humans. + * It should be checked into source control and tracked carefully. + * Re-generate by setting TAP_SNAPSHOT=1 and running tests. + * Make sure to inspect the output below. Do not ignore changes! + */ +'use strict' +exports[`test/lib/utils/log-file.js TAP snapshot > must match snapshot 1`] = ` +0 error no prefix +1 error prefix with prefix +2 error prefix 1 2 3 +3 verbose { obj: { with: { many: [Object] } } } +4 verbose {"obj":{"with":{"many":{"props":1}}}} +5 verbose { +5 verbose "obj": { +5 verbose "with": { +5 verbose "many": { +5 verbose "props": 1 +5 verbose } +5 verbose } +5 verbose } +5 verbose } +6 verbose [ 'test', 'with', 'an', 'array' ] +7 verbose ["test","with","an","array"] +8 verbose [ +8 verbose "test", +8 verbose "with", +8 verbose "an", +8 verbose "array" +8 verbose ] +9 verbose [ 'test', [ 'with', [ 'an', [Array] ] ] ] +10 verbose ["test",["with",["an",["array"]]]] +11 verbose [ +11 verbose "test", +11 verbose [ +11 verbose "with", +11 verbose [ +11 verbose "an", +11 verbose [ +11 verbose "array" +11 verbose ] +11 verbose ] +11 verbose ] +11 verbose ] +12 error pre has many errors Error: message +12 error pre at stack trace line 0 +12 error pre at stack trace line 1 +12 error pre at stack trace line 2 +12 error pre at stack trace line 3 +12 error pre at stack trace line 4 +12 error pre at stack trace line 5 +12 error pre at stack trace line 6 +12 error pre at stack trace line 7 +12 error pre at stack trace line 8 +12 error pre at stack trace line 9 Error: message2 +12 error pre at stack trace line 0 +12 error pre at stack trace line 1 +12 error pre at stack trace line 2 +12 error pre at stack trace line 3 +12 error pre at stack trace line 4 +12 error pre at stack trace line 5 +12 error pre at stack trace line 6 +12 error pre at stack trace line 7 +12 error pre at stack trace line 8 +12 error pre at stack trace line 9 +13 error nostack [Error: message] + +` diff --git a/test/coverage-map.js b/test/coverage-map.js index b29fcd8618557..3bfe523322aba 100644 --- a/test/coverage-map.js +++ b/test/coverage-map.js @@ -11,6 +11,9 @@ const coverageMap = (filename) => { // this one doesn't provide any coverage nyc can track return [] } + if (/^test\/lib\/fixtures\//.test(filename)) { + return filename.replace(/\/lib\//, '/') + } if (/^test\/(lib\/|bin\/|index\.js$)/.test(filename)) { return filename.replace(/^test\//, '') } diff --git a/test/fixtures/clean-snapshot.js b/test/fixtures/clean-snapshot.js new file mode 100644 index 0000000000000..037155eea186d --- /dev/null +++ b/test/fixtures/clean-snapshot.js @@ -0,0 +1,19 @@ +// XXX: this also cleans quoted " in json snapshots +// ideally this could be avoided but its easier to just +// run this command inside cleanSnapshot +const normalizePath = (str) => str + .replace(/\r\n/g, '\n') // normalize line endings (for ini) + .replace(/[A-z]:\\/g, '\\') // turn windows roots to posix ones + .replace(/\\+/g, '/') // replace \ with / + +const cleanCwd = (path) => normalizePath(path) + .replace(new RegExp(normalizePath(process.cwd()), 'g'), '{CWD}') + +const cleanDate = (str) => + str.replace(/\d{4}-\d{2}-\d{2}T\d{2}[_:]\d{2}[_:]\d{2}[_:]\d{3}Z/g, '{DATE}') + +module.exports = { + normalizePath, + cleanCwd, + cleanDate, +} diff --git a/test/fixtures/mock-globals.js b/test/fixtures/mock-globals.js new file mode 100644 index 0000000000000..8d8499f895814 --- /dev/null +++ b/test/fixtures/mock-globals.js @@ -0,0 +1,172 @@ +const originalPathKey = process.env.PATH ? 'PATH' : process.env.Path ? 'Path' : 'path' +const last = (arr) => arr[arr.length - 1] +const has = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key) + +const getGlobalAncestors = (keys) => + keys.split('.').reduce((acc, k) => { + const value = last(acc)[k] + acc.push(value) + return acc + }, [global]) + +// A weird getter that can look up keys on nested objects but also +// match keys with dots in their names, eg { 'process.env': { TERM: 'a' } } +// can be looked up with the key 'process.env.TERM' +const get = (obj, fullKey, childKey) => { + if (has(obj, fullKey)) { + return childKey ? get(obj[fullKey], childKey) : obj[fullKey] + } else { + const lastDot = fullKey.lastIndexOf('.') + return lastDot === -1 ? undefined : get( + obj, + fullKey.slice(0, lastDot), + fullKey.slice(lastDot + 1) + (childKey ? `.${childKey}` : '') + ) + } +} + +// { a: 1, b: { c: 2 } } => ['a', 'b.c'] +const getKeys = (values, p = '', acc = []) => + Object.entries(values).reduce((memo, [k, value]) => { + const key = p ? `${p}.${k}` : k + return value && typeof value === 'object' && !Array.isArray(value) + ? getKeys(value, key, memo) + : memo.concat(key) + }, acc) + +// Walk prototype chain to get first available descriptor +const getPropertyDescriptor = (obj, key, fullKey) => { + if (fullKey.toUpperCase() === 'PROCESS.ENV.PATH') { + // if getting original env.path value, use cross platform compatible key + key = originalPathKey + } + let d = Object.getOwnPropertyDescriptor(obj, key) + while (!d) { + obj = Object.getPrototypeOf(obj) + if (!obj) { + return + } + d = Object.getOwnPropertyDescriptor(obj, key) + } + return d +} + +class MockGlobals { + #cache = new Map() + #resets = [] + #defaultDescriptor = { + configurable: true, + writable: true, + enumerable: true, + } + + teardown () { + this.#resets.forEach(r => r.reset(true)) + } + + registerGlobals (globals, { replace = false } = {}) { + const resets = this.createResets(globals, { replace }) + this.#resets.push(...resets) + return resets.reduce((acc, r) => { + acc[r.fullKey] = r.reset + return acc + }, {}) + } + + createResets (g, { replace }) { + const keys = replace ? Object.keys(g) : getKeys(g) + return keys.map(k => this.set(k, g)) + } + + cacheKey (k) { + return `__${k}__` + } + + pushDescriptor (key, value) { + const cache = this.#cache.get(this.cacheKey(key)) + if (cache) { + this.#cache.get(this.cacheKey(key)).push(value) + } else { + this.#cache.set(this.cacheKey(key), [value]) + } + return value + } + + popDescriptor (key) { + const cache = this.#cache.get(this.cacheKey(key)) + if (!cache) { + return null + } + const value = cache.pop() + if (!cache.length) { + this.#cache.delete(this.cacheKey(key)) + } + return value + } + + createReset (parent, key, fullKey) { + const res = { + fullKey, + key, + reset: (teardown) => { + const popped = this.popDescriptor(fullKey) + if (popped === null) { + return + } + const index = this.#resets.findIndex((v) => v === res) + if (!teardown && index > -1) { + this.#resets.splice(index, 1) + } + return popped + ? Object.defineProperty(parent, key, popped) + : (delete parent[key]) + }, + } + return res + } + + set (fullKey, globals) { + const values = getGlobalAncestors(fullKey) + const parent = values[values.length - 2] + + const key = last(fullKey.split('.')) + const value = get(globals, fullKey) + + const currentDescriptor = getPropertyDescriptor(parent, key, fullKey) + this.pushDescriptor(fullKey, currentDescriptor) + + const reset = this.createReset(parent, key, fullKey) + + if (value === undefined) { + delete parent[key] + } else { + const newDescriptor = { ...(currentDescriptor || this.#defaultDescriptor) } + if (newDescriptor.get) { + newDescriptor.get = () => value + } else { + newDescriptor.value = value + } + Object.defineProperty(parent, key, newDescriptor) + } + + return reset + } +} + +const cache = new Map() + +const mockGlobals = (t, globals, options) => { + const hasInstance = cache.has(t) + const instance = hasInstance ? cache.get(t) : new MockGlobals() + const reset = instance.registerGlobals(globals, options) + if (!hasInstance) { + cache.set(t, instance) + t.teardown(() => { + instance.teardown() + cache.delete(t) + }) + } + return { reset } +} + +module.exports = mockGlobals diff --git a/test/fixtures/mock-logs.js b/test/fixtures/mock-logs.js new file mode 100644 index 0000000000000..80037c6ffa88d --- /dev/null +++ b/test/fixtures/mock-logs.js @@ -0,0 +1,71 @@ + +const NPMLOG = require('npmlog') +const { LEVELS } = require('proc-log') + +const merge = (...objs) => objs.reduce((acc, obj) => ({ ...acc, ...obj })) + +const mockLogs = (otherMocks = {}) => { + // Return mocks as an array with getters for each level + // that return an array of logged properties with the + // level removed. This is for convenience throughout tests + const logs = Object.defineProperties( + [], + ['timing', ...LEVELS].reduce((acc, level) => { + acc[level] = { + get () { + return this + .filter(([l]) => level === l) + .map(([l, ...args]) => args) + }, + } + return acc + }, {}) + ) + + // This returns an object with mocked versions of all necessary + // logging modules. It mocks them with methods that add logs + // to an array which it also returns. The reason it also returns + // the mocks is that in tests the same instance of these mocks + // should be passed to multiple calls to t.mock. + // XXX: this is messy and fragile and should be removed in favor + // of some other way to collect and filter logs across all tests + const logMocks = { + 'proc-log': merge( + { LEVELS }, + LEVELS.reduce((acc, l) => { + acc[l] = (...args) => { + // Re-emit log item for since the log file listens on these + process.emit('log', l, ...args) + // Dont add pause/resume events to the logs. Those aren't displayed + // and emitting them is tested in the display layer + if (l !== 'pause' && l !== 'resume') { + logs.push([l, ...args]) + } + } + return acc + }, {}), + otherMocks['proc-log'] + ), + // Object.assign is important here because we need to assign + // mocked properties directly to npmlog and then mock with that + // object. This is necessary so tests can still directly set + // `log.level = 'silent'` anywhere in the test and have that + // that reflected in the npmlog singleton. + // XXX: remove with npmlog + npmlog: Object.assign(NPMLOG, merge( + // no-op all npmlog methods by default so tests + // dont output anything to the terminal + Object.keys(NPMLOG.levels).reduce((acc, k) => { + acc[k] = () => {} + return acc + }, {}), + // except collect timing logs + { timing: (...args) => logs.push(['timing', ...args]) }, + otherMocks.npmlog + )), + } + + return { logs, logMocks } +} + +module.exports = mockLogs diff --git a/test/fixtures/mock-npm.js b/test/fixtures/mock-npm.js index a51ec3e5bb879..7518855319b4a 100644 --- a/test/fixtures/mock-npm.js +++ b/test/fixtures/mock-npm.js @@ -1,71 +1,126 @@ -const npmlog = require('npmlog') -const procLog = require('../../lib/utils/proc-log-listener.js') -procLog.reset() - -// In theory we shouldn't have to do this if all the tests were tearing down -// their listeners properly, we're still getting warnings even though -// perfStop() and procLog.reset() is in the teardown script. This silences the -// warnings for now -require('events').defaultMaxListeners = Infinity - -const realLog = {} -for (const level in npmlog.levels) { - realLog[level] = npmlog[level] -} - -const { title, execPath } = process +const os = require('os') +const fs = require('fs').promises +const path = require('path') +const mockLogs = require('./mock-logs') +const mockGlobals = require('./mock-globals') +const log = require('../../lib/utils/log-shim') -// Eventually this should default to having a prefix of an empty testdir, and -// awaiting npm.load() unless told not to (for npm tests for example). Ideally -// the prefix of an empty dir is inferred rather than explicitly set const RealMockNpm = (t, otherMocks = {}) => { - const mock = {} - mock.logs = [] - mock.outputs = [] - mock.joinedOutput = () => { - return mock.outputs.map(o => o.join(' ')).join('\n') + const mock = { + ...mockLogs(otherMocks), + outputs: [], + joinedOutput: () => mock.outputs.map(o => o.join(' ')).join('\n'), } - mock.filteredLogs = title => mock.logs.filter(([t]) => t === title).map(([, , msg]) => msg) - const Npm = t.mock('../../lib/npm.js', otherMocks) - class MockNpm extends Npm { - constructor () { - super() - for (const level in npmlog.levels) { - npmlog[level] = (...msg) => { - mock.logs.push([level, ...msg]) - - const l = npmlog.level - npmlog.level = 'silent' - realLog[level](...msg) - npmlog.level = l - } - } - // npm.js tests need this restored to actually test this function! - mock.npmOutput = this.output - this.output = (...msg) => mock.outputs.push(msg) + + const Npm = t.mock('../../lib/npm.js', { + ...otherMocks, + ...mock.logMocks, + }) + + mock.Npm = class MockNpm extends Npm { + // lib/npm.js tests needs this to actually test the function! + originalOutput (...args) { + super.output(...args) + } + + output (...args) { + mock.outputs.push(args) } } - mock.Npm = MockNpm - t.afterEach(() => { - mock.outputs.length = 0 - mock.logs.length = 0 + + return mock +} + +// Resolve some options to a function call with supplied args +const result = (fn, ...args) => typeof fn === 'function' ? fn(...args) : fn + +const LoadMockNpm = async (t, { + init = true, + load = init, + testdir = {}, + config = {}, + mocks = {}, + globals = null, +} = {}) => { + // Mock some globals with their original values so they get torn down + // back to the original at the end of the test since they are manipulated + // by npm itself + mockGlobals(t, { + process: { + title: process.title, + execPath: process.execPath, + env: { + npm_command: process.env.npm_command, + COLOR: process.env.COLOR, + }, + }, }) - t.teardown(() => { - process.removeAllListeners('time') - process.removeAllListeners('timeEnd') - npmlog.record.length = 0 - for (const level in npmlog.levels) { - npmlog[level] = realLog[level] - } - procLog.reset() - process.title = title - process.execPath = execPath - delete process.env.npm_command - delete process.env.COLOR + const { Npm, ...rest } = RealMockNpm(t, mocks) + + if (!init && load) { + throw new Error('cant `load` without `init`') + } + + const _level = log.level + t.teardown(() => log.level = _level) + + if (config.loglevel) { + // Set log level as early as possible since it is set + // on the npmlog singleton and shared across everything + log.level = config.loglevel + } + + const dir = t.testdir({ root: testdir, cache: {} }) + const prefix = path.join(dir, 'root') + const cache = path.join(dir, 'cache') + + // Set cache to testdir via env var so it is available when load is run + // XXX: remove this for a solution where cache argv is passed in + mockGlobals(t, { + 'process.env.npm_config_cache': cache, }) - return mock + if (globals) { + mockGlobals(t, result(globals, { prefix, cache })) + } + + const npm = init ? new Npm() : null + t.teardown(() => npm && npm.unload()) + + if (load) { + await npm.load() + for (const [k, v] of Object.entries(result(config, { npm, prefix, cache }))) { + npm.config.set(k, v) + } + if (config.loglevel) { + // Set global loglevel *again* since it possibly got reset during load + // XXX: remove with npmlog + log.level = config.loglevel + } + npm.prefix = prefix + npm.cache = cache + } + + return { + ...rest, + Npm, + npm, + prefix, + cache, + debugFile: async () => { + const readFiles = npm.logFiles.map(f => fs.readFile(f)) + const logFiles = await Promise.all(readFiles) + return logFiles + .flatMap((d) => d.toString().trim().split(os.EOL)) + .filter(Boolean) + .join('\n') + }, + timingFile: async () => { + const data = await fs.readFile(path.resolve(cache, '_timing.json'), 'utf8') + return JSON.parse(data) // XXX: this fails if multiple timings are written + }, + } } const realConfig = require('../../lib/utils/config') @@ -96,21 +151,6 @@ class MockNpm { 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) { @@ -127,5 +167,5 @@ const FakeMockNpm = (base = {}) => { module.exports = { fake: FakeMockNpm, - real: RealMockNpm, + load: LoadMockNpm, } diff --git a/test/fixtures/sandbox.js b/test/fixtures/sandbox.js index b012790fb535d..701d9cea72397 100644 --- a/test/fixtures/sandbox.js +++ b/test/fixtures/sandbox.js @@ -4,15 +4,12 @@ const { homedir, tmpdir } = require('os') const { dirname, join } = require('path') const { promisify } = require('util') const mkdirp = require('mkdirp-infer-owner') -const npmlog = require('npmlog') const rimraf = promisify(require('rimraf')) +const mockLogs = require('./mock-logs') const chain = new Map() const sandboxes = new Map() -// Disable lint errors for assigning to process global -/* global process:writable */ - // keep a reference to the real process const _process = process @@ -34,19 +31,6 @@ createHook({ }, }).enable() -for (const level in npmlog.levels) { - npmlog[`_${level}`] = npmlog[level] - npmlog[level] = (...args) => { - process._logs = process._logs || {} - process._logs[level] = process._logs[level] || [] - process._logs[level].push(args) - const _level = npmlog.level - npmlog.level = 'silent' - npmlog[`_${level}`](...args) - npmlog.level = _level - } -} - const _data = Symbol('sandbox.data') const _dirs = Symbol('sandbox.dirs') const _test = Symbol('sandbox.test') @@ -57,6 +41,7 @@ const _output = Symbol('sandbox.output') const _proxy = Symbol('sandbox.proxy') const _get = Symbol('sandbox.proxy.get') const _set = Symbol('sandbox.proxy.set') +const _logs = Symbol('sandbox.logs') // these config keys can be redacted widely const redactedDefaults = [ @@ -92,6 +77,7 @@ class Sandbox extends EventEmitter { global: options.global || join(tempDir, 'global'), home: options.home || join(tempDir, 'home'), project: options.project || join(tempDir, 'project'), + cache: options.cache || join(tempDir, 'cache'), } this[_proxy] = new Proxy(_process, { @@ -111,7 +97,7 @@ class Sandbox extends EventEmitter { } get logs () { - return this[_proxy]._logs + return this[_logs] } get global () { @@ -126,6 +112,10 @@ class Sandbox extends EventEmitter { return this[_dirs].project } + get cache () { + return this[_dirs].cache + } + get process () { return this[_proxy] } @@ -205,7 +195,9 @@ class Sandbox extends EventEmitter { if (this[_parent]) { sandboxes.delete(this[_parent]) } - + if (this[_npm]) { + this[_npm].unload() + } return rimraf(this[_dirs].temp).catch(() => null) } @@ -275,11 +267,17 @@ class Sandbox extends EventEmitter { '--prefix', this.project, '--userconfig', join(this.home, '.npmrc'), '--globalconfig', join(this.global, 'npmrc'), + '--cache', this.cache, command, ...argv, ] - const Npm = this[_test].mock('../../lib/npm.js', this[_mocks]) + const mockedLogs = mockLogs(this[_mocks]) + this[_logs] = mockedLogs.logs + const Npm = this[_test].mock('../../lib/npm.js', { + ...this[_mocks], + ...mockedLogs.logMocks, + }) this[_npm] = new Npm() this[_npm].output = (...args) => this[_output].push(args) await this[_npm].load() @@ -321,11 +319,17 @@ class Sandbox extends EventEmitter { '--prefix', this.project, '--userconfig', join(this.home, '.npmrc'), '--globalconfig', join(this.global, 'npmrc'), + '--cache', this.cache, command, ...argv, ] - const Npm = this[_test].mock('../../lib/npm.js', this[_mocks]) + const mockedLogs = mockLogs(this[_mocks]) + this[_logs] = mockedLogs.logs + const Npm = this[_test].mock('../../lib/npm.js', { + ...this[_mocks], + ...mockedLogs.logMocks, + }) this[_npm] = new Npm() this[_npm].output = (...args) => this[_output].push(args) await this[_npm].load() diff --git a/test/index.js b/test/index.js index 26db16e1f78ba..081c89cee9c70 100644 --- a/test/index.js +++ b/test/index.js @@ -1,16 +1,18 @@ const t = require('tap') const index = require.resolve('../index.js') const packageIndex = require.resolve('../') + t.equal(index, packageIndex, 'index is main package require() export') t.throws(() => require(index), { message: 'The programmatic API was removed in npm v8.0.0', }) t.test('loading as main module will load the cli', t => { + const cwd = t.testdir() const { spawn } = require('child_process') const LS = require('../lib/commands/ls.js') const ls = new LS({}) - const p = spawn(process.execPath, [index, 'ls', '-h']) + const p = spawn(process.execPath, [index, 'ls', '-h', '--cache', cwd]) const out = [] p.stdout.on('data', c => out.push(c)) p.on('close', (code, signal) => { diff --git a/test/lib/auth/legacy.js b/test/lib/auth/legacy.js index 7b61e9f6e946e..0c23f8ba6b335 100644 --- a/test/lib/auth/legacy.js +++ b/test/lib/auth/legacy.js @@ -6,7 +6,7 @@ const token = '24528a24f240' const profile = {} const read = {} const legacy = t.mock('../../../lib/auth/legacy.js', { - npmlog: { + 'proc-log': { info: (...msgs) => { log += msgs.join(' ') }, diff --git a/test/lib/auth/sso.js b/test/lib/auth/sso.js index d5922055931e1..473c8cc241467 100644 --- a/test/lib/auth/sso.js +++ b/test/lib/auth/sso.js @@ -11,7 +11,7 @@ const SSO_URL = 'https://registry.npmjs.org/{SSO_URL}' const profile = {} const npmFetch = {} const sso = t.mock('../../../lib/auth/sso.js', { - npmlog: { + 'proc-log': { info: (...msgs) => { log += msgs.join(' ') + '\n' }, diff --git a/test/lib/cli.js b/test/lib/cli.js index d762943b47008..f02c57d8cf730 100644 --- a/test/lib/cli.js +++ b/test/lib/cli.js @@ -1,176 +1,153 @@ const t = require('tap') -const { real: mockNpm } = require('../fixtures/mock-npm.js') - -const unsupportedMock = { - checkForBrokenNode: () => {}, - checkForUnsupportedNode: () => {}, -} - -let exitHandlerCalled = null -let exitHandlerNpm = null -let exitHandlerCb -const exitHandlerMock = (...args) => { - exitHandlerCalled = args - if (exitHandlerCb) { - exitHandlerCb() +const mockGlobals = require('../fixtures/mock-globals.js') +const { load: loadMockNpm } = require('../fixtures/mock-npm.js') + +const cliMock = async (t, mocks) => { + let exitHandlerArgs = null + let npm = null + const exitHandlerMock = (...args) => { + exitHandlerArgs = args + npm.unload() } -} -exitHandlerMock.setNpm = npm => { - exitHandlerNpm = npm -} - -const logs = [] -const npmlogMock = { - pause: () => logs.push('pause'), - verbose: (...msg) => logs.push(['verbose', ...msg]), - info: (...msg) => logs.push(['info', ...msg]), -} + exitHandlerMock.setNpm = _npm => npm = _npm -const cliMock = Npm => - t.mock('../../lib/cli.js', { + const { Npm, outputs, logMocks, logs } = await loadMockNpm(t, { mocks, init: false }) + const cli = t.mock('../../lib/cli.js', { '../../lib/npm.js': Npm, '../../lib/utils/update-notifier.js': async () => null, - '../../lib/utils/unsupported.js': unsupportedMock, + '../../lib/utils/unsupported.js': { + checkForBrokenNode: () => {}, + checkForUnsupportedNode: () => {}, + }, '../../lib/utils/exit-handler.js': exitHandlerMock, - npmlog: npmlogMock, + ...logMocks, }) -const processMock = proc => { - const mocked = { - ...process, - on: () => {}, - ...proc, + return { + Npm, + cli, + outputs, + exitHandlerCalled: () => exitHandlerArgs, + exitHandlerNpm: () => npm, + logs, } - // nopt looks at process directly - process.argv = mocked.argv - return mocked } -const { argv } = process - t.afterEach(() => { - logs.length = 0 - process.argv = argv - exitHandlerCalled = null - exitHandlerNpm = null + delete process.exitCode }) t.test('print the version, and treat npm_g as npm -g', async t => { - const proc = processMock({ - argv: ['node', 'npm_g', '-v'], - version: process.version, + mockGlobals(t, { + 'process.argv': ['node', 'npm_g', '-v'], }) - const { Npm, outputs } = mockNpm(t) - const cli = cliMock(Npm) - await cli(proc) + const { logs, cli, Npm, outputs, exitHandlerCalled } = await cliMock(t) + await cli(process) - t.strictSame(proc.argv, ['node', 'npm', '-g', '-v'], 'npm process.argv was rewritten') t.strictSame(process.argv, ['node', 'npm', '-g', '-v'], 'system process.argv was rewritten') - t.strictSame(logs, [ - 'pause', - ['verbose', 'cli', proc.argv], - ['info', 'using', 'npm@%s', Npm.version], - ['info', 'using', 'node@%s', process.version], + t.strictSame(logs.verbose.filter(([p]) => p !== 'logfile'), [ + ['cli', process.argv], + ]) + t.strictSame(logs.info, [ + ['using', 'npm@%s', Npm.version], + ['using', 'node@%s', process.version], ]) t.strictSame(outputs, [[Npm.version]]) - t.strictSame(exitHandlerCalled, []) + t.strictSame(exitHandlerCalled(), []) }) t.test('calling with --versions calls npm version with no args', async t => { - t.plan(5) - const proc = processMock({ - argv: ['node', 'npm', 'install', 'or', 'whatever', '--versions'], + t.plan(6) + mockGlobals(t, { + 'process.argv': ['node', 'npm', 'install', 'or', 'whatever', '--versions'], }) - const { Npm, outputs } = mockNpm(t, { + const { logs, cli, Npm, outputs, exitHandlerCalled } = await cliMock(t, { '../../lib/commands/version.js': class Version { async exec (args) { t.strictSame(args, []) } }, }) - const cli = cliMock(Npm) - await cli(proc) - t.equal(proc.title, 'npm') - t.strictSame(logs, [ - 'pause', - ['verbose', 'cli', proc.argv], - ['info', 'using', 'npm@%s', Npm.version], - ['info', 'using', 'node@%s', process.version], + + await cli(process) + t.equal(process.title, 'npm install or whatever') + t.strictSame(logs.verbose.filter(([p]) => p !== 'logfile'), [ + ['cli', process.argv], + ]) + t.strictSame(logs.info, [ + ['using', 'npm@%s', Npm.version], + ['using', 'node@%s', process.version], ]) t.strictSame(outputs, []) - t.strictSame(exitHandlerCalled, []) + t.strictSame(exitHandlerCalled(), []) }) t.test('logged argv is sanitized', async t => { - const proc = processMock({ - argv: [ + mockGlobals(t, { + 'process.argv': [ 'node', 'npm', 'version', 'https://username:password@npmjs.org/test_url_with_a_password', ], }) - const { Npm } = mockNpm(t, { + const { logs, cli, Npm } = await cliMock(t, { '../../lib/commands/version.js': class Version { async exec (args) {} }, }) - const cli = cliMock(Npm) - - await cli(proc) - t.equal(proc.title, 'npm') - t.strictSame(logs, [ - 'pause', + await cli(process) + t.ok(process.title.startsWith('npm version https://username:***@npmjs.org')) + t.strictSame(logs.verbose.filter(([p]) => p !== 'logfile'), [ [ - 'verbose', 'cli', ['node', 'npm', 'version', 'https://username:***@npmjs.org/test_url_with_a_password'], ], - ['info', 'using', 'npm@%s', Npm.version], - ['info', 'using', 'node@%s', process.version], + ]) + t.strictSame(logs.info, [ + ['using', 'npm@%s', Npm.version], + ['using', 'node@%s', process.version], ]) }) t.test('print usage if no params provided', async t => { - const proc = processMock({ - argv: ['node', 'npm'], + mockGlobals(t, { + 'process.argv': ['node', 'npm'], }) - const { Npm, outputs } = mockNpm(t) - const cli = cliMock(Npm) - await cli(proc) + const { cli, outputs, exitHandlerCalled, exitHandlerNpm } = await cliMock(t) + await cli(process) t.match(outputs[0][0], 'Usage:', 'outputs npm usage') - t.match(exitHandlerCalled, [], 'should call exitHandler with no args') - t.ok(exitHandlerNpm, 'exitHandler npm is set') - t.match(proc.exitCode, 1) + t.match(exitHandlerCalled(), [], 'should call exitHandler with no args') + t.ok(exitHandlerNpm(), 'exitHandler npm is set') + t.match(process.exitCode, 1) }) t.test('print usage if non-command param provided', async t => { - const proc = processMock({ - argv: ['node', 'npm', 'tset'], + mockGlobals(t, { + 'process.argv': ['node', 'npm', 'tset'], }) - const { Npm, outputs } = mockNpm(t) - const cli = cliMock(Npm) - await cli(proc) + const { cli, outputs, exitHandlerCalled, exitHandlerNpm } = await cliMock(t) + await cli(process) t.match(outputs[0][0], 'Unknown command: "tset"') t.match(outputs[0][0], 'Did you mean this?') - t.match(exitHandlerCalled, [], 'should call exitHandler with no args') - t.ok(exitHandlerNpm, 'exitHandler npm is set') - t.match(proc.exitCode, 1) + t.match(exitHandlerCalled(), [], 'should call exitHandler with no args') + t.ok(exitHandlerNpm(), 'exitHandler npm is set') + t.match(process.exitCode, 1) }) t.test('load error calls error handler', async t => { - const proc = processMock({ - argv: ['node', 'npm', 'asdf'], + mockGlobals(t, { + 'process.argv': ['node', 'npm', 'asdf'], }) const err = new Error('test load error') - const { Npm } = mockNpm(t, { + const { cli, exitHandlerCalled } = await cliMock(t, { '../../lib/utils/config/index.js': { definitions: null, flatten: null, @@ -182,7 +159,6 @@ t.test('load error calls error handler', async t => { } }, }) - const cli = cliMock(Npm) - await cli(proc) - t.strictSame(exitHandlerCalled, [err]) + await cli(process) + t.strictSame(exitHandlerCalled(), [err]) }) diff --git a/test/lib/commands/access.js b/test/lib/commands/access.js index fdf132aff97f3..298897e4f5ffc 100644 --- a/test/lib/commands/access.js +++ b/test/lib/commands/access.js @@ -1,18 +1,9 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm.js') - -const { Npm } = mockNpm(t) -const npm = new Npm() - -const prefix = t.testdir({}) - -t.before(async () => { - await npm.load() - npm.prefix = prefix -}) +const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') t.test('completion', async t => { + const { npm } = await loadMockNpm(t) const access = await npm.cmd('access') const testComp = (argv, expect) => { const res = access.completion({ conf: { argv: { remain: argv } } }) @@ -42,6 +33,7 @@ t.test('completion', async t => { }) t.test('subcommand required', async t => { + const { npm } = await loadMockNpm(t) const access = await npm.cmd('access') await t.rejects( npm.exec('access', []), @@ -50,6 +42,7 @@ t.test('subcommand required', async t => { }) t.test('unrecognized subcommand', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', ['blerg']), /Usage: blerg is not a recognized subcommand/, @@ -58,6 +51,7 @@ t.test('unrecognized subcommand', async t => { }) t.test('edit', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', ['edit', '@scoped/another']), /edit subcommand is not implemented yet/, @@ -66,15 +60,13 @@ t.test('edit', async t => { }) t.test('access public on unscoped package', async t => { - t.teardown(() => { - npm.prefix = prefix - }) - const testdir = t.testdir({ - 'package.json': JSON.stringify({ - name: 'npm-access-public-pkg', - }), + const { npm } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'npm-access-public-pkg', + }), + }, }) - npm.prefix = testdir await t.rejects( npm.exec('access', ['public']), /Usage: This command is only available for scoped packages/, @@ -84,30 +76,30 @@ t.test('access public on unscoped package', async t => { t.test('access public on scoped package', async t => { t.plan(2) - const { Npm } = mockNpm(t, { - libnpmaccess: { - public: (pkg, { registry }) => { - t.equal(pkg, name, 'should use pkg name ref') - t.equal( - registry, - 'https://registry.npmjs.org/', - 'should forward correct options' - ) - return true + const name = '@scoped/npm-access-public-pkg' + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + public: (pkg, { registry }) => { + t.equal(pkg, name, 'should use pkg name ref') + t.equal( + registry, + 'https://registry.npmjs.org/', + 'should forward correct options' + ) + return true + }, }, }, + testdir: { + 'package.json': JSON.stringify({ name }), + }, }) - const npm = new Npm() - await npm.load() - const name = '@scoped/npm-access-public-pkg' - const testdir = t.testdir({ - 'package.json': JSON.stringify({ name }), - }) - npm.prefix = testdir await npm.exec('access', ['public']) }) t.test('access public on missing package.json', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', ['public']), /no package name passed to command and no package.json found/, @@ -116,14 +108,12 @@ t.test('access public on missing package.json', async t => { }) t.test('access public on invalid package.json', async t => { - t.teardown(() => { - npm.prefix = prefix - }) - const testdir = t.testdir({ - 'package.json': '{\n', - node_modules: {}, + const { npm } = await loadMockNpm(t, { + testdir: { + 'package.json': '{\n', + node_modules: {}, + }, }) - npm.prefix = testdir await t.rejects( npm.exec('access', ['public']), { code: 'EJSONPARSE' }, @@ -132,15 +122,13 @@ t.test('access public on invalid package.json', async t => { }) t.test('access restricted on unscoped package', async t => { - t.teardown(() => { - npm.prefix = prefix - }) - const testdir = t.testdir({ - 'package.json': JSON.stringify({ - name: 'npm-access-restricted-pkg', - }), + const { npm } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'npm-access-restricted-pkg', + }), + }, }) - npm.prefix = testdir await t.rejects( npm.exec('access', ['public']), /Usage: This command is only available for scoped packages/, @@ -150,30 +138,30 @@ t.test('access restricted on unscoped package', async t => { t.test('access restricted on scoped package', async t => { t.plan(2) - const { Npm } = mockNpm(t, { - libnpmaccess: { - restricted: (pkg, { registry }) => { - t.equal(pkg, name, 'should use pkg name ref') - t.equal( - registry, - 'https://registry.npmjs.org/', - 'should forward correct options' - ) - return true + const name = '@scoped/npm-access-restricted-pkg' + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + restricted: (pkg, { registry }) => { + t.equal(pkg, name, 'should use pkg name ref') + t.equal( + registry, + 'https://registry.npmjs.org/', + 'should forward correct options' + ) + return true + }, }, }, + testdir: { + 'package.json': JSON.stringify({ name }), + }, }) - const npm = new Npm() - await npm.load() - const name = '@scoped/npm-access-restricted-pkg' - const testdir = t.testdir({ - 'package.json': JSON.stringify({ name }), - }) - npm.prefix = testdir await npm.exec('access', ['restricted']) }) t.test('access restricted on missing package.json', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', ['restricted']), /no package name passed to command and no package.json found/, @@ -182,14 +170,12 @@ t.test('access restricted on missing package.json', async t => { }) t.test('access restricted on invalid package.json', async t => { - t.teardown(() => { - npm.prefix = prefix - }) - const testdir = t.testdir({ - 'package.json': '{\n', - node_modules: {}, + const { npm } = await loadMockNpm(t, { + testdir: { + 'package.json': '{\n', + node_modules: {}, + }, }) - npm.prefix = testdir await t.rejects( npm.exec('access', ['restricted']), { code: 'EJSONPARSE' }, @@ -199,17 +185,18 @@ t.test('access restricted on invalid package.json', async t => { t.test('access grant read-only', async t => { t.plan(3) - const { Npm } = mockNpm(t, { - libnpmaccess: { - grant: (spec, team, permissions) => { - t.equal(spec, '@scoped/another', 'should use expected spec') - t.equal(team, 'myorg:myteam', 'should use expected team') - t.equal(permissions, 'read-only', 'should forward permissions') - return true + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + grant: (spec, team, permissions) => { + t.equal(spec, '@scoped/another', 'should use expected spec') + t.equal(team, 'myorg:myteam', 'should use expected team') + t.equal(permissions, 'read-only', 'should forward permissions') + return true + }, }, }, }) - const npm = new Npm() await npm.exec('access', [ 'grant', 'read-only', @@ -220,17 +207,18 @@ t.test('access grant read-only', async t => { t.test('access grant read-write', async t => { t.plan(3) - const { Npm } = mockNpm(t, { - libnpmaccess: { - grant: (spec, team, permissions) => { - t.equal(spec, '@scoped/another', 'should use expected spec') - t.equal(team, 'myorg:myteam', 'should use expected team') - t.equal(permissions, 'read-write', 'should forward permissions') - return true + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + grant: (spec, team, permissions) => { + t.equal(spec, '@scoped/another', 'should use expected spec') + t.equal(team, 'myorg:myteam', 'should use expected team') + t.equal(permissions, 'read-write', 'should forward permissions') + return true + }, }, }, }) - const npm = new Npm() await npm.exec('access', [ 'grant', 'read-write', @@ -241,24 +229,23 @@ t.test('access grant read-write', async t => { t.test('access grant current cwd', async t => { t.plan(3) - const testdir = t.testdir({ - 'package.json': JSON.stringify({ - name: 'yargs', - }), - }) - const { Npm } = mockNpm(t, { - libnpmaccess: { - grant: (spec, team, permissions) => { - t.equal(spec, 'yargs', 'should use expected spec') - t.equal(team, 'myorg:myteam', 'should use expected team') - t.equal(permissions, 'read-write', 'should forward permissions') - return true + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + grant: (spec, team, permissions) => { + t.equal(spec, 'yargs', 'should use expected spec') + t.equal(team, 'myorg:myteam', 'should use expected team') + t.equal(permissions, 'read-write', 'should forward permissions') + return true + }, }, }, + testdir: { + 'package.json': JSON.stringify({ + name: 'yargs', + }), + }, }) - const npm = new Npm() - await npm.load() - npm.prefix = testdir await npm.exec('access', [ 'grant', 'read-write', @@ -267,6 +254,7 @@ t.test('access grant current cwd', async t => { }) t.test('access grant others', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', [ 'grant', @@ -280,6 +268,7 @@ t.test('access grant others', async t => { }) t.test('access grant missing team args', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', [ 'grant', @@ -293,6 +282,7 @@ t.test('access grant missing team args', async t => { }) t.test('access grant malformed team arg', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', [ 'grant', @@ -307,36 +297,37 @@ t.test('access grant malformed team arg', async t => { t.test('access 2fa-required/2fa-not-required', async t => { t.plan(2) - const { Npm } = mockNpm(t, { - libnpmaccess: { - tfaRequired: (spec) => { - t.equal(spec, '@scope/pkg', 'should use expected spec') - return true - }, - tfaNotRequired: (spec) => { - t.equal(spec, 'unscoped-pkg', 'should use expected spec') - return true + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + tfaRequired: (spec) => { + t.equal(spec, '@scope/pkg', 'should use expected spec') + return true + }, + tfaNotRequired: (spec) => { + t.equal(spec, 'unscoped-pkg', 'should use expected spec') + return true + }, }, }, }) - const npm = new Npm() - await npm.exec('access', ['2fa-required', '@scope/pkg']) await npm.exec('access', ['2fa-not-required', 'unscoped-pkg']) }) t.test('access revoke', async t => { t.plan(2) - const { Npm } = mockNpm(t, { - libnpmaccess: { - revoke: (spec, team) => { - t.equal(spec, '@scoped/another', 'should use expected spec') - t.equal(team, 'myorg:myteam', 'should use expected team') - return true + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + revoke: (spec, team) => { + t.equal(spec, '@scoped/another', 'should use expected spec') + t.equal(team, 'myorg:myteam', 'should use expected team') + return true + }, }, }, }) - const npm = new Npm() await npm.exec('access', [ 'revoke', 'myorg:myteam', @@ -345,6 +336,7 @@ t.test('access revoke', async t => { }) t.test('access revoke missing team args', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', [ 'revoke', @@ -357,6 +349,7 @@ t.test('access revoke missing team args', async t => { }) t.test('access revoke malformed team arg', async t => { + const { npm } = await loadMockNpm(t) await t.rejects( npm.exec('access', [ 'revoke', @@ -370,30 +363,32 @@ t.test('access revoke malformed team arg', async t => { t.test('npm access ls-packages with no team', async t => { t.plan(1) - const { Npm } = mockNpm(t, { - libnpmaccess: { - lsPackages: (entity) => { - t.equal(entity, 'foo', 'should use expected entity') - return {} + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + lsPackages: (entity) => { + t.equal(entity, 'foo', 'should use expected entity') + return {} + }, }, + '../../lib/utils/get-identity.js': () => Promise.resolve('foo'), }, - '../../lib/utils/get-identity.js': () => Promise.resolve('foo'), }) - const npm = new Npm() await npm.exec('access', ['ls-packages']) }) t.test('access ls-packages on team', async t => { t.plan(1) - const { Npm } = mockNpm(t, { - libnpmaccess: { - lsPackages: (entity) => { - t.equal(entity, 'myorg:myteam', 'should use expected entity') - return {} + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + lsPackages: (entity) => { + t.equal(entity, 'myorg:myteam', 'should use expected entity') + return {} + }, }, }, }) - const npm = new Npm() await npm.exec('access', [ 'ls-packages', 'myorg:myteam', @@ -402,36 +397,36 @@ t.test('access ls-packages on team', async t => { t.test('access ls-collaborators on current', async t => { t.plan(1) - const testdir = t.testdir({ - 'package.json': JSON.stringify({ - name: 'yargs', - }), - }) - const { Npm } = mockNpm(t, { - libnpmaccess: { - lsCollaborators: (spec) => { - t.equal(spec, 'yargs', 'should use expected spec') - return {} + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + lsCollaborators: (spec) => { + t.equal(spec, 'yargs', 'should use expected spec') + return {} + }, }, }, + testdir: { + 'package.json': JSON.stringify({ + name: 'yargs', + }), + }, }) - const npm = new Npm() - await npm.load() - npm.prefix = testdir await npm.exec('access', ['ls-collaborators']) }) t.test('access ls-collaborators on spec', async t => { t.plan(1) - const { Npm } = mockNpm(t, { - libnpmaccess: { - lsCollaborators: (spec) => { - t.equal(spec, 'yargs', 'should use expected spec') - return {} + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmaccess: { + lsCollaborators: (spec) => { + t.equal(spec, 'yargs', 'should use expected spec') + return {} + }, }, }, }) - const npm = new Npm() await npm.exec('access', [ 'ls-collaborators', 'yargs', diff --git a/test/lib/commands/adduser.js b/test/lib/commands/adduser.js index 71d79ea9351b1..8a9358f9ab21a 100644 --- a/test/lib/commands/adduser.js +++ b/test/lib/commands/adduser.js @@ -20,6 +20,13 @@ const authDummy = (npm, options) => { throw new Error('did not pass full flatOptions to auth function') } + if (!options.log) { + // A quick to test to make sure a log gets passed to auth + // XXX: should be refactored with change to real mock npm + // https://github.com/npm/statusboard/issues/411 + throw new Error('pass log to auth') + } + return Promise.resolve({ message: 'success', newCreds: { @@ -71,6 +78,8 @@ const AddUser = t.mock('../../../lib/commands/adduser.js', { npmlog: { clearProgress: () => null, disableProgress: () => null, + }, + 'proc-log': { notice: (_, msg) => { registryOutput = msg }, diff --git a/test/lib/commands/audit.js b/test/lib/commands/audit.js index 3c87c76a8fe5f..05f268d6bcd0e 100644 --- a/test/lib/commands/audit.js +++ b/test/lib/commands/audit.js @@ -1,5 +1,5 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: _loadMockNpm } = require('../../fixtures/mock-npm') t.test('should audit using Arborist', async t => { let ARB_ARGS = null @@ -8,36 +8,35 @@ t.test('should audit using Arborist', async t => { let AUDIT_REPORT_CALLED = false let ARB_OBJ = null - const { Npm, outputs } = mockNpm(t, { - 'npm-audit-report': () => { - AUDIT_REPORT_CALLED = true - return { - report: 'there are vulnerabilities', - exitCode: 0, - } - }, - '@npmcli/arborist': function (args) { - ARB_ARGS = args - ARB_OBJ = this - this.audit = () => { - AUDIT_CALLED = true - this.auditReport = {} - } - }, - '../../lib/utils/reify-finish.js': (npm, arb) => { - if (arb !== ARB_OBJ) { - throw new Error('got wrong object passed to reify-output') - } + const loadMockNpm = (t) => _loadMockNpm(t, { + mocks: { + 'npm-audit-report': () => { + AUDIT_REPORT_CALLED = true + return { + report: 'there are vulnerabilities', + exitCode: 0, + } + }, + '@npmcli/arborist': function (args) { + ARB_ARGS = args + ARB_OBJ = this + this.audit = () => { + AUDIT_CALLED = true + this.auditReport = {} + } + }, + '../../lib/utils/reify-finish.js': (npm, arb) => { + if (arb !== ARB_OBJ) { + throw new Error('got wrong object passed to reify-output') + } - REIFY_FINISH_CALLED = true + REIFY_FINISH_CALLED = true + }, }, }) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir() - t.test('audit', async t => { + const { npm, outputs } = await loadMockNpm(t) await npm.exec('audit', []) t.match(ARB_ARGS, { audit: true, path: npm.prefix }) t.equal(AUDIT_CALLED, true, 'called audit') @@ -46,6 +45,7 @@ t.test('should audit using Arborist', async t => { }) t.test('audit fix', async t => { + const { npm } = await loadMockNpm(t) await npm.exec('audit', ['fix']) t.equal(REIFY_FINISH_CALLED, true, 'called reify output') }) @@ -53,69 +53,67 @@ t.test('should audit using Arborist', async t => { t.test('should audit - json', async t => { t.plan(1) - const { Npm } = mockNpm(t, { - 'npm-audit-report': (_, opts) => { - t.match(opts.reporter, 'json') - return { - report: 'there are vulnerabilities', - exitCode: 0, - } + const { npm } = await _loadMockNpm(t, { + mocks: { + 'npm-audit-report': (_, opts) => { + t.match(opts.reporter, 'json') + return { + report: 'there are vulnerabilities', + exitCode: 0, + } + }, + '@npmcli/arborist': function () { + this.audit = () => { + this.auditReport = {} + } + }, + '../../lib/utils/reify-output.js': () => {}, }, - '@npmcli/arborist': function () { - this.audit = () => { - this.auditReport = {} - } + config: { + json: true, }, - '../../lib/utils/reify-output.js': () => {}, }) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir() - npm.config.set('json', true) await npm.exec('audit', []) }) t.test('report endpoint error', async t => { - const { Npm, outputs, filteredLogs } = mockNpm(t, { - 'npm-audit-report': () => { - throw new Error('should not call audit report when there are errors') - }, - '@npmcli/arborist': function () { - this.audit = () => { - this.auditReport = { - error: { - message: 'hello, this didnt work', - method: 'POST', - uri: 'https://example.com/', - headers: { - head: ['ers'], + const loadMockNpm = (t, options) => _loadMockNpm(t, { + mocks: { + 'npm-audit-report': () => { + throw new Error('should not call audit report when there are errors') + }, + '@npmcli/arborist': function () { + this.audit = () => { + this.auditReport = { + error: { + message: 'hello, this didnt work', + method: 'POST', + uri: 'https://example.com/', + headers: { + head: ['ers'], + }, + statusCode: 420, + body: 'this is a string', }, - statusCode: 420, - body: 'this is a string', - // body: json ? { nope: 'lol' } : Buffer.from('i had a vuln but i eated it lol'), - }, + } } - } + }, + '../../lib/utils/reify-output.js': () => {}, }, - '../../lib/utils/reify-output.js': () => {}, + ...options, }) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir() - // npm.config.set('json', ) + t.test('json=false', async t => { + const { npm, outputs, logs } = await loadMockNpm(t, { config: { json: false } }) await t.rejects(npm.exec('audit', []), 'audit endpoint returned an error') - t.match(filteredLogs('warn'), ['hello, this didnt work']) + t.match(logs.warn, [['audit', 'hello, this didnt work']]) t.strictSame(outputs, [['this is a string']]) }) t.test('json=true', async t => { - t.teardown(() => { - npm.config.set('json', false) - }) - npm.config.set('json', true) + const { npm, outputs, logs } = await loadMockNpm(t, { config: { json: true } }) await t.rejects(npm.exec('audit', []), 'audit endpoint returned an error') - t.match(filteredLogs('warn'), ['hello, this didnt work']) + t.match(logs.warn, [['audit', 'hello, this didnt work']]) t.strictSame(outputs, [[ '{\n' + ' "message": "hello, this didnt work",\n' + @@ -135,8 +133,7 @@ t.test('report endpoint error', async t => { }) t.test('completion', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await _loadMockNpm(t) const audit = await npm.cmd('audit') t.test('fix', async t => { await t.resolveMatch( diff --git a/test/lib/commands/birthday.js b/test/lib/commands/birthday.js index 8c95dd57b2e3a..9156d3df09421 100644 --- a/test/lib/commands/birthday.js +++ b/test/lib/commands/birthday.js @@ -1,14 +1,15 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('birthday', async t => { t.plan(2) - const { Npm } = mockNpm(t, { - libnpmexec: ({ args, yes }) => { - t.ok(yes) - t.match(args, ['@npmcli/npm-birthday']) + const { npm } = await loadMockNpm(t, { + mocks: { + libnpmexec: ({ args, yes }) => { + t.ok(yes) + t.match(args, ['@npmcli/npm-birthday']) + }, }, }) - const npm = new Npm() await npm.exec('birthday', []) }) diff --git a/test/lib/commands/cache.js b/test/lib/commands/cache.js index 70a8ba1b2022f..fc92facff7187 100644 --- a/test/lib/commands/cache.js +++ b/test/lib/commands/cache.js @@ -12,11 +12,6 @@ const rimraf = (path, cb) => { } let logOutput = [] -const npmlog = { - silly: (...args) => { - logOutput.push(['silly', ...args]) - }, -} let tarballStreamSpec = '' let tarballStreamOpts = {} @@ -141,9 +136,16 @@ const cacache = { const Cache = t.mock('../../../lib/commands/cache.js', { cacache, - npmlog, pacote, rimraf, + 'proc-log': { + silly: (...args) => { + logOutput.push(['silly', ...args]) + }, + warn: (...args) => { + logOutput.push(['warn', ...args]) + }, + }, }) const npm = mockNpm({ @@ -153,11 +155,6 @@ const npm = mockNpm({ output: (msg) => { outputOutput.push(msg) }, - log: { - warn: (...args) => { - logOutput.push(['warn', ...args]) - }, - }, }) const cache = new Cache(npm) diff --git a/test/lib/commands/ci.js b/test/lib/commands/ci.js index 1091f9125b041..537d0784f8963 100644 --- a/test/lib/commands/ci.js +++ b/test/lib/commands/ci.js @@ -159,7 +159,7 @@ t.test('should throw if package-lock.json or npm-shrinkwrap missing', async t => const CI = t.mock('../../../lib/commands/ci.js', { '@npmcli/run-script': opts => {}, '../../../lib/utils/reify-finish.js': async () => {}, - npmlog: { + 'proc-log': { verbose: () => { t.ok(true, 'log fn called') }, diff --git a/test/lib/commands/completion.js b/test/lib/commands/completion.js index 51212f06d888e..dd571baf793a7 100644 --- a/test/lib/commands/completion.js +++ b/test/lib/commands/completion.js @@ -6,189 +6,153 @@ const completionScript = fs .readFileSync(path.resolve(__dirname, '../../../lib/utils/completion.sh'), { encoding: 'utf8' }) .replace(/^#!.*?\n/, '') -const { real: mockNpm } = require('../../fixtures/mock-npm') - -const { Npm, outputs } = mockNpm(t, { - '../../lib/utils/is-windows-shell.js': false, -}) -const npm = new Npm() +const { load: _loadMockNpm } = require('../../fixtures/mock-npm') +const mockGlobals = require('../../fixtures/mock-globals') + +const loadMockCompletion = async (t, o = {}) => { + const { globals, windows, ...options } = o + let resetGlobals = {} + if (globals) { + resetGlobals = mockGlobals(t, globals).reset + } + const res = await _loadMockNpm(t, { + mocks: { + '../../lib/utils/is-windows-shell.js': !!windows, + ...options.mocks, + }, + ...options, + }) + const completion = await res.npm.cmd('completion') + return { + resetGlobals, + completion, + ...res, + } +} + +const loadMockCompletionComp = async (t, word, line) => + loadMockCompletion(t, { + globals: { + 'process.env.COMP_CWORD': word, + 'process.env.COMP_LINE': line, + 'process.env.COMP_POINT': line.length, + }, + }) t.test('completion', async t => { - const completion = await npm.cmd('completion') t.test('completion completion', async t => { - const home = process.env.HOME - t.teardown(() => { - process.env.HOME = home - }) - - process.env.HOME = t.testdir({ - '.bashrc': '', - '.zshrc': '', + const { outputs, completion, prefix } = await loadMockCompletion(t, { + testdir: { + '.bashrc': 'aaa', + '.zshrc': 'aaa', + }, }) + mockGlobals(t, { 'process.env.HOME': prefix }) await completion.completion({ w: 2 }) t.matchSnapshot(outputs, 'both shells') }) t.test('completion completion no known shells', async t => { - const home = process.env.HOME - t.teardown(() => { - process.env.HOME = home - }) - - process.env.HOME = t.testdir() + const { outputs, completion, prefix } = await loadMockCompletion(t) + mockGlobals(t, { 'process.env.HOME': prefix }) await completion.completion({ w: 2 }) t.matchSnapshot(outputs, 'no responses') }) t.test('completion completion wrong word count', async t => { + const { outputs, completion } = await loadMockCompletion(t) + await completion.completion({ w: 3 }) t.matchSnapshot(outputs, 'no responses') }) t.test('dump script when completion is not being attempted', async t => { - const _write = process.stdout.write - const _on = process.stdout.on - t.teardown(() => { - process.stdout.write = _write - process.stdout.on = _on + let errorHandler, data + const { completion, resetGlobals } = await loadMockCompletion(t, { + globals: { + 'process.stdout.on': (event, handler) => { + errorHandler = handler + resetGlobals['process.stdout.on']() + }, + 'process.stdout.write': (chunk, callback) => { + data = chunk + process.nextTick(() => { + callback() + errorHandler({ errno: 'EPIPE' }) + }) + resetGlobals['process.stdout.write']() + }, + }, }) - let errorHandler - process.stdout.on = (event, handler) => { - errorHandler = handler - process.stdout.on = _on - } - - let data - process.stdout.write = (chunk, callback) => { - data = chunk - process.stdout.write = _write - process.nextTick(() => { - callback() - errorHandler({ errno: 'EPIPE' }) - }) - } - await completion.exec({}) - t.equal(data, completionScript, 'wrote the completion script') }) t.test('dump script exits correctly when EPIPE is emitted on stdout', async t => { - const _write = process.stdout.write - const _on = process.stdout.on - t.teardown(() => { - process.stdout.write = _write - process.stdout.on = _on + let errorHandler, data + const { completion, resetGlobals } = await loadMockCompletion(t, { + globals: { + 'process.stdout.on': (event, handler) => { + if (event === 'error') { + errorHandler = handler + } + resetGlobals['process.stdout.on']() + }, + 'process.stdout.write': (chunk, callback) => { + data = chunk + process.nextTick(() => { + errorHandler({ errno: 'EPIPE' }) + callback() + }) + resetGlobals['process.stdout.write']() + }, + }, }) - let errorHandler - process.stdout.on = (event, handler) => { - errorHandler = handler - process.stdout.on = _on - } - - let data - process.stdout.write = (chunk, callback) => { - data = chunk - process.stdout.write = _write - process.nextTick(() => { - errorHandler({ errno: 'EPIPE' }) - callback() - }) - } - await completion.exec({}) t.equal(data, completionScript, 'wrote the completion script') }) t.test('single command name', async t => { - process.env.COMP_CWORD = 1 - process.env.COMP_LINE = 'npm conf' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 1, 'npm conf') await completion.exec(['npm', 'conf']) t.matchSnapshot(outputs, 'single command name') }) t.test('multiple command names', async t => { - process.env.COMP_CWORD = 1 - process.env.COMP_LINE = 'npm a' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 1, 'npm a') await completion.exec(['npm', 'a']) t.matchSnapshot(outputs, 'multiple command names') }) t.test('completion of invalid command name does nothing', async t => { - process.env.COMP_CWORD = 1 - process.env.COMP_LINE = 'npm compute' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 1, 'npm compute') await completion.exec(['npm', 'compute']) t.matchSnapshot(outputs, 'no results') }) t.test('subcommand completion', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm access ' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 2, 'npm access ') await completion.exec(['npm', 'access', '']) t.matchSnapshot(outputs, 'subcommands') }) t.test('filtered subcommands', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm access p' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 2, 'npm access p') await completion.exec(['npm', 'access', 'p']) t.matchSnapshot(outputs, 'filtered subcommands') }) t.test('commands with no completion', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm adduser ' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 2, 'npm adduser ') // quotes around adduser are to ensure coverage when unescaping commands await completion.exec(['npm', "'adduser'", '']) @@ -196,63 +160,28 @@ t.test('completion', async t => { }) t.test('flags', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm install --v' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 2, 'npm install --v') await completion.exec(['npm', 'install', '--v']) - t.matchSnapshot(outputs, 'flags') }) t.test('--no- flags', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm install --no-v' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 2, 'npm install --no-v') await completion.exec(['npm', 'install', '--no-v']) - t.matchSnapshot(outputs, 'flags') }) t.test('double dashes escape from flag completion', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm -- install --' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 2, 'npm -- install --') await completion.exec(['npm', '--', 'install', '--']) - t.matchSnapshot(outputs, 'full command list') }) t.test('completion cannot complete options that take a value in mid-command', async t => { - process.env.COMP_CWORD = 2 - process.env.COMP_LINE = 'npm --registry install' - process.env.COMP_POINT = process.env.COMP_LINE.length - - t.teardown(() => { - delete process.env.COMP_CWORD - delete process.env.COMP_LINE - delete process.env.COMP_POINT - }) + const { outputs, completion } = await loadMockCompletionComp(t, 2, 'npm --registry install') await completion.exec(['npm', '--registry', 'install']) t.matchSnapshot(outputs, 'does not try to complete option arguments in the middle of a command') @@ -260,11 +189,7 @@ t.test('completion', async t => { }) t.test('windows without bash', async t => { - const { Npm, outputs } = mockNpm(t, { - '../../lib/utils/is-windows-shell.js': true, - }) - const npm = new Npm() - const completion = await npm.cmd('completion') + const { outputs, completion } = await loadMockCompletion(t, { windows: true }) await t.rejects( completion.exec({}), { code: 'ENOTSUP', message: /completion supported only in MINGW/ }, diff --git a/test/lib/commands/dedupe.js b/test/lib/commands/dedupe.js index 8fc0be06181e0..2e2fae238103f 100644 --- a/test/lib/commands/dedupe.js +++ b/test/lib/commands/dedupe.js @@ -1,11 +1,12 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('should throw in global mode', async (t) => { - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.config.set('global', true) + const { npm } = await loadMockNpm(t, { + config: { + global: true, + }, + }) t.rejects( npm.exec('dedupe', []), { code: 'EDEDUPEGLOBAL' }, @@ -15,39 +16,41 @@ t.test('should throw in global mode', async (t) => { t.test('should remove dupes using Arborist', async (t) => { t.plan(5) - const { Npm } = mockNpm(t, { - '@npmcli/arborist': function (args) { - t.ok(args, 'gets options object') - t.ok(args.path, 'gets path option') - t.ok(args.dryRun, 'gets dryRun from user') - this.dedupe = () => { - t.ok(true, 'dedupe is called') - } + const { npm } = await loadMockNpm(t, { + mocks: { + '@npmcli/arborist': function (args) { + t.ok(args, 'gets options object') + t.ok(args.path, 'gets path option') + t.ok(args.dryRun, 'gets dryRun from user') + this.dedupe = () => { + t.ok(true, 'dedupe is called') + } + }, + '../../lib/utils/reify-finish.js': (npm, arb) => { + t.ok(arb, 'gets arborist tree') + }, }, - '../../lib/utils/reify-finish.js': (npm, arb) => { - t.ok(arb, 'gets arborist tree') + config: { + 'dry-run': 'true', }, }) - const npm = new Npm() - await npm.load() - npm.config.set('prefix', 'foo') - npm.config.set('dry-run', 'true') await npm.exec('dedupe', []) }) t.test('should remove dupes using Arborist - no arguments', async (t) => { t.plan(1) - const { Npm } = mockNpm(t, { - '@npmcli/arborist': function (args) { - t.ok(args.dryRun, 'gets dryRun from config') - this.dedupe = () => {} + const { npm } = await loadMockNpm(t, { + mocks: { + '@npmcli/arborist': function (args) { + t.ok(args.dryRun, 'gets dryRun from config') + this.dedupe = () => {} + }, + '../../lib/utils/reify-output.js': () => {}, + '../../lib/utils/reify-finish.js': () => {}, + }, + config: { + 'dry-run': true, }, - '../../lib/utils/reify-output.js': () => {}, - '../../lib/utils/reify-finish.js': () => {}, }) - const npm = new Npm() - await npm.load() - npm.config.set('prefix', 'foo') - npm.config.set('dry-run', true) await npm.exec('dedupe', []) }) diff --git a/test/lib/commands/diff.js b/test/lib/commands/diff.js index 811936fe6d24c..ed0702e3784a6 100644 --- a/test/lib/commands/diff.js +++ b/test/lib/commands/diff.js @@ -31,7 +31,7 @@ const npm = mockNpm({ }) const mocks = { - npmlog: { info: noop, verbose: noop }, + 'proc-log': { info: noop, verbose: noop }, libnpmdiff: (...args) => libnpmdiff(...args), 'npm-registry-fetch': async () => ({}), '../../../lib/utils/usage.js': () => 'usage instructions', diff --git a/test/lib/commands/dist-tag.js b/test/lib/commands/dist-tag.js index 6b45dc1167557..756a09d7de002 100644 --- a/test/lib/commands/dist-tag.js +++ b/test/lib/commands/dist-tag.js @@ -61,7 +61,7 @@ const logger = (...msgs) => { } const DistTag = t.mock('../../../lib/commands/dist-tag.js', { - npmlog: { + 'proc-log': { error: logger, info: logger, verbose: logger, diff --git a/test/lib/commands/doctor.js b/test/lib/commands/doctor.js index e3ad5cc72692f..51b6111a0ae70 100644 --- a/test/lib/commands/doctor.js +++ b/test/lib/commands/doctor.js @@ -50,13 +50,13 @@ const logs = { info: [], } -const clearLogs = (obj = logs) => { +const clearLogs = () => { output.length = 0 - for (const key in obj) { - if (Array.isArray(obj[key])) { - obj[key].length = 0 + for (const key in logs) { + if (Array.isArray(logs[key])) { + logs[key].length = 0 } else { - delete obj[key] + delete logs[key] } } } @@ -65,13 +65,41 @@ const npm = { flatOptions: { registry: 'https://registry.npmjs.org/', }, - log: { + version: '7.1.0', + output: data => { + output.push(data) + }, +} + +let latestNpm = npm.version +const pacote = { + manifest: async () => { + return { version: latestNpm } + }, +} + +let verifyResponse = { verifiedCount: 1, verifiedContent: 1 } +const cacache = { + verify: async () => { + return verifyResponse + }, +} + +const mocks = { + '../../../lib/utils/is-windows.js': false, + '../../../lib/utils/ping.js': ping, + cacache, + pacote, + 'make-fetch-happen': fetch, + which, + 'proc-log': { info: msg => { logs.info.push(msg) }, + }, + npmlog: { newItem: name => { logs[name] = {} - return { info: (_, msg) => { if (!logs[name].info) { @@ -109,33 +137,11 @@ const npm = { error: 0, }, }, - version: '7.1.0', - output: data => { - output.push(data) - }, -} -let latestNpm = npm.version -const pacote = { - manifest: async () => { - return { version: latestNpm } - }, -} - -let verifyResponse = { verifiedCount: 1, verifiedContent: 1 } -const cacache = { - verify: async () => { - return verifyResponse - }, } const Doctor = t.mock('../../../lib/commands/doctor.js', { - '../../../lib/utils/is-windows.js': false, - '../../../lib/utils/ping.js': ping, - cacache, - pacote, - 'make-fetch-happen': fetch, - which, + ...mocks, }) const doctor = new Doctor(npm) @@ -205,7 +211,7 @@ t.test('node versions', t => { npm.globalDir = dir npm.localBin = dir npm.globalBin = dir - npm.log.level = 'info' + mocks.npmlog.level = 'info' st.teardown(() => { delete npm.cache @@ -214,7 +220,7 @@ t.test('node versions', t => { delete npm.globalDir delete npm.localBin delete npm.globalBin - npm.log.level = 'error' + mocks.npmlog.level = 'error' clearLogs() }) @@ -293,12 +299,8 @@ t.test('node versions', t => { vt.test('npm doctor skips some tests in windows', async st => { const WinDoctor = t.mock('../../../lib/commands/doctor.js', { + ...mocks, '../../../lib/utils/is-windows.js': true, - '../../../lib/utils/ping.js': ping, - cacache, - pacote, - 'make-fetch-happen': fetch, - which, }) const winDoctor = new WinDoctor(npm) @@ -592,12 +594,7 @@ t.test('node versions', t => { } const Doctor = t.mock('../../../lib/commands/doctor.js', { - '../../../lib/utils/is-windows.js': false, - '../../../lib/utils/ping.js': ping, - cacache, - pacote, - 'make-fetch-happen': fetch, - which, + ...mocks, fs, }) const doctor = new Doctor(npm) diff --git a/test/lib/commands/exec.js b/test/lib/commands/exec.js index 4ab26568f1091..3c75c1d8d8273 100644 --- a/test/lib/commands/exec.js +++ b/test/lib/commands/exec.js @@ -44,17 +44,6 @@ const npm = mockNpm({ localPrefix: 'local-prefix', localBin: 'local-bin', globalBin: 'global-bin', - log: { - disableProgress: () => { - PROGRESS_ENABLED = false - }, - enableProgress: () => { - PROGRESS_ENABLED = true - }, - warn: (...args) => { - LOG_WARN.push(args) - }, - }, }) const RUN_SCRIPTS = [] @@ -87,6 +76,23 @@ const PATH = require('../../../lib/utils/path.js') let CI_NAME = 'travis-ci' +const log = { + 'proc-log': { + warn: (...args) => { + LOG_WARN.push(args) + }, + }, + npmlog: { + disableProgress: () => { + PROGRESS_ENABLED = false + }, + enableProgress: () => { + PROGRESS_ENABLED = true + }, + clearProgress: () => {}, + }, +} + const mocks = { libnpmexec: t.mock('libnpmexec', { '@npmcli/arborist': Arborist, @@ -95,7 +101,9 @@ const mocks = { pacote, read, 'mkdirp-infer-owner': mkdirp, + ...log, }), + ...log, } const Exec = t.mock('../../../lib/commands/exec.js', mocks) const exec = new Exec(npm) diff --git a/test/lib/commands/explore.js b/test/lib/commands/explore.js index b2e7be2136b76..d1355d76712a6 100644 --- a/test/lib/commands/explore.js +++ b/test/lib/commands/explore.js @@ -51,14 +51,17 @@ const getExplore = (windows) => { path: require('path')[windows ? 'win32' : 'posix'], 'read-package-json-fast': mockRPJ, '@npmcli/run-script': mockRunScript, - }) - const npm = { - dir: windows ? 'c:\\npm\\dir' : '/npm/dir', - log: { + 'proc-log': { error: (...msg) => logs.push(msg), + warn: () => {}, + }, + npmlog: { disableProgress: () => {}, enableProgress: () => {}, }, + }) + const npm = { + dir: windows ? 'c:\\npm\\dir' : '/npm/dir', flatOptions: { shell: 'shell-command', }, diff --git a/test/lib/commands/find-dupes.js b/test/lib/commands/find-dupes.js index c1b9c71df5a79..06bd097b6ca59 100644 --- a/test/lib/commands/find-dupes.js +++ b/test/lib/commands/find-dupes.js @@ -1,27 +1,28 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('should run dedupe in dryRun mode', async (t) => { t.plan(5) - const { Npm } = 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') - } + const { npm } = await loadMockNpm(t, { + mocks: { + '@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') + } + }, + '../../lib/utils/reify-finish.js': (npm, arb) => { + t.ok(arb, 'gets arborist tree') + }, }, - '../../lib/utils/reify-finish.js': (npm, arb) => { - t.ok(arb, 'gets arborist tree') + config: { + // explicitly set to false so we can be 100% sure it's always true when it + // hits arborist + 'dry-run': false, }, }) - const npm = new Npm() - 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 npm.exec('find-dupes', []) }) diff --git a/test/lib/commands/get.js b/test/lib/commands/get.js index ba9e770e3e0a1..597cccc3ff0ba 100644 --- a/test/lib/commands/get.js +++ b/test/lib/commands/get.js @@ -1,12 +1,10 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('should retrieve values from config', async t => { - const { joinedOutput, Npm } = mockNpm(t) - const npm = new Npm() + const { joinedOutput, npm } = await loadMockNpm(t) const name = 'editor' const value = 'vigor' - await npm.load() npm.config.set(name, value) await npm.exec('get', [name]) t.equal( diff --git a/test/lib/commands/init.js b/test/lib/commands/init.js index 74b33168ade58..215ebc58118e7 100644 --- a/test/lib/commands/init.js +++ b/test/lib/commands/init.js @@ -3,14 +3,6 @@ const fs = require('fs') const { resolve } = require('path') const { fake: mockNpm } = require('../../fixtures/mock-npm') -const npmLog = { - disableProgress: () => null, - enableProgress: () => null, - info: () => null, - pause: () => null, - resume: () => null, - silly: () => null, -} const config = { cache: 'bad-cache-dir', 'init-module': '~/.npm-init.js', @@ -23,10 +15,19 @@ const flatOptions = { const npm = mockNpm({ flatOptions, config, - log: npmLog, }) const mocks = { '../../../lib/utils/usage.js': () => 'usage instructions', + npmlog: { + disableProgress: () => null, + enableProgress: () => null, + }, + 'proc-log': { + info: () => null, + pause: () => null, + resume: () => null, + silly: () => null, + }, } const Init = t.mock('../../../lib/commands/init.js', mocks) const init = new Init(npm) @@ -37,7 +38,6 @@ const noop = () => {} t.afterEach(() => { config.yes = true config.package = undefined - npm.log = npmLog process.chdir(_cwd) console.log = _consolelog }) @@ -251,13 +251,15 @@ t.test('npm init cancel', async t => { 'init-package-json': (dir, initFile, config, cb) => cb( new Error('canceled') ), + 'proc-log': { + ...mocks['proc-log'], + warn: (title, msg) => { + t.equal(title, 'init', 'should have init title') + t.equal(msg, 'canceled', 'should log canceled') + }, + }, }) const init = new Init(npm) - npm.log = { ...npm.log } - npm.log.warn = (title, msg) => { - t.equal(title, 'init', 'should have init title') - t.equal(msg, 'canceled', 'should log canceled') - } process.chdir(npm.localPrefix) await init.exec([]) diff --git a/test/lib/commands/install.js b/test/lib/commands/install.js index 994684596aacf..d5db3af673caa 100644 --- a/test/lib/commands/install.js +++ b/test/lib/commands/install.js @@ -1,7 +1,10 @@ const t = require('tap') const path = require('path') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: _loadMockNpm } = require('../../fixtures/mock-npm') + +// Make less churn in the test to pass in mocks only signature +const loadMockNpm = (t, mocks) => _loadMockNpm(t, { mocks }) t.test('with args, dev=true', async t => { const SCRIPTS = [] @@ -9,7 +12,7 @@ t.test('with args, dev=true', async t => { let REIFY_CALLED = false let ARB_OBJ = null - const { Npm, filteredLogs } = mockNpm(t, { + const { npm, logs } = await loadMockNpm(t, { '@npmcli/run-script': ({ event }) => { SCRIPTS.push(event) }, @@ -27,8 +30,6 @@ t.test('with args, dev=true', async t => { }, }) - const npm = new Npm() - await npm.load() // This is here because CI calls tests with `--ignore-scripts`, which config // picks up from argv npm.config.set('ignore-scripts', false) @@ -41,8 +42,8 @@ t.test('with args, dev=true', async t => { await npm.exec('install', ['fizzbuzz']) t.match( - filteredLogs('warn'), - ['Usage of the `--dev` option is deprecated. Use `--include=dev` instead.'] + logs.warn, + [['install', 'Usage of the `--dev` option is deprecated. Use `--include=dev` instead.']] ) t.match( ARB_ARGS, @@ -59,7 +60,7 @@ t.test('without args', async t => { let REIFY_CALLED = false let ARB_OBJ = null - const { Npm } = mockNpm(t, { + const { npm } = await loadMockNpm(t, { '@npmcli/run-script': ({ event }) => { SCRIPTS.push(event) }, @@ -77,8 +78,6 @@ t.test('without args', async t => { }, }) - const npm = new Npm() - await npm.load() npm.prefix = path.resolve(t.testdir({})) npm.config.set('ignore-scripts', false) await npm.exec('install', []) @@ -98,7 +97,7 @@ t.test('without args', async t => { t.test('should ignore scripts with --ignore-scripts', async t => { const SCRIPTS = [] let REIFY_CALLED = false - const { Npm } = mockNpm(t, { + const { npm } = await loadMockNpm(t, { '../../lib/utils/reify-finish.js': async () => {}, '@npmcli/run-script': ({ event }) => { SCRIPTS.push(event) @@ -109,8 +108,6 @@ t.test('should ignore scripts with --ignore-scripts', async t => { } }, }) - const npm = new Npm() - await npm.load() npm.config.set('ignore-scripts', true) npm.prefix = path.resolve(t.testdir({})) await npm.exec('install', []) @@ -122,7 +119,7 @@ t.test('should install globally using Arborist', async t => { const SCRIPTS = [] let ARB_ARGS = null let REIFY_CALLED - const { Npm } = mockNpm(t, { + const { npm } = await loadMockNpm(t, { '@npmcli/run-script': ({ event }) => { SCRIPTS.push(event) }, @@ -134,8 +131,6 @@ t.test('should install globally using Arborist', async t => { } }, }) - const npm = new Npm() - await npm.load() npm.config.set('global', true) npm.globalPrefix = path.resolve(t.testdir({})) await npm.exec('install', []) @@ -148,7 +143,7 @@ t.test('should install globally using Arborist', async t => { }) t.test('npm i -g npm engines check success', async t => { - const { Npm } = mockNpm(t, { + const { npm } = await loadMockNpm(t, { '../../lib/utils/reify-finish.js': async () => {}, '@npmcli/arborist': function () { this.reify = () => {} @@ -164,8 +159,6 @@ t.test('npm i -g npm engines check success', async t => { }, }, }) - const npm = new Npm() - await npm.load() npm.globalDir = t.testdir({}) npm.config.set('global', true) await npm.exec('install', ['npm']) @@ -173,7 +166,7 @@ t.test('npm i -g npm engines check success', async t => { }) t.test('npm i -g npm engines check failure', async t => { - const { Npm } = mockNpm(t, { + const { npm } = await loadMockNpm(t, { pacote: { manifest: () => { return { @@ -186,8 +179,6 @@ t.test('npm i -g npm engines check failure', async t => { }, }, }) - const npm = new Npm() - await npm.load() npm.globalDir = t.testdir({}) npm.config.set('global', true) await t.rejects( @@ -208,7 +199,7 @@ t.test('npm i -g npm engines check failure', async t => { }) t.test('npm i -g npm engines check failure forced override', async t => { - const { Npm } = mockNpm(t, { + const { npm } = await loadMockNpm(t, { '../../lib/utils/reify-finish.js': async () => {}, '@npmcli/arborist': function () { this.reify = () => {} @@ -225,8 +216,6 @@ t.test('npm i -g npm engines check failure forced override', async t => { }, }, }) - const npm = new Npm() - await npm.load() npm.globalDir = t.testdir({}) npm.config.set('global', true) npm.config.set('force', true) @@ -235,7 +224,7 @@ t.test('npm i -g npm engines check failure forced override', async t => { }) t.test('npm i -g npm@version engines check failure', async t => { - const { Npm } = mockNpm(t, { + const { npm } = await loadMockNpm(t, { pacote: { manifest: () => { return { @@ -248,8 +237,6 @@ t.test('npm i -g npm@version engines check failure', async t => { }, }, }) - const npm = new Npm() - await npm.load() npm.globalDir = t.testdir({}) npm.config.set('global', true) await t.rejects( @@ -283,8 +270,7 @@ t.test('completion', async t => { }) t.test('completion to folder - has a match', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await _loadMockNpm(t, { load: false }) const install = await npm.cmd('install') process.chdir(testdir) const res = await install.completion({ partialWord: './ar' }) @@ -292,16 +278,14 @@ t.test('completion', async t => { }) t.test('completion to folder - invalid dir', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await _loadMockNpm(t, { load: false }) const install = await npm.cmd('install') const res = await install.completion({ partialWord: '/does/not/exist' }) t.strictSame(res, [], 'invalid dir: no matching') }) t.test('completion to folder - no matches', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await _loadMockNpm(t, { load: false }) const install = await npm.cmd('install') process.chdir(testdir) const res = await install.completion({ partialWord: './pa' }) @@ -309,8 +293,7 @@ t.test('completion', async t => { }) t.test('completion to folder - match is not a package', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await _loadMockNpm(t, { load: false }) const install = await npm.cmd('install') process.chdir(testdir) const res = await install.completion({ partialWord: './othe' }) @@ -318,8 +301,7 @@ t.test('completion', async t => { }) t.test('completion to url', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await _loadMockNpm(t, { load: false }) const install = await npm.cmd('install') process.chdir(testdir) const res = await install.completion({ partialWord: 'http://path/to/url' }) @@ -327,8 +309,7 @@ t.test('completion', async t => { }) t.test('no /', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await _loadMockNpm(t, { load: false }) const install = await npm.cmd('install') process.chdir(testdir) const res = await install.completion({ partialWord: 'toto' }) @@ -336,8 +317,7 @@ t.test('completion', async t => { }) t.test('only /', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await _loadMockNpm(t, { load: false }) const install = await npm.cmd('install') process.chdir(testdir) const res = await install.completion({ partialWord: '/' }) diff --git a/test/lib/commands/logout.js b/test/lib/commands/logout.js index 39ef86c843e2b..ee01e7500d141 100644 --- a/test/lib/commands/logout.js +++ b/test/lib/commands/logout.js @@ -10,45 +10,31 @@ const flatOptions = { scope: '', } const npm = mockNpm({ config, flatOptions }) - -const npmlog = {} - let result = null -const npmFetch = (url, opts) => { - result = { url, opts } -} -const mocks = { - npmlog, - 'npm-registry-fetch': npmFetch, +const mockLogout = (otherMocks) => { + const Logout = t.mock('../../../lib/commands/logout.js', { + 'npm-registry-fetch': (url, opts) => { + result = { url, opts } + }, + ...otherMocks, + }) + return new Logout(npm) } -const Logout = t.mock('../../../lib/commands/logout.js', mocks) -const logout = new Logout(npm) +t.afterEach(() => { + delete flatOptions.token + result = null + config.clearCredentialsByURI = null + config.delete = null + config.save = null +}) t.test('token logout', async t => { - t.teardown(() => { - delete flatOptions.token - result = null - mocks['npm-registry-fetch'] = null - config.clearCredentialsByURI = null - config.delete = null - config.save = null - npmlog.verbose = null - }) t.plan(5) flatOptions['//registry.npmjs.org/:_authToken'] = '@foo/' - npmlog.verbose = (title, msg) => { - t.equal(title, 'logout', 'should have correcct log prefix') - t.equal( - msg, - 'clearing token for https://registry.npmjs.org/', - 'should log message with correct registry' - ) - } - npm.config.clearCredentialsByURI = registry => { t.equal( registry, @@ -61,6 +47,19 @@ t.test('token logout', async t => { t.equal(type, 'user', 'should save to user config') } + const logout = mockLogout({ + 'proc-log': { + verbose: (title, msg) => { + t.equal(title, 'logout', 'should have correcct log prefix') + t.equal( + msg, + 'clearing token for https://registry.npmjs.org/', + 'should log message with correct registry' + ) + }, + }, + }) + await logout.exec([]) t.same( @@ -87,12 +86,11 @@ t.test('token scoped logout', async t => { delete config['@myscope:registry'] delete flatOptions.scope result = null - mocks['npm-registry-fetch'] = null config.clearCredentialsByURI = null config.delete = null config.save = null - npmlog.verbose = null }) + t.plan(7) flatOptions['//diff-registry.npmjs.com/:_authToken'] = '@bar/' @@ -102,15 +100,6 @@ t.test('token scoped logout', async t => { flatOptions.scope = '@myscope' flatOptions['@myscope:registry'] = 'https://diff-registry.npmjs.com/' - npmlog.verbose = (title, msg) => { - t.equal(title, 'logout', 'should have correcct log prefix') - t.equal( - msg, - 'clearing token for https://diff-registry.npmjs.com/', - 'should log message with correct registry' - ) - } - npm.config.clearCredentialsByURI = registry => { t.equal( registry, @@ -128,6 +117,19 @@ t.test('token scoped logout', async t => { t.equal(type, 'user', 'should save to user config') } + const logout = mockLogout({ + 'proc-log': { + verbose: (title, msg) => { + t.equal(title, 'logout', 'should have correcct log prefix') + t.equal( + msg, + 'clearing token for https://diff-registry.npmjs.com/', + 'should log message with correct registry' + ) + }, + }, + }) + await logout.exec([]) t.same( @@ -154,29 +156,34 @@ t.test('user/pass logout', async t => { delete flatOptions['//registry.npmjs.org/:_password'] npm.config.clearCredentialsByURI = null npm.config.save = null - npmlog.verbose = null }) t.plan(2) flatOptions['//registry.npmjs.org/:username'] = 'foo' flatOptions['//registry.npmjs.org/:_password'] = 'bar' - npmlog.verbose = (title, msg) => { - t.equal(title, 'logout', 'should have correct log prefix') - t.equal( - msg, - 'clearing user credentials for https://registry.npmjs.org/', - 'should log message with correct registry' - ) - } - npm.config.clearCredentialsByURI = () => null npm.config.save = () => null + const logout = mockLogout({ + 'proc-log': { + verbose: (title, msg) => { + t.equal(title, 'logout', 'should have correct log prefix') + t.equal( + msg, + 'clearing user credentials for https://registry.npmjs.org/', + 'should log message with correct registry' + ) + }, + }, + }) + await logout.exec([]) }) t.test('missing credentials', async t => { + const logout = mockLogout() + await t.rejects( logout.exec([]), { @@ -191,11 +198,9 @@ t.test('ignore invalid scoped registry config', async t => { t.teardown(() => { delete flatOptions.token result = null - mocks['npm-registry-fetch'] = null config.clearCredentialsByURI = null config.delete = null config.save = null - npmlog.verbose = null }) t.plan(4) @@ -203,15 +208,6 @@ t.test('ignore invalid scoped registry config', async t => { config.scope = '@myscope' flatOptions['@myscope:registry'] = '' - npmlog.verbose = (title, msg) => { - t.equal(title, 'logout', 'should have correcct log prefix') - t.equal( - msg, - 'clearing token for https://registry.npmjs.org/', - 'should log message with correct registry' - ) - } - npm.config.clearCredentialsByURI = registry => { t.equal( registry, @@ -223,6 +219,19 @@ t.test('ignore invalid scoped registry config', async t => { npm.config.delete = () => null npm.config.save = () => null + const logout = mockLogout({ + 'proc-log': { + verbose: (title, msg) => { + t.equal(title, 'logout', 'should have correcct log prefix') + t.equal( + msg, + 'clearing token for https://registry.npmjs.org/', + 'should log message with correct registry' + ) + }, + }, + }) + await logout.exec([]) t.same( diff --git a/test/lib/commands/owner.js b/test/lib/commands/owner.js index 8645b349f82fe..b5d4d1584289d 100644 --- a/test/lib/commands/owner.js +++ b/test/lib/commands/owner.js @@ -14,11 +14,11 @@ const npm = mockNpm({ }) const npmFetch = { json: noop } -const npmlog = { error: noop, info: noop, verbose: noop } +const log = { error: noop, info: noop, verbose: noop } const pacote = { packument: noop } const mocks = { - npmlog, + 'proc-log': log, 'npm-registry-fetch': npmFetch, pacote, '../../../lib/utils/otplease.js': async (opts, fn) => fn({ otp: '123456', opts }), @@ -97,7 +97,7 @@ t.test('owner ls no args no cwd package', async t => { result = '' t.teardown(() => { result = '' - npmlog.error = noop + log.error = noop }) await t.rejects( @@ -114,14 +114,14 @@ t.test('owner ls fails to retrieve packument', async t => { pacote.packument = () => { throw new Error('ERR') } - npmlog.error = (title, msg, pkgName) => { + log.error = (title, msg, pkgName) => { t.equal(title, 'owner ls', 'should list npm owner ls title') t.equal(msg, "Couldn't get owner data", 'should use expected msg') t.equal(pkgName, '@npmcli/map-workspaces', 'should use pkg name') } t.teardown(() => { result = '' - npmlog.error = noop + log.error = noop pacote.packument = noop }) @@ -276,7 +276,7 @@ t.test('owner add already an owner', async t => { t.plan(2) result = '' - npmlog.info = (title, msg) => { + log.info = (title, msg) => { t.equal(title, 'owner add', 'should use expected title') t.equal( msg, @@ -304,7 +304,7 @@ t.test('owner add already an owner', async t => { } t.teardown(() => { result = '' - npmlog.info = noop + log.info = noop npmFetch.json = noop pacote.packument = noop }) @@ -385,7 +385,7 @@ t.test('owner add fails to retrieve user info', async t => { t.plan(3) result = '' - npmlog.error = (title, msg) => { + log.error = (title, msg) => { t.equal(title, 'owner mutate', 'should use expected title') t.equal(msg, 'Error getting user data for foo') } @@ -406,7 +406,7 @@ t.test('owner add fails to retrieve user info', async t => { }) t.teardown(() => { result = '' - npmlog.error = noop + log.error = noop npmFetch.json = noop pacote.packument = noop }) @@ -552,7 +552,7 @@ t.test('owner rm not a current owner', async t => { t.plan(2) result = '' - npmlog.info = (title, msg) => { + log.info = (title, msg) => { t.equal(title, 'owner rm', 'should log expected title') t.equal(msg, 'Not a package owner: foo', 'should log.info not a package owner msg') } @@ -578,7 +578,7 @@ t.test('owner rm not a current owner', async t => { } t.teardown(() => { result = '' - npmlog.info = noop + log.info = noop npmFetch.json = noop pacote.packument = noop }) diff --git a/test/lib/commands/pack.js b/test/lib/commands/pack.js index bc88772086322..21057e207953e 100644 --- a/test/lib/commands/pack.js +++ b/test/lib/commands/pack.js @@ -1,5 +1,5 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') const path = require('path') const fs = require('fs') @@ -9,33 +9,31 @@ t.afterEach(t => { }) t.test('should pack current directory with no arguments', async t => { - const { Npm, outputs, filteredLogs } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir({ - 'package.json': JSON.stringify({ - name: 'test-package', - version: '1.0.0', - }), + const { npm, outputs, logs } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'test-package', + version: '1.0.0', + }), + }, }) process.chdir(npm.prefix) await npm.exec('pack', []) const filename = 'test-package-1.0.0.tgz' t.strictSame(outputs, [[filename]]) - t.matchSnapshot(filteredLogs('notice'), 'logs pack contents') + t.matchSnapshot(logs.notice.map(([, m]) => m), 'logs pack contents') t.ok(fs.statSync(path.resolve(npm.prefix, filename))) }) t.test('follows pack-destination config', async t => { - const { Npm, outputs } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir({ - 'package.json': JSON.stringify({ - name: 'test-package', - version: '1.0.0', - }), - 'tar-destination': {}, + const { npm, outputs } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'test-package', + version: '1.0.0', + }), + 'tar-destination': {}, + }, }) process.chdir(npm.prefix) npm.config.set('pack-destination', path.join(npm.prefix, 'tar-destination')) @@ -46,14 +44,13 @@ t.test('follows pack-destination config', async t => { }) t.test('should pack given directory for scoped package', async t => { - const { Npm, outputs } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir({ - 'package.json': JSON.stringify({ - name: '@npm/test-package', - version: '1.0.0', - }), + const { npm, outputs } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: '@npm/test-package', + version: '1.0.0', + }), + }, }) process.chdir(npm.prefix) await npm.exec('pack', []) @@ -63,49 +60,46 @@ t.test('should pack given directory for scoped package', async t => { }) t.test('should log output as valid json', async t => { - const { Npm, outputs, filteredLogs } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir({ - 'package.json': JSON.stringify({ - name: 'test-package', - version: '1.0.0', - }), + const { npm, outputs, logs } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'test-package', + version: '1.0.0', + }), + }, }) process.chdir(npm.prefix) npm.config.set('json', true) await npm.exec('pack', []) const filename = 'test-package-1.0.0.tgz' t.matchSnapshot(outputs.map(JSON.parse), 'outputs as json') - t.matchSnapshot(filteredLogs('notice'), 'logs pack contents') + t.matchSnapshot(logs.notice.map(([, m]) => m), 'logs pack contents') t.ok(fs.statSync(path.resolve(npm.prefix, filename))) }) t.test('dry run', async t => { - const { Npm, outputs, filteredLogs } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir({ - 'package.json': JSON.stringify({ - name: 'test-package', - version: '1.0.0', - }), + const { npm, outputs, logs } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'test-package', + version: '1.0.0', + }), + }, }) npm.config.set('dry-run', true) process.chdir(npm.prefix) await npm.exec('pack', []) const filename = 'test-package-1.0.0.tgz' t.strictSame(outputs, [[filename]]) - t.matchSnapshot(filteredLogs('notice'), 'logs pack contents') + t.matchSnapshot(logs.notice.map(([, m]) => m), 'logs pack contents') t.throws(() => fs.statSync(path.resolve(npm.prefix, filename))) }) t.test('invalid packument', async t => { - const { Npm, outputs } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir({ - 'package.json': '{}', + const { npm, outputs } = await loadMockNpm(t, { + testdir: { + 'package.json': '{}', + }, }) process.chdir(npm.prefix) await t.rejects( @@ -116,52 +110,58 @@ t.test('invalid packument', async t => { }) t.test('workspaces', async t => { - const { Npm, outputs } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.prefix = t.testdir({ - 'package.json': JSON.stringify( - { - name: 'workspaces-test', - version: '1.0.0', - workspaces: ['workspace-a', 'workspace-b'], + const loadWorkspaces = (t) => loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify( + { + name: 'workspaces-test', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b'], + }, + null, + 2 + ), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + version: '1.0.0', + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + version: '1.0.0', + }), }, - null, - 2 - ), - 'workspace-a': { - 'package.json': JSON.stringify({ - name: 'workspace-a', - version: '1.0.0', - }), }, - 'workspace-b': { - 'package.json': JSON.stringify({ - name: 'workspace-b', - version: '1.0.0', - }), + config: { + workspaces: true, }, }) - npm.config.set('workspaces', true) + t.test('all workspaces', async t => { + const { npm, outputs } = await loadWorkspaces(t) process.chdir(npm.prefix) await npm.exec('pack', []) t.strictSame(outputs, [['workspace-a-1.0.0.tgz'], ['workspace-b-1.0.0.tgz']]) }) t.test('all workspaces, `.` first arg', async t => { + const { npm, outputs } = await loadWorkspaces(t) process.chdir(npm.prefix) await npm.exec('pack', ['.']) t.strictSame(outputs, [['workspace-a-1.0.0.tgz'], ['workspace-b-1.0.0.tgz']]) }) t.test('one workspace', async t => { + const { npm, outputs } = await loadWorkspaces(t) process.chdir(npm.prefix) await npm.exec('pack', ['workspace-a']) t.strictSame(outputs, [['workspace-a-1.0.0.tgz']]) }) t.test('specific package', async t => { + const { npm, outputs } = await loadWorkspaces(t) process.chdir(npm.prefix) await npm.exec('pack', [npm.prefix]) t.strictSame(outputs, [['workspaces-test-1.0.0.tgz']]) diff --git a/test/lib/commands/ping.js b/test/lib/commands/ping.js index 7011c709b0bac..f808e0ac3ba2a 100644 --- a/test/lib/commands/ping.js +++ b/test/lib/commands/ping.js @@ -11,7 +11,7 @@ t.test('pings', async t => { t.equal(spec.registry, registry, 'passes flatOptions') return {} }, - npmlog: { + 'proc-log': { notice: (type, spec) => { ++noticeCalls if (noticeCalls === 1) { @@ -45,7 +45,7 @@ t.test('pings and logs details', async t => { t.equal(spec.registry, registry, 'passes flatOptions') return details }, - npmlog: { + 'proc-log': { notice: (type, spec) => { ++noticeCalls if (noticeCalls === 1) { @@ -83,7 +83,7 @@ t.test('pings and returns json', async t => { t.equal(spec.registry, registry, 'passes flatOptions') return details }, - npmlog: { + 'proc-log': { notice: (type, spec) => { ++noticeCalls if (noticeCalls === 1) { diff --git a/test/lib/commands/prefix.js b/test/lib/commands/prefix.js index 6f059e73a7ec5..e8295cf6a5b3c 100644 --- a/test/lib/commands/prefix.js +++ b/test/lib/commands/prefix.js @@ -1,9 +1,8 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('prefix', async t => { - const { joinedOutput, Npm } = mockNpm(t) - const npm = new Npm() + const { joinedOutput, npm } = await loadMockNpm(t, { load: false }) await npm.exec('prefix', []) t.equal( joinedOutput(), diff --git a/test/lib/commands/profile.js b/test/lib/commands/profile.js index 6554ca89e40f8..0f16c1db1d295 100644 --- a/test/lib/commands/profile.js +++ b/test/lib/commands/profile.js @@ -22,6 +22,8 @@ const mocks = { ansistyles: { bright: a => a }, npmlog: { gauge: { show () {} }, + }, + 'proc-log': { info () {}, notice () {}, warn () {}, @@ -489,23 +491,23 @@ t.test('profile set ', t => { }, } - const npmlog = { - gauge: { - show () {}, - }, - warn (title, msg) { - t.equal(title, 'profile', 'should use expected profile') - t.equal( - msg, - 'Passwords do not match, please try again.', - 'should log password mismatch message' - ) - }, - } - const Profile = t.mock('../../../lib/commands/profile.js', { ...mocks, - npmlog, + npmlog: { + gauge: { + show () {}, + }, + }, + 'proc-log': { + warn (title, msg) { + t.equal(title, 'profile', 'should use expected profile') + t.equal( + msg, + 'Passwords do not match, please try again.', + 'should log password mismatch message' + ) + }, + }, 'npm-profile': npmProfile, '../../../lib/utils/read-user-info.js': readUserInfo, }) diff --git a/test/lib/commands/prune.js b/test/lib/commands/prune.js index 49d5ab9be3514..a7f56547b105d 100644 --- a/test/lib/commands/prune.js +++ b/test/lib/commands/prune.js @@ -1,20 +1,22 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('should prune using Arborist', async (t) => { t.plan(4) - const { Npm } = mockNpm(t, { - '@npmcli/arborist': function (args) { - t.ok(args, 'gets options object') - t.ok(args.path, 'gets path option') - this.prune = () => { - t.ok(true, 'prune is called') - } - }, - '../../lib/utils/reify-finish.js': (arb) => { - t.ok(arb, 'gets arborist tree') + const { npm } = await loadMockNpm(t, { + load: false, + mocks: { + '@npmcli/arborist': function (args) { + t.ok(args, 'gets options object') + t.ok(args.path, 'gets path option') + this.prune = () => { + t.ok(true, 'prune is called') + } + }, + '../../lib/utils/reify-finish.js': (arb) => { + t.ok(arb, 'gets arborist tree') + }, }, }) - const npm = new Npm() await npm.exec('prune', []) }) diff --git a/test/lib/commands/publish.js b/test/lib/commands/publish.js index 5f4fb401064c2..1178cd6ee1edf 100644 --- a/test/lib/commands/publish.js +++ b/test/lib/commands/publish.js @@ -1,13 +1,15 @@ const t = require('tap') const { fake: mockNpm } = require('../../fixtures/mock-npm') const fs = require('fs') +const log = require('../../../lib/utils/log-shim') // The way we set loglevel is kind of convoluted, and there is no way to affect // it from these tests, which only interact with lib/publish.js, which assumes // that the code that is requiring and calling lib/publish.js has already // taken care of the loglevel -const log = require('npmlog') -log.level = 'silent' +const _level = log.level +t.beforeEach(() => (log.level = 'silent')) +t.teardown(() => (log.level = _level)) t.cleanSnapshot = data => { return data.replace(/^ *"gitHead": .*$\n/gm, '') @@ -19,8 +21,6 @@ const defaults = Object.entries(definitions).reduce((defaults, [key, def]) => { return defaults }, {}) -t.afterEach(() => (log.level = 'silent')) - t.test( /* eslint-disable-next-line max-len */ 'should publish with libnpmpublish, passing through flatOptions and respecting publishConfig.registry', @@ -147,7 +147,7 @@ t.test('if loglevel=info and json, should not output package contents', async t id: 'someid', }), logTar: () => { - t.pass('logTar is called') + t.fail('logTar is not called in json mode') }, }, libnpmpublish: { @@ -188,7 +188,6 @@ t.test( ), }) - log.level = 'silent' const Publish = t.mock('../../../lib/commands/publish.js', { '../../../lib/utils/tar.js': { getContents: () => ({ @@ -681,9 +680,12 @@ t.test('private workspaces', async t => { } t.test('with color', async t => { + t.plan(4) + + log.level = 'info' const Publish = t.mock('../../../lib/commands/publish.js', { ...mocks, - npmlog: { + 'proc-log': { notice () {}, verbose () {}, warn (title, msg) { @@ -707,9 +709,12 @@ t.test('private workspaces', async t => { }) t.test('colorless', async t => { + t.plan(4) + + log.level = 'info' const Publish = t.mock('../../../lib/commands/publish.js', { ...mocks, - npmlog: { + 'proc-log': { notice () {}, verbose () {}, warn (title, msg) { @@ -730,6 +735,8 @@ t.test('private workspaces', async t => { }) t.test('unexpected error', async t => { + t.plan(1) + const Publish = t.mock('../../../lib/commands/publish.js', { ...mocks, libnpmpublish: { @@ -741,7 +748,7 @@ t.test('private workspaces', async t => { publishes.push(manifest) }, }, - npmlog: { + 'proc-log': { notice () {}, verbose () {}, }, @@ -755,6 +762,8 @@ t.test('private workspaces', async t => { }) t.test('runs correct lifecycle scripts', async t => { + t.plan(5) + const testDir = t.testdir({ 'package.json': JSON.stringify( { @@ -773,6 +782,7 @@ t.test('runs correct lifecycle scripts', async t => { }) const scripts = [] + log.level = 'info' const Publish = t.mock('../../../lib/commands/publish.js', { '@npmcli/run-script': args => { scripts.push(args) @@ -810,6 +820,8 @@ t.test('runs correct lifecycle scripts', async t => { }) t.test('does not run scripts on --ignore-scripts', async t => { + t.plan(4) + const testDir = t.testdir({ 'package.json': JSON.stringify( { @@ -821,6 +833,7 @@ t.test('does not run scripts on --ignore-scripts', async t => { ), }) + log.level = 'info' const Publish = t.mock('../../../lib/commands/publish.js', { '@npmcli/run-script': () => { t.fail('should not call run-script') diff --git a/test/lib/commands/repo.js b/test/lib/commands/repo.js index 4e61047b4e7c7..93eb6d0311e1c 100644 --- a/test/lib/commands/repo.js +++ b/test/lib/commands/repo.js @@ -1,8 +1,8 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm.js') -const { join, sep } = require('path') +const { load: _loadMockNpm } = require('../../fixtures/mock-npm.js') +const { sep } = require('path') -const pkgDirs = t.testdir({ +const fixture = { 'package.json': JSON.stringify({ name: 'thispkg', version: '1.2.3', @@ -149,35 +149,36 @@ const pkgDirs = t.testdir({ }, }), }, - workspaces: { +} + +const workspaceFixture = { + 'package.json': JSON.stringify({ + name: 'workspaces-test', + version: '1.2.3-test', + workspaces: ['workspace-a', 'workspace-b', 'workspace-c'], + repository: 'https://github.com/npm/workspaces-test', + }), + 'workspace-a': { 'package.json': JSON.stringify({ - name: 'workspaces-test', - version: '1.2.3-test', - workspaces: ['workspace-a', 'workspace-b', 'workspace-c'], - repository: 'https://github.com/npm/workspaces-test', + name: 'workspace-a', + version: '1.2.3-a', + repository: 'http://repo.workspace-a/', }), - 'workspace-a': { - 'package.json': JSON.stringify({ - name: 'workspace-a', - version: '1.2.3-a', - repository: 'http://repo.workspace-a/', - }), - }, - 'workspace-b': { - 'package.json': JSON.stringify({ - name: 'workspace-b', - version: '1.2.3-n', - repository: 'https://github.com/npm/workspace-b', - }), - }, - 'workspace-c': JSON.stringify({ - 'package.json': { - name: 'workspace-n', - version: '1.2.3-n', - }, + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + version: '1.2.3-n', + repository: 'https://github.com/npm/workspace-b', }), }, -}) + 'workspace-c': JSON.stringify({ + 'package.json': { + name: 'workspace-n', + version: '1.2.3-n', + }, + }), +} // keep a tally of which urls got opened let opened = {} @@ -185,20 +186,18 @@ const openUrl = async (npm, url, errMsg) => { opened[url] = opened[url] || 0 opened[url]++ } - -const { Npm } = mockNpm(t, { - '../../lib/utils/open-url.js': openUrl, -}) -const npm = new Npm() - -t.before(async () => { - await npm.load() -}) - t.afterEach(() => opened = {}) -t.test('open repo urls', t => { - npm.localPrefix = pkgDirs +const loadMockNpm = async (t, prefix) => { + const res = await _loadMockNpm(t, { + mocks: { '../../lib/utils/open-url.js': openUrl }, + testdir: prefix, + }) + return res +} + +t.test('open repo urls', async t => { + const { npm } = await loadMockNpm(t, fixture) const expect = { hostedgit: 'https://github.com/foo/hostedgit', hostedgitat: 'https://github.com/foo/hostedgitat', @@ -239,8 +238,9 @@ t.test('open repo urls', t => { }) }) -t.test('fail if cannot figure out repo url', t => { - npm.localPrefix = pkgDirs +t.test('fail if cannot figure out repo url', async t => { + const { npm } = await loadMockNpm(t, fixture) + const cases = [ 'norepo', 'repoobbj-nourl', @@ -261,13 +261,13 @@ t.test('fail if cannot figure out repo url', t => { }) t.test('open default package if none specified', async t => { - npm.localPrefix = pkgDirs + const { npm } = await loadMockNpm(t, fixture) await npm.exec('repo', []) t.equal(opened['https://example.com/thispkg'], 1, 'opened expected url', { opened }) }) -t.test('workspaces', t => { - npm.localPrefix = join(pkgDirs, 'workspaces') +t.test('workspaces', async t => { + const { npm } = await loadMockNpm(t, workspaceFixture) t.afterEach(() => { npm.config.set('workspaces', null) @@ -311,5 +311,4 @@ t.test('workspaces', t => { ) t.match({}, opened, 'opened no repo urls') }) - t.end() }) diff --git a/test/lib/commands/restart.js b/test/lib/commands/restart.js index 608de0331deef..7730f1a3011f6 100644 --- a/test/lib/commands/restart.js +++ b/test/lib/commands/restart.js @@ -1,6 +1,6 @@ const t = require('tap') const spawk = require('spawk') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') spawk.preventUnmatched() t.teardown(() => { @@ -12,24 +12,24 @@ t.teardown(() => { // 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', - }, - }), +t.test('should run restart script from package.json', async t => { + const { npm } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + restart: 'node ./test-restart.js', + }, + }), + }, + config: { + loglevel: 'silent', + }, }) - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.log.level = 'silent' - npm.localPrefix = prefix - const [scriptShell] = makeSpawnArgs({ path: prefix }) + const [scriptShell] = makeSpawnArgs({ path: npm.prefix }) const script = spawk.spawn(scriptShell, (args) => { - t.ok(args.includes('node ./test-restart.js "foo"'), 'ran stop script with extra args') + t.ok(args.includes('node ./test-restart.js "foo"'), 'ran restart script with extra args') return true }) await npm.exec('restart', ['foo']) diff --git a/test/lib/commands/root.js b/test/lib/commands/root.js index 9871ddb25dc67..a886b30c3ee48 100644 --- a/test/lib/commands/root.js +++ b/test/lib/commands/root.js @@ -1,9 +1,8 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') t.test('prefix', async (t) => { - const { joinedOutput, Npm } = mockNpm(t) - const npm = new Npm() + const { joinedOutput, npm } = await loadMockNpm(t, { load: false }) await npm.exec('root', []) t.equal( joinedOutput(), diff --git a/test/lib/commands/run-script.js b/test/lib/commands/run-script.js index e421c655ef64f..ea0227cda08ca 100644 --- a/test/lib/commands/run-script.js +++ b/test/lib/commands/run-script.js @@ -31,13 +31,16 @@ const output = [] const npmlog = { disableProgress: () => null, level: 'warn', +} + +const log = { error: () => null, } t.afterEach(() => { npm.color = false npmlog.level = 'warn' - npmlog.error = () => null + log.error = () => null output.length = 0 RUN_SCRIPTS.length = 0 config['if-present'] = false @@ -56,6 +59,7 @@ const getRS = windows => { } ), npmlog, + 'proc-log': log, '../../../lib/utils/is-windows-shell.js': windows, }) return new RunScript(npm) @@ -758,7 +762,7 @@ t.test('workspaces', t => { t.test('missing scripts in all workspaces', async t => { const LOG = [] - npmlog.error = err => { + log.error = err => { LOG.push(String(err)) } await t.rejects( @@ -805,7 +809,7 @@ t.test('workspaces', t => { t.test('missing scripts in some workspaces', async t => { const LOG = [] - npmlog.error = err => { + log.error = err => { LOG.push(String(err)) } await runScript.execWorkspaces(['test'], ['a', 'b', 'c', 'd']) @@ -857,6 +861,7 @@ t.test('workspaces', t => { throw new Error('err') }, npmlog, + 'proc-log': log, '../../../lib/utils/is-windows-shell.js': false, }) const runScript = new RunScript(npm) @@ -875,6 +880,7 @@ t.test('workspaces', t => { RUN_SCRIPTS.push(opts) }, npmlog, + 'proc-log': log, '../../../lib/utils/is-windows-shell.js': false, }) const runScript = new RunScript(npm) diff --git a/test/lib/commands/set-script.js b/test/lib/commands/set-script.js index 592a2431c2e3e..2c4fe57d68542 100644 --- a/test/lib/commands/set-script.js +++ b/test/lib/commands/set-script.js @@ -10,7 +10,7 @@ const npm = mockNpm(flatOptions) const ERROR_OUTPUT = [] const WARN_OUTPUT = [] const SetScript = t.mock('../../../lib/commands/set-script.js', { - npmlog: { + 'proc-log': { error: (...args) => { ERROR_OUTPUT.push(args) }, diff --git a/test/lib/commands/set.js b/test/lib/commands/set.js index a57ea1a5401dd..feeb901571768 100644 --- a/test/lib/commands/set.js +++ b/test/lib/commands/set.js @@ -2,6 +2,7 @@ const t = require('tap') // can't run this until npm set can save to project level npmrc t.skip('npm set', async t => { + // XXX: convert to loadMockNpm const { real: mockNpm } = require('../../fixtures/mock-npm') const { joinedOutput, Npm } = mockNpm(t) const npm = new Npm() diff --git a/test/lib/commands/shrinkwrap.js b/test/lib/commands/shrinkwrap.js index db4021abd6560..2b9e46c70c98e 100644 --- a/test/lib/commands/shrinkwrap.js +++ b/test/lib/commands/shrinkwrap.js @@ -1,7 +1,7 @@ const t = require('tap') const fs = require('fs') const { resolve } = require('path') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') // Attempt to parse json values in snapshots before // stringifying to remove escaped values like \\" @@ -13,7 +13,7 @@ t.formatSnapshot = obj => (k, v) => { try { return JSON.parse(v) - } catch (_) {} + } catch {} return v }, 2 @@ -23,33 +23,25 @@ t.formatSnapshot = obj => // and make some assertions that should always be true. Sets // the results on t.context for use in child tests const shrinkwrap = async (t, testdir = {}, config = {}, mocks = {}) => { - const { Npm, filteredLogs } = mockNpm(t, mocks) - const npm = new Npm() - await npm.load() - - npm.localPrefix = t.testdir(testdir) - if (config.lockfileVersion) { - npm.config.set('lockfile-version', config.lockfileVersion) - } - if (config.global) { - npm.config.set('global', config.global) - } + const { npm, logs } = await loadMockNpm(t, { + mocks, + config, + testdir, + }) await npm.exec('shrinkwrap', []) - const newFile = resolve(npm.localPrefix, 'npm-shrinkwrap.json') - const oldFile = resolve(npm.localPrefix, 'package-lock.json') - const notices = filteredLogs('notice') - const warnings = filteredLogs('warn') + const newFile = resolve(npm.prefix, 'npm-shrinkwrap.json') + const oldFile = resolve(npm.prefix, 'package-lock.json') t.notOk(fs.existsSync(oldFile), 'package-lock is always deleted') - t.same(warnings, [], 'no warnings') + t.same(logs.warn, [], 'no warnings') t.teardown(() => delete t.context) t.context = { localPrefix: testdir, config, shrinkwrap: JSON.parse(fs.readFileSync(newFile)), - logs: notices, + logs: logs.notice.map(([, m]) => m), } } @@ -58,8 +50,8 @@ const shrinkwrap = async (t, testdir = {}, config = {}, mocks = {}) => { const shrinkwrapMatrix = async (t, file, assertions) => { const ancient = JSON.stringify({ lockfileVersion: 1 }) const existing = JSON.stringify({ lockfileVersion: 2 }) - const upgrade = { lockfileVersion: 3 } - const downgrade = { lockfileVersion: 1 } + const upgrade = { 'lockfile-version': 3 } + const downgrade = { 'lockfile-version': 1 } let ancientDir = {} let existingDir = null diff --git a/test/lib/commands/star.js b/test/lib/commands/star.js index 13838bb105afc..9a49036422d5e 100644 --- a/test/lib/commands/star.js +++ b/test/lib/commands/star.js @@ -15,9 +15,9 @@ const npm = mockNpm({ }, }) const npmFetch = { json: noop } -const npmlog = { error: noop, info: noop, verbose: noop } +const log = { error: noop, info: noop, verbose: noop } const mocks = { - npmlog, + 'proc-log': log, 'npm-registry-fetch': npmFetch, '../../../lib/utils/get-identity.js': async () => 'foo', '../../../lib/utils/usage.js': () => 'usage instructions', @@ -29,7 +29,7 @@ const star = new Star(npm) t.afterEach(() => { config.unicode = false config['star.unstar'] = false - npmlog.info = noop + log.info = noop result = '' }) @@ -53,7 +53,7 @@ t.test('star a package', async t => { : {} ), }) - npmlog.info = (title, msg, id) => { + log.info = (title, msg, id) => { t.equal(title, 'star', 'should use expected title') t.equal(msg, 'starring', 'should use expected msg') t.equal(id, pkgName, 'should use expected id') @@ -78,7 +78,7 @@ t.test('unstar a package', async t => { : { foo: true } ), }) - npmlog.info = (title, msg, id) => { + log.info = (title, msg, id) => { t.equal(title, 'unstar', 'should use expected title') t.equal(msg, 'unstarring', 'should use expected msg') t.equal(id, pkgName, 'should use expected id') diff --git a/test/lib/commands/stars.js b/test/lib/commands/stars.js index 4ed64385892fd..959739653da7a 100644 --- a/test/lib/commands/stars.js +++ b/test/lib/commands/stars.js @@ -11,9 +11,9 @@ const npm = { }, } const npmFetch = { json: noop } -const npmlog = { warn: noop } +const log = { warn: noop } const mocks = { - npmlog, + 'proc-log': log, 'npm-registry-fetch': npmFetch, '../../../lib/utils/get-identity.js': async () => 'foo', '../../../lib/utils/usage.js': () => 'usage instructions', @@ -24,7 +24,7 @@ const stars = new Stars(npm) t.afterEach(() => { npm.config = { get () {} } - npmlog.warn = noop + log.warn = noop result = '' }) @@ -81,7 +81,7 @@ t.test('unauthorized request', async t => { ) } - npmlog.warn = (title, msg) => { + log.warn = (title, msg) => { t.equal(title, 'stars', 'should use expected title') t.equal( msg, @@ -108,7 +108,7 @@ t.test('unexpected error', async t => { throw new Error('ERROR') } - npmlog.warn = (title, msg) => { + log.warn = (title, msg) => { throw new Error('Should not output extra warning msgs') } @@ -123,7 +123,7 @@ t.test('no pkg starred', async t => { t.plan(2) npmFetch.json = async (uri, opts) => ({ rows: [] }) - npmlog.warn = (title, msg) => { + log.warn = (title, msg) => { t.equal(title, 'stars', 'should use expected title') t.equal( msg, diff --git a/test/lib/commands/start.js b/test/lib/commands/start.js index 1f26f38ead0de..4f7dc366dbc19 100644 --- a/test/lib/commands/start.js +++ b/test/lib/commands/start.js @@ -1,6 +1,6 @@ const t = require('tap') const spawk = require('spawk') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') spawk.preventUnmatched() t.teardown(() => { @@ -12,22 +12,23 @@ t.teardown(() => { // 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', - }, - }), +t.test('should run start script from package.json', async t => { + t.plan(2) + const { npm } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + start: 'node ./test-start.js', + }, + }), + }, + config: { + loglevel: 'silent', + }, }) - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.log.level = 'silent' - npm.localPrefix = prefix - const [scriptShell] = makeSpawnArgs({ path: prefix }) + const [scriptShell] = makeSpawnArgs({ path: npm.prefix }) const script = spawk.spawn(scriptShell, (args) => { t.ok(args.includes('node ./test-start.js "foo"'), 'ran start script with extra args') return true diff --git a/test/lib/commands/stop.js b/test/lib/commands/stop.js index 4f189449ba077..53d057b711306 100644 --- a/test/lib/commands/stop.js +++ b/test/lib/commands/stop.js @@ -1,6 +1,6 @@ const t = require('tap') const spawk = require('spawk') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') spawk.preventUnmatched() t.teardown(() => { @@ -13,21 +13,21 @@ t.teardown(() => { 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 { npm } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + stop: 'node ./test-stop.js', + }, + }), + }, + config: { + loglevel: 'silent', + }, }) - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.log.level = 'silent' - npm.localPrefix = prefix - const [scriptShell] = makeSpawnArgs({ path: prefix }) + const [scriptShell] = makeSpawnArgs({ path: npm.prefix }) const script = spawk.spawn(scriptShell, (args) => { t.ok(args.includes('node ./test-stop.js "foo"'), 'ran stop script with extra args') return true diff --git a/test/lib/commands/test.js b/test/lib/commands/test.js index 4e5ce289bca9b..a3dbd3ff4cffb 100644 --- a/test/lib/commands/test.js +++ b/test/lib/commands/test.js @@ -1,6 +1,6 @@ const t = require('tap') const spawk = require('spawk') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') spawk.preventUnmatched() t.teardown(() => { @@ -12,22 +12,22 @@ t.teardown(() => { // 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: { - test: 'node ./test-test.js', - }, - }), +t.test('should run test script from package.json', async t => { + const { npm } = await loadMockNpm(t, { + testdir: { + 'package.json': JSON.stringify({ + name: 'x', + version: '1.2.3', + scripts: { + test: 'node ./test-test.js', + }, + }), + }, + config: { + loglevel: 'silent', + }, }) - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.log.level = 'silent' - npm.localPrefix = prefix - const [scriptShell] = makeSpawnArgs({ path: prefix }) + const [scriptShell] = makeSpawnArgs({ path: npm.prefix }) const script = spawk.spawn(scriptShell, (args) => { t.ok(args.includes('node ./test-test.js "foo"'), 'ran test script with extra args') return true diff --git a/test/lib/commands/token.js b/test/lib/commands/token.js index 6d0dc9d7e0874..65a094a0bca24 100644 --- a/test/lib/commands/token.js +++ b/test/lib/commands/token.js @@ -3,25 +3,24 @@ const t = require('tap') const mocks = { profile: {}, output: () => {}, - log: {}, readUserInfo: {}, } const npm = { output: (...args) => mocks.output(...args), } -const Token = t.mock('../../../lib/commands/token.js', { +const mockToken = (otherMocks) => t.mock('../../../lib/commands/token.js', { '../../../lib/utils/otplease.js': (opts, fn) => { return Promise.resolve().then(() => fn(opts)) }, '../../../lib/utils/read-user-info.js': mocks.readUserInfo, 'npm-profile': mocks.profile, - npmlog: mocks.log, + ...otherMocks, }) -const token = new Token(npm) +const tokenWithMocks = (options = {}) => { + const { log, ...mockRequests } = options -const tokenWithMocks = mockRequests => { for (const mod in mockRequests) { if (mod === 'npm') { mockRequests.npm = { ...npm, ...mockRequests.npm } @@ -50,13 +49,24 @@ const tokenWithMocks = mockRequests => { } } - const token = new Token(mockRequests.npm || npm) + const MockedToken = mockToken(log ? { + 'proc-log': { + info: log.info, + }, + npmlog: { + gauge: log.gauge, + newItem: log.newItem, + }, + } : {}) + const token = new MockedToken(mockRequests.npm || npm) return [token, reset] } t.test('completion', t => { t.plan(5) + const [token] = tokenWithMocks() + const testComp = (argv, expect) => { t.resolveMatch(token.completion({ conf: { argv: { remain: argv } } }), expect, argv.join(' ')) } @@ -74,7 +84,7 @@ t.test('completion', t => { t.test('token foobar', async t => { t.plan(2) - const [, reset] = tokenWithMocks({ + const [token, reset] = tokenWithMocks({ log: { gauge: { show: name => { diff --git a/test/lib/commands/unpublish.js b/test/lib/commands/unpublish.js index 6ac2067531c80..1424adf5c9851 100644 --- a/test/lib/commands/unpublish.js +++ b/test/lib/commands/unpublish.js @@ -17,7 +17,6 @@ const testDir = t.testdir({ const npm = mockNpm({ localPrefix: testDir, - log: { silly () {}, verbose () {} }, config, output: (...msg) => { result += msg.join('\n') @@ -30,10 +29,10 @@ const mocks = { 'npm-registry-fetch': { json: noop }, '../../../lib/utils/otplease.js': async (opts, fn) => fn(opts), '../../../lib/utils/get-identity.js': async () => 'foo', + 'proc-log': { silly () {}, verbose () {} }, } t.afterEach(() => { - npm.log = { silly () {}, verbose () {} } npm.localPrefix = testDir result = '' config['dry-run'] = false @@ -44,7 +43,7 @@ t.afterEach(() => { t.test('no args --force', async t => { config.force = true - npm.log = { + const log = { silly (title) { t.equal(title, 'unpublish', 'should silly log args') }, @@ -74,6 +73,7 @@ t.test('no args --force', async t => { const Unpublish = t.mock('../../../lib/commands/unpublish.js', { ...mocks, libnpmpublish, + 'proc-log': log, }) const unpublish = new Unpublish(npm) @@ -147,7 +147,7 @@ t.test('too many args', async t => { }) t.test('unpublish @version', async t => { - npm.log = { + const log = { silly (title, key, value) { t.equal(title, 'unpublish', 'should silly log args') if (key === 'spec') { @@ -172,6 +172,7 @@ t.test('unpublish @version', async t => { const Unpublish = t.mock('../../../lib/commands/unpublish.js', { ...mocks, libnpmpublish, + 'proc-log': log, }) const unpublish = new Unpublish(npm) diff --git a/test/lib/commands/update.js b/test/lib/commands/update.js index 6ca6dbc87d968..aecb2c32b5e3f 100644 --- a/test/lib/commands/update.js +++ b/test/lib/commands/update.js @@ -9,12 +9,10 @@ const config = { const noop = () => null const npm = mockNpm({ globalDir: '', - log: noop, config, prefix: '', }) const mocks = { - npmlog: { warn () {} }, '@npmcli/arborist': class { reify () {} }, @@ -29,22 +27,23 @@ t.afterEach(() => { }) t.test('no args', async t => { - t.plan(3) + t.plan(4) npm.prefix = '/project/a' class Arborist { constructor (args) { + const { log, ...rest } = args t.same( - args, + rest, { ...npm.flatOptions, path: npm.prefix, - log: noop, workspaces: null, }, 'should call arborist contructor with expected args' ) + t.match(log, {}, 'log is passed in') } reify ({ update }) { @@ -65,22 +64,23 @@ t.test('no args', async t => { }) t.test('with args', async t => { - t.plan(3) + t.plan(4) npm.prefix = '/project/a' class Arborist { constructor (args) { + const { log, ...rest } = args t.same( - args, + rest, { ...npm.flatOptions, path: npm.prefix, - log: noop, workspaces: null, }, 'should call arborist contructor with expected args' ) + t.match(log, {}, 'log is passed in') } reify ({ update }) { @@ -108,7 +108,7 @@ t.test('update --depth=', async t => { const Update = t.mock('../../../lib/commands/update.js', { ...mocks, - npmlog: { + 'proc-log': { warn: (title, msg) => { t.equal(title, 'update', 'should print expected title') t.match( @@ -125,7 +125,7 @@ t.test('update --depth=', async t => { }) t.test('update --global', async t => { - t.plan(2) + t.plan(3) const normalizePath = p => p.replace(/\\+/g, '/') const redactCwd = (path) => normalizePath(path) @@ -137,13 +137,15 @@ t.test('update --global', async t => { class Arborist { constructor (args) { - const { path, ...opts } = args + const { path, log, ...rest } = args t.same( - opts, - { ...npm.flatOptions, log: noop, workspaces: undefined }, + rest, + { ...npm.flatOptions, workspaces: undefined }, 'should call arborist contructor with expected options' ) + t.match(log, {}, 'log is passed in') + t.equal( redactCwd(path), '{CWD}/global/lib', diff --git a/test/lib/commands/version.js b/test/lib/commands/version.js index 6603b581061a6..980353897c29a 100644 --- a/test/lib/commands/version.js +++ b/test/lib/commands/version.js @@ -1,5 +1,6 @@ const t = require('tap') const { fake: mockNpm } = require('../../fixtures/mock-npm') +const mockGlobals = require('../../fixtures/mock-globals.js') let result = [] @@ -26,294 +27,301 @@ const mocks = { const Version = t.mock('../../../lib/commands/version.js', mocks) const version = new Version(npm) -const _processVersions = process.versions t.afterEach(() => { config.json = false npm.prefix = '' - process.versions = _processVersions result = [] }) -t.test('no args', async t => { - const prefix = t.testdir({ - 'package.json': JSON.stringify({ - name: 'test-version-no-args', - version: '3.2.1', - }), - }) - npm.prefix = prefix - Object.defineProperty(process, 'versions', { value: { node: '1.0.0' } }) - - await version.exec([]) - - t.same( - result, - [ - { - 'test-version-no-args': '3.2.1', - node: '1.0.0', - npm: '1.0.0', - }, - ], - 'should output expected values for various versions in npm' - ) -}) - -t.test('too many args', async t => { - await t.rejects( - version.exec(['foo', 'bar']), - /npm version/, - 'should throw usage instructions error' - ) -}) - -t.test('completion', async t => { - const testComp = async (argv, expect) => { - const res = await version.completion({ conf: { argv: { remain: argv } } }) - t.strictSame(res, expect, argv.join(' ')) - } - - await testComp( - ['npm', 'version'], - ['major', 'minor', 'patch', 'premajor', 'preminor', 'prepatch', 'prerelease', 'from-git'] - ) - await testComp(['npm', 'version', 'major'], []) - - t.end() -}) - -t.test('failure reading package.json', async t => { - const prefix = t.testdir({}) - npm.prefix = prefix - - await version.exec([]) - - t.same( - result, - [ - { - npm: '1.0.0', - node: '1.0.0', - }, - ], - 'should not have package name on returning object' - ) -}) - -t.test('--json option', async t => { - const prefix = t.testdir({}) - config.json = true - npm.prefix = prefix - Object.defineProperty(process, 'versions', { value: {} }) - - await version.exec([]) - t.same(result, ['{\n "npm": "1.0.0"\n}'], 'should return json stringified result') -}) +t.test('node@1', t => { + mockGlobals(t, { 'process.versions': { node: '1.0.0' } }, { replace: true }) -t.test('with one arg', async t => { - const Version = t.mock('../../../lib/commands/version.js', { - ...mocks, - libnpmversion: (arg, opts) => { - t.equal(arg, 'major', 'should forward expected value') - t.same( - opts, - { - path: '', - }, - 'should forward expected options' - ) - return '4.0.0' - }, - }) - const version = new Version(npm) - - await version.exec(['major']) - t.same(result, ['v4.0.0'], 'outputs the new version prefixed by the tagVersionPrefix') -}) + t.test('no args', async t => { + const prefix = t.testdir({ + 'package.json': JSON.stringify({ + name: 'test-version-no-args', + version: '3.2.1', + }), + }) + npm.prefix = prefix -t.test('workspaces', async t => { - t.teardown(() => { - npm.localPrefix = '' - npm.prefix = '' - }) + await version.exec([]) - t.test('no args, all workspaces', async t => { - const testDir = t.testdir({ - 'package.json': JSON.stringify( - { - name: 'workspaces-test', - version: '1.0.0', - workspaces: ['workspace-a', 'workspace-b'], - }, - null, - 2 - ), - 'workspace-a': { - 'package.json': JSON.stringify({ - name: 'workspace-a', - version: '1.0.0', - }), - }, - 'workspace-b': { - 'package.json': JSON.stringify({ - name: 'workspace-b', - version: '1.0.0', - }), - }, - }) - npm.localPrefix = testDir - npm.prefix = testDir - const version = new Version(npm) - await version.execWorkspaces([], []) t.same( result, [ { - 'workspaces-test': '1.0.0', - 'workspace-a': '1.0.0', - 'workspace-b': '1.0.0', + 'test-version-no-args': '3.2.1', + node: '1.0.0', npm: '1.0.0', }, ], - 'outputs includes main package and workspace versions' + 'should output expected values for various versions in npm' ) }) - t.test('no args, single workspaces', async t => { - const testDir = t.testdir({ - 'package.json': JSON.stringify( - { - name: 'workspaces-test', - version: '1.0.0', - workspaces: ['workspace-a', 'workspace-b'], - }, - null, - 2 - ), - 'workspace-a': { - 'package.json': JSON.stringify({ - name: 'workspace-a', - version: '1.0.0', - }), - }, - 'workspace-b': { - 'package.json': JSON.stringify({ - name: 'workspace-b', - version: '1.0.0', - }), - }, - }) - npm.localPrefix = testDir - npm.prefix = testDir - const version = new Version(npm) - await version.execWorkspaces([], ['workspace-a']) - t.same( - result, - [ - { - 'workspaces-test': '1.0.0', - 'workspace-a': '1.0.0', - npm: '1.0.0', - }, - ], - 'outputs includes main package and requested workspace versions' + t.test('too many args', async t => { + await t.rejects( + version.exec(['foo', 'bar']), + /npm version/, + 'should throw usage instructions error' ) }) - t.test('no args, all workspaces, workspace with missing name or version', async t => { - const testDir = t.testdir({ - 'package.json': JSON.stringify( - { - name: 'workspaces-test', - version: '1.0.0', - workspaces: ['workspace-a', 'workspace-b', 'workspace-c'], - }, - null, - 2 - ), - 'workspace-a': { - 'package.json': JSON.stringify({ - name: 'workspace-a', - version: '1.0.0', - }), - }, - 'workspace-b': { - 'package.json': JSON.stringify({ - name: 'workspace-b', - }), - }, - 'workspace-c': { - 'package.json': JSON.stringify({ - version: '1.0.0', - }), - }, - }) - npm.localPrefix = testDir - npm.prefix = testDir - const version = new Version(npm) - await version.execWorkspaces([], []) + t.test('completion', async t => { + const testComp = async (argv, expect) => { + const res = await version.completion({ conf: { argv: { remain: argv } } }) + t.strictSame(res, expect, argv.join(' ')) + } + + await testComp( + ['npm', 'version'], + ['major', 'minor', 'patch', 'premajor', 'preminor', 'prepatch', 'prerelease', 'from-git'] + ) + await testComp(['npm', 'version', 'major'], []) + + t.end() + }) + + t.test('failure reading package.json', async t => { + const prefix = t.testdir({}) + npm.prefix = prefix + + await version.exec([]) + t.same( result, [ { - 'workspaces-test': '1.0.0', - 'workspace-a': '1.0.0', npm: '1.0.0', + node: '1.0.0', }, ], - 'outputs includes main package and valid workspace versions' + 'should not have package name on returning object' ) }) + t.end() +}) - t.test('with one arg, all workspaces', async t => { - const libNpmVersionArgs = [] - const testDir = t.testdir({ - 'package.json': JSON.stringify( - { - name: 'workspaces-test', - version: '1.0.0', - workspaces: ['workspace-a', 'workspace-b'], - }, - null, - 2 - ), - 'workspace-a': { - 'package.json': JSON.stringify({ - name: 'workspace-a', - version: '1.0.0', - }), - }, - 'workspace-b': { - 'package.json': JSON.stringify({ - name: 'workspace-b', - version: '1.0.0', - }), - }, - }) +t.test('empty versions', t => { + mockGlobals(t, { 'process.versions': {} }, { replace: true }) + + t.test('--json option', async t => { + const prefix = t.testdir({}) + config.json = true + npm.prefix = prefix + + await version.exec([]) + t.same(result, ['{\n "npm": "1.0.0"\n}'], 'should return json stringified result') + }) + + t.test('with one arg', async t => { const Version = t.mock('../../../lib/commands/version.js', { ...mocks, libnpmversion: (arg, opts) => { - libNpmVersionArgs.push([arg, opts]) - return '2.0.0' + t.equal(arg, 'major', 'should forward expected value') + t.same( + opts, + { + path: '', + }, + 'should forward expected options' + ) + return '4.0.0' }, }) - npm.localPrefix = testDir - npm.prefix = testDir const version = new Version(npm) - await version.execWorkspaces(['major'], []) - t.same( - result, - ['workspace-a', 'v2.0.0', 'workspace-b', 'v2.0.0'], - 'outputs the new version for only the workspaces prefixed by the tagVersionPrefix' - ) + await version.exec(['major']) + t.same(result, ['v4.0.0'], 'outputs the new version prefixed by the tagVersionPrefix') }) - t.test('too many args', async t => { - await t.rejects( - version.execWorkspaces(['foo', 'bar'], []), - /npm version/, - 'should throw usage instructions error' - ) + t.test('workspaces', async t => { + t.teardown(() => { + npm.localPrefix = '' + npm.prefix = '' + }) + + t.test('no args, all workspaces', async t => { + const testDir = t.testdir({ + 'package.json': JSON.stringify( + { + name: 'workspaces-test', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b'], + }, + null, + 2 + ), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + version: '1.0.0', + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + version: '1.0.0', + }), + }, + }) + npm.localPrefix = testDir + npm.prefix = testDir + const version = new Version(npm) + await version.execWorkspaces([], []) + t.same( + result, + [ + { + 'workspaces-test': '1.0.0', + 'workspace-a': '1.0.0', + 'workspace-b': '1.0.0', + npm: '1.0.0', + }, + ], + 'outputs includes main package and workspace versions' + ) + }) + + t.test('no args, single workspaces', async t => { + const testDir = t.testdir({ + 'package.json': JSON.stringify( + { + name: 'workspaces-test', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b'], + }, + null, + 2 + ), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + version: '1.0.0', + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + version: '1.0.0', + }), + }, + }) + npm.localPrefix = testDir + npm.prefix = testDir + const version = new Version(npm) + await version.execWorkspaces([], ['workspace-a']) + t.same( + result, + [ + { + 'workspaces-test': '1.0.0', + 'workspace-a': '1.0.0', + npm: '1.0.0', + }, + ], + 'outputs includes main package and requested workspace versions' + ) + }) + + t.test('no args, all workspaces, workspace with missing name or version', async t => { + const testDir = t.testdir({ + 'package.json': JSON.stringify( + { + name: 'workspaces-test', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b', 'workspace-c'], + }, + null, + 2 + ), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + version: '1.0.0', + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + }), + }, + 'workspace-c': { + 'package.json': JSON.stringify({ + version: '1.0.0', + }), + }, + }) + npm.localPrefix = testDir + npm.prefix = testDir + const version = new Version(npm) + await version.execWorkspaces([], []) + t.same( + result, + [ + { + 'workspaces-test': '1.0.0', + 'workspace-a': '1.0.0', + npm: '1.0.0', + }, + ], + 'outputs includes main package and valid workspace versions' + ) + }) + + t.test('with one arg, all workspaces', async t => { + const libNpmVersionArgs = [] + const testDir = t.testdir({ + 'package.json': JSON.stringify( + { + name: 'workspaces-test', + version: '1.0.0', + workspaces: ['workspace-a', 'workspace-b'], + }, + null, + 2 + ), + 'workspace-a': { + 'package.json': JSON.stringify({ + name: 'workspace-a', + version: '1.0.0', + }), + }, + 'workspace-b': { + 'package.json': JSON.stringify({ + name: 'workspace-b', + version: '1.0.0', + }), + }, + }) + const Version = t.mock('../../../lib/commands/version.js', { + ...mocks, + libnpmversion: (arg, opts) => { + libNpmVersionArgs.push([arg, opts]) + return '2.0.0' + }, + }) + npm.localPrefix = testDir + npm.prefix = testDir + const version = new Version(npm) + + await version.execWorkspaces(['major'], []) + t.same( + result, + ['workspace-a', 'v2.0.0', 'workspace-b', 'v2.0.0'], + 'outputs the new version for only the workspaces prefixed by the tagVersionPrefix' + ) + }) + + t.test('too many args', async t => { + await t.rejects( + version.execWorkspaces(['foo', 'bar'], []), + /npm version/, + 'should throw usage instructions error' + ) + }) }) + + t.end() }) diff --git a/test/lib/commands/view.js b/test/lib/commands/view.js index 728787ec4aacc..035490a79fbf7 100644 --- a/test/lib/commands/view.js +++ b/test/lib/commands/view.js @@ -1,6 +1,7 @@ const t = require('tap') -t.cleanSnapshot = str => str.replace(/published .*? ago/g, 'published {TIME} ago') +t.cleanSnapshot = str => str + .replace(/(published ).*?( ago)/g, '$1{TIME}$2') // run the same as tap does when running directly with node process.stdout.columns = undefined @@ -17,8 +18,8 @@ const cleanLogs = () => { console.log = fn } -// 25 hours ago -const yesterday = new Date(Date.now() - 1000 * 60 * 60 * 25) +// 3 days. its never yesterday and never a week ago +const yesterday = new Date(Date.now() - 1000 * 60 * 60 * 24 * 3) const packument = (nv, opts) => { if (!opts.fullMetadata) { @@ -564,6 +565,12 @@ t.test('workspaces', async t => { pacote: { packument, }, + 'proc-log': { + warn: (msg) => { + warnMsg = msg + }, + silly: () => {}, + }, }) const config = { unicode: false, @@ -571,11 +578,6 @@ t.test('workspaces', async t => { } let warnMsg const npm = mockNpm({ - log: { - warn: (msg) => { - warnMsg = msg - }, - }, config, localPrefix: testDir, }) diff --git a/test/lib/commands/whoami.js b/test/lib/commands/whoami.js index dc6144ec1dd28..66c3f0c6b30bf 100644 --- a/test/lib/commands/whoami.js +++ b/test/lib/commands/whoami.js @@ -1,26 +1,24 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm') +const { load: _loadMockNpm } = require('../../fixtures/mock-npm') const username = 'foo' -const { joinedOutput, Npm } = mockNpm(t, { - '../../lib/utils/get-identity.js': () => Promise.resolve(username), -}) -const npm = new Npm() - -t.before(async () => { - await npm.load() +const loadMockNpm = (t, options) => _loadMockNpm(t, { + mocks: { + '../../lib/utils/get-identity.js': () => Promise.resolve(username), + }, + ...options, }) t.test('npm whoami', async (t) => { + const { npm, joinedOutput } = await loadMockNpm(t) await npm.exec('whoami', []) t.equal(joinedOutput(), username, 'should print username') }) t.test('npm whoami --json', async (t) => { - t.teardown(() => { - npm.config.set('json', false) + const { npm, joinedOutput } = await loadMockNpm(t, { + config: { json: true }, }) - npm.config.set('json', true) await npm.exec('whoami', []) t.equal(JSON.parse(joinedOutput()), username, 'should print username') }) diff --git a/test/lib/fixtures/mock-globals.js b/test/lib/fixtures/mock-globals.js new file mode 100644 index 0000000000000..ef89573b3f913 --- /dev/null +++ b/test/lib/fixtures/mock-globals.js @@ -0,0 +1,253 @@ +const t = require('tap') +const mockGlobals = require('../../fixtures/mock-globals') + +const originals = { + platform: process.platform, + error: console.error, + stderrOn: process.stderr.on, + stderrWrite: process.stderr.write, + shell: process.env.SHELL, + home: process.env.HOME, + argv: process.argv, +} +const callConsole = (...args) => console.error(...args) + +t.test('console', async t => { + await t.test('mocks', async (t) => { + const errors = [] + mockGlobals(t, { + 'console.error': (...args) => errors.push(...args), + }) + + callConsole(1) + callConsole(2) + callConsole(3) + t.strictSame(errors, [1, 2, 3], 'i got my errors') + }) + + t.equal(console.error, originals.error) +}) + +t.test('platform', async (t) => { + t.equal(process.platform, originals.platform) + + await t.test('posix', async (t) => { + mockGlobals(t, { 'process.platform': 'posix' }) + t.equal(process.platform, 'posix') + + await t.test('win32 --> woo', async (t) => { + mockGlobals(t, { 'process.platform': 'win32' }) + t.equal(process.platform, 'win32') + + mockGlobals(t, { 'process.platform': 'woo' }) + t.equal(process.platform, 'woo') + }) + + t.equal(process.platform, 'posix') + }) + + t.equal(process.platform, originals.platform) +}) + +t.test('manual reset', async t => { + let errorHandler, data + + const { reset } = mockGlobals(t, { + 'process.stderr.on': (__, handler) => { + errorHandler = handler + reset['process.stderr.on']() + }, + 'process.stderr.write': (chunk, callback) => { + data = chunk + process.nextTick(() => { + errorHandler({ errno: 'EPIPE' }) + callback() + }) + reset['process.stderr.write']() + }, + }) + + await new Promise((res, rej) => { + process.stderr.on('error', er => er.errno === 'EPIPE' ? res() : rej(er)) + process.stderr.write('hey', res) + }) + + t.equal(process.stderr.on, originals.stderrOn) + t.equal(process.stderr.write, originals.stderrWrite) + t.equal(data, 'hey', 'handles EPIPE errors') + t.ok(errorHandler) +}) + +t.test('multiple with manual reset', async (t) => { + t.equal(process.platform, originals.platform) + + await t.test('a', async (t) => { + mockGlobals(t, { 'process.platform': 'a' }) + t.equal(process.platform, 'a') + + await t.test('b, c', async (t) => { + const { reset: resetB } = mockGlobals(t, { 'process.platform': 'b' }) + t.equal(process.platform, 'b') + + const { reset: resetC } = mockGlobals(t, { 'process.platform': 'c' }) + t.equal(process.platform, 'c') + + resetC['process.platform']() + t.equal(process.platform, 'b') + + resetB['process.platform']() + t.equal(process.platform, 'a') + }) + + t.equal(process.platform, 'a') + }) + + t.equal(process.platform, originals.platform) +}) + +t.test('too many resets', async (t) => { + await t.test('single reset', async t => { + const { reset } = mockGlobals(t, { 'process.platform': 'z' }) + t.equal(process.platform, 'z') + + reset['process.platform']() + t.equal(process.platform, originals.platform) + + reset['process.platform']() + reset['process.platform']() + reset['process.platform']() + t.equal(process.platform, originals.platform) + }) + + t.equal(process.platform, originals.platform) +}) + +t.test('object mode', async t => { + await t.test('mocks', async t => { + const home = t.testdir() + let data + + mockGlobals(t, { + process: { + stderr: { + write: (chunk, callback) => { + data = chunk + process.nextTick(() => callback()) + }, + }, + env: { + HOME: home, + }, + }, + }) + + await new Promise((res) => { + process.stderr.write('hey', res) + }) + + t.equal(data, 'hey', 'handles EPIPE errors') + t.equal(process.env.HOME, home) + }) + + t.equal(process.env.HOME, originals.home) + t.equal(process.stderr.write, originals.stderrWrite) +}) + +t.test('mixed object/string mode', async t => { + await t.test('mocks', async t => { + const home = t.testdir() + + mockGlobals(t, { + 'process.env': { + HOME: home, + TEST: '1', + }, + }) + + t.equal(process.env.HOME, home) + t.equal(process.env.TEST, '1') + }) + + t.equal(process.env.HOME, originals.home) + t.equal(process.env.TEST, undefined) +}) + +t.test('date', async t => { + await t.test('mocks', async t => { + mockGlobals(t, { + 'Date.now': () => 100, + 'Date.prototype.toISOString': () => 'DDD', + }) + t.equal(Date.now(), 100) + t.equal(new Date().toISOString(), 'DDD') + }) + + t.ok(Date.now() > 100) + t.ok(new Date().toISOString().includes('T')) +}) + +t.test('argv', async t => { + await t.test('argv', async t => { + mockGlobals(t, { + 'process.argv': ['node', 'woo'], + }) + t.strictSame(process.argv, ['node', 'woo']) + }) + + t.strictSame(process.argv, originals.argv) +}) + +t.skip('multiple mocks and resets', async (t) => { + t.test('in order', async t => { + mockGlobals(t, { 'process.platform': 'a' }) + t.equal(process.platform, 'a') + + await t.test('b, c', async (t) => { + const { reset: resetB } = mockGlobals(t, { 'process.platform': 'b' }) + t.equal(process.platform, 'b') + + const { reset: resetC } = mockGlobals(t, { 'process.platform': 'c' }) + t.equal(process.platform, 'c') + + resetC['process.platform']() + resetC['process.platform']() + resetC['process.platform']() + resetC['process.platform']() + t.equal(process.platform, 'b') + + resetB['process.platform']() + resetB['process.platform']() + resetB['process.platform']() + resetB['process.platform']() + t.equal(process.platform, 'a') + }) + + t.equal(process.platform, 'a') + }) + + t.test('out of order', async (t) => { + mockGlobals(t, { 'process.platform': 'a' }) + t.equal(process.platform, 'a') + + await t.test('b, c', async (t) => { + const { reset: resetB } = mockGlobals(t, { 'process.platform': 'b' }) + t.equal(process.platform, 'b') + + const { reset: resetC } = mockGlobals(t, { 'process.platform': 'c' }) + + resetB['process.platform']() + resetB['process.platform']() + resetB['process.platform']() + resetB['process.platform']() + t.equal(process.platform, 'a') + + resetC['process.platform']() + resetC['process.platform']() + resetC['process.platform']() + resetC['process.platform']() + t.equal(process.platform, 'b') + }) + + t.equal(process.platform, 'a') + }) +}) diff --git a/test/lib/load-all-commands.js b/test/lib/load-all-commands.js index f813e50b220e1..248c81a30ab4d 100644 --- a/test/lib/load-all-commands.js +++ b/test/lib/load-all-commands.js @@ -4,21 +4,16 @@ // renders also ensures that any params we've defined in our commands work. const t = require('tap') const util = require('util') -const { real: mockNpm } = require('../fixtures/mock-npm.js') +const { load: loadMockNpm } = require('../fixtures/mock-npm.js') const { cmdList } = require('../../lib/utils/cmd-list.js') -const { Npm, outputs } = mockNpm(t) -const npm = new Npm() - t.test('load each command', async t => { - t.afterEach(() => { - outputs.length = 0 - }) t.plan(cmdList.length) - await npm.load() - npm.config.set('usage', true) // This makes npm.exec output the usage for (const cmd of cmdList.sort((a, b) => a.localeCompare(b, 'en'))) { t.test(cmd, async t => { + const { npm, outputs } = await loadMockNpm(t, { + config: { usage: true }, + }) const impl = await npm.cmd(cmd) if (impl.completion) { t.type(impl.completion, 'function', 'completion, if present, is a function') diff --git a/test/lib/load-all.js b/test/lib/load-all.js index fb45331ba92aa..e5d7b558c2a5b 100644 --- a/test/lib/load-all.js +++ b/test/lib/load-all.js @@ -1,34 +1,31 @@ const t = require('tap') const glob = require('glob') const { resolve } = require('path') -const { real: mockNpm } = require('../fixtures/mock-npm') +const { load: loadMockNpm } = 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 { - const { Npm } = mockNpm(t) - const npm = new Npm() + t.test('load all', async (t) => { + const { npm } = await loadMockNpm(t, { }) - t.teardown(() => { - const exitHandler = require('../../lib/utils/exit-handler.js') - exitHandler.setNpm(npm) - exitHandler() - }) - - t.before(async t => { - await npm.load() - }) + t.teardown(() => { + const exitHandler = require('../../lib/utils/exit-handler.js') + exitHandler.setNpm(npm) + exitHandler() + }) - t.test('load all the files', t => { - // just load all the files so we measure coverage for the missing tests - const dir = resolve(__dirname, '../../lib') - for (const f of glob.sync(`${dir}/**/*.js`)) { - require(f) - t.pass('loaded ' + f) - } - t.pass('loaded all files') - t.end() + t.test('load all the files', t => { + // just load all the files so we measure coverage for the missing tests + const dir = resolve(__dirname, '../../lib') + for (const f of glob.sync(`${dir}/**/*.js`)) { + require(f) + t.pass('loaded ' + f) + } + t.pass('loaded all files') + t.end() + }) }) } diff --git a/test/lib/npm.js b/test/lib/npm.js index 1ccd26e375803..2a0c5a89d2d99 100644 --- a/test/lib/npm.js +++ b/test/lib/npm.js @@ -1,7 +1,8 @@ const t = require('tap') +const { resolve, dirname } = require('path') -const npmlog = require('npmlog') -const { real: mockNpm } = require('../fixtures/mock-npm.js') +const { load: loadMockNpm } = require('../fixtures/mock-npm.js') +const mockGlobals = require('../fixtures/mock-globals') // delete this so that we don't have configs from the fact that it // is being run by 'npm test' @@ -15,7 +16,7 @@ for (const env of Object.keys(process.env).filter(e => /^npm_/.test(e))) { // if this test is just run directly, which is also acceptable. if (event === 'test') { t.ok( - ['test', 'run-script'].some(i => i === event), + ['test', 'run-script'].some(i => i === process.env[env]), 'should match "npm test" or "npm run test"' ) } else { @@ -25,41 +26,14 @@ for (const env of Object.keys(process.env).filter(e => /^npm_/.test(e))) { delete process.env[env] } -const { resolve, dirname } = require('path') - -const actualPlatform = process.platform -const beWindows = () => { - Object.defineProperty(process, 'platform', { - value: 'win32', - configurable: true, - }) -} -const bePosix = () => { - Object.defineProperty(process, 'platform', { - value: 'posix', - configurable: true, - }) -} -const argv = [...process.argv] - -t.afterEach(() => { +t.afterEach(async (t) => { for (const env of Object.keys(process.env).filter(e => /^npm_/.test(e))) { delete process.env[env] } - process.env.npm_config_cache = CACHE - process.argv = argv - Object.defineProperty(process, 'platform', { - value: actualPlatform, - configurable: true, - }) }) -const CACHE = t.testdir() -process.env.npm_config_cache = CACHE - t.test('not yet loaded', async t => { - const { Npm, logs } = mockNpm(t) - const npm = new Npm() + const { npm, logs } = await loadMockNpm(t, { load: false }) t.match(npm, { started: Number, command: null, @@ -79,8 +53,7 @@ t.test('not yet loaded', async t => { t.test('npm.load', async t => { t.test('load error', async t => { - const { Npm } = mockNpm(t) - const npm = new Npm() + const { npm } = await loadMockNpm(t, { load: false }) const loadError = new Error('load error') npm.config.load = async () => { throw loadError @@ -103,32 +76,28 @@ t.test('npm.load', async t => { }) t.test('basic loading', async t => { - const { Npm, logs } = mockNpm(t) - const npm = new Npm() - const dir = t.testdir({ - node_modules: {}, + const { npm, logs, prefix: dir, cache } = await loadMockNpm(t, { + testdir: { node_modules: {} }, }) - await npm.load() + t.equal(npm.loaded, true) t.equal(npm.config.loaded, true) t.equal(npm.config.get('force'), false) t.ok(npm.usage, 'has usage') - npm.config.set('prefix', dir) t.match(npm, { flatOptions: {}, }) - t.match(logs, [ - ['timing', 'npm:load', /Completed in [0-9.]+ms/], + t.match(logs.timing.filter(([p]) => p === 'npm:load'), [ + ['npm:load', /Completed in [0-9.]+ms/], ]) - bePosix() - t.equal(resolve(npm.cache), resolve(CACHE), 'cache is cache') + mockGlobals(t, { process: { platform: 'posix' } }) + t.equal(resolve(npm.cache), resolve(cache), 'cache is cache') const newCache = t.testdir() npm.cache = newCache t.equal(npm.config.get('cache'), newCache, 'cache setter sets config') t.equal(npm.cache, newCache, 'cache getter gets new config') - t.equal(npm.log, npmlog, 'npmlog getter') t.equal(npm.lockfileVersion, 2, 'lockfileVersion getter') t.equal(npm.prefix, npm.localPrefix, 'prefix is local prefix') t.not(npm.prefix, npm.globalPrefix, 'prefix is not global prefix') @@ -160,10 +129,9 @@ t.test('npm.load', async t => { t.equal(npm.bin, npm.globalBin, 'bin is global bin after prefix setter') t.not(npm.bin, npm.localBin, 'bin is not local bin after prefix setter') - beWindows() + mockGlobals(t, { process: { platform: 'win32' } }) t.equal(npm.bin, npm.globalBin, 'bin is global bin in windows mode') t.equal(npm.dir, npm.globalDir, 'dir is global dir in windows mode') - bePosix() const tmp = npm.tmp t.match(tmp, String, 'npm.tmp is a string') @@ -171,13 +139,12 @@ t.test('npm.load', async t => { }) t.test('forceful loading', async t => { - process.argv = [...process.argv, '--force', '--color', 'always'] - const { Npm, logs } = mockNpm(t) - const npm = new Npm() - await npm.load() - t.match(logs.filter(l => l[0] !== 'timing'), [ + mockGlobals(t, { + 'process.argv': [...process.argv, '--force', '--color', 'always'], + }) + const { logs } = await loadMockNpm(t) + t.match(logs.warn, [ [ - 'warn', 'using --force', 'Recommended protections disabled.', ], @@ -185,54 +152,42 @@ t.test('npm.load', async t => { }) t.test('node is a symlink', async t => { - const node = actualPlatform === 'win32' ? 'node.exe' : 'node' - const dir = t.testdir({ - '.npmrc': 'foo = bar', - bin: t.fixture('symlink', dirname(process.execPath)), + const node = process.platform === 'win32' ? 'node.exe' : 'node' + mockGlobals(t, { + 'process.argv': [ + node, + process.argv[1], + '--usage', + '--scope=foo', + 'token', + 'revoke', + 'blergggg', + ], }) - - const PATH = process.env.PATH || process.env.Path - process.env.PATH = resolve(dir, 'bin') - process.argv = [ - node, - process.argv[1], - '--prefix', dir, - '--userconfig', `${dir}/.npmrc`, - '--usage', - '--scope=foo', - 'token', - 'revoke', - 'blergggg', - ] - - t.teardown(() => { - process.env.PATH = PATH + const { npm, logs, outputs, prefix } = await loadMockNpm(t, { + testdir: { + bin: t.fixture('symlink', dirname(process.execPath)), + }, + globals: ({ prefix }) => ({ + 'process.env.PATH': resolve(prefix, 'bin'), + }), }) - const { Npm, logs, outputs } = mockNpm(t) - const npm = new Npm() - await npm.load() t.equal(npm.config.get('scope'), '@foo', 'added the @ sign to scope') - t.match(logs.filter(l => l[0] !== 'timing' || !/^config:/.test(l[1])), [ - [ - 'timing', - 'npm:load:whichnode', - /Completed in [0-9.]+ms/, - ], - [ - 'verbose', - 'node symlink', - resolve(dir, 'bin', node), - ], - [ - 'timing', - 'npm:load', - /Completed in [0-9.]+ms/, - ], + t.match([ + ...logs.timing.filter(([p]) => p === 'npm:load:whichnode'), + ...logs.verbose, + ...logs.timing.filter(([p]) => p === 'npm:load'), + ], [ + ['npm:load:whichnode', /Completed in [0-9.]+ms/], + ['node symlink', resolve(prefix, 'bin', node)], + ['logfile', /.*-debug-0.log/], + ['npm:load', /Completed in [0-9.]+ms/], ]) - t.equal(process.execPath, resolve(dir, 'bin', node)) + t.equal(process.execPath, resolve(prefix, 'bin', node)) outputs.length = 0 + logs.length = 0 await npm.exec('ll', []) t.equal(npm.command, 'll', 'command set to first npm command') @@ -271,33 +226,34 @@ t.test('npm.load', async t => { }) t.test('--no-workspaces with --workspace', async t => { - const dir = t.testdir({ - packages: { - a: { - 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - scripts: { test: 'echo test a' }, - }), + mockGlobals(t, { + 'process.argv': [ + process.execPath, + process.argv[1], + '--color', 'false', + '--workspaces', 'false', + '--workspace', 'a', + ], + }) + const { npm } = await loadMockNpm(t, { + load: false, + testdir: { + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + scripts: { test: 'echo test a' }, + }), + }, }, + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + workspaces: ['./packages/*'], + }), }, - 'package.json': JSON.stringify({ - name: 'root', - version: '1.0.0', - workspaces: ['./packages/*'], - }), }) - process.argv = [ - process.execPath, - process.argv[1], - '--userconfig', resolve(dir, '.npmrc'), - '--color', 'false', - '--workspaces', 'false', - '--workspace', 'a', - ] - const { Npm } = mockNpm(t) - const npm = new Npm() - npm.localPrefix = dir await t.rejects( npm.exec('run', []), /Can not use --no-workspaces and --workspace at the same time/ @@ -305,47 +261,40 @@ t.test('npm.load', async t => { }) t.test('workspace-aware configs and commands', async t => { - const dir = t.testdir({ - packages: { - a: { - 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - scripts: { test: 'echo test a' }, - }), - }, - b: { - 'package.json': JSON.stringify({ - name: 'b', - version: '1.0.0', - scripts: { test: 'echo test b' }, - }), + mockGlobals(t, { + 'process.argv': [ + process.execPath, + process.argv[1], + '--color', 'false', + '--workspaces', 'true', + ], + }) + const { npm, outputs } = await loadMockNpm(t, { + testdir: { + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + scripts: { test: 'echo test a' }, + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + version: '1.0.0', + scripts: { test: 'echo test b' }, + }), + }, }, + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + workspaces: ['./packages/*'], + }), }, - 'package.json': JSON.stringify({ - name: 'root', - version: '1.0.0', - workspaces: ['./packages/*'], - }), - '.npmrc': '', }) - process.argv = [ - process.execPath, - process.argv[1], - '--userconfig', - resolve(dir, '.npmrc'), - '--color', - 'false', - '--workspaces', - 'true', - ] - - const { Npm, outputs } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.localPrefix = dir - // verify that calling the command with a short name still sets // the npm.command property to the full canonical name of the cmd. npm.command = null @@ -368,44 +317,42 @@ t.test('npm.load', async t => { }) t.test('workspaces in global mode', async t => { - const dir = t.testdir({ - packages: { - a: { - 'package.json': JSON.stringify({ - name: 'a', - version: '1.0.0', - scripts: { test: 'echo test a' }, - }), - }, - b: { - 'package.json': JSON.stringify({ - name: 'b', - version: '1.0.0', - scripts: { test: 'echo test b' }, - }), + mockGlobals(t, { + 'process.argv': [ + process.execPath, + process.argv[1], + '--color', + 'false', + '--workspaces', + '--global', + 'true', + ], + }) + const { npm } = await loadMockNpm(t, { + testdir: { + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + version: '1.0.0', + scripts: { test: 'echo test a' }, + }), + }, + b: { + 'package.json': JSON.stringify({ + name: 'b', + version: '1.0.0', + scripts: { test: 'echo test b' }, + }), + }, }, + 'package.json': JSON.stringify({ + name: 'root', + version: '1.0.0', + workspaces: ['./packages/*'], + }), }, - 'package.json': JSON.stringify({ - name: 'root', - version: '1.0.0', - workspaces: ['./packages/*'], - }), }) - process.argv = [ - process.execPath, - process.argv[1], - '--userconfig', - resolve(dir, '.npmrc'), - '--color', - 'false', - '--workspaces', - '--global', - 'true', - ] - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() - npm.localPrefix = dir // verify that calling the command with a short name still sets // the npm.command property to the full canonical name of the cmd. npm.command = null @@ -418,109 +365,156 @@ t.test('npm.load', async t => { t.test('set process.title', async t => { t.test('basic title setting', async t => { - process.argv = [ - process.execPath, - process.argv[1], - '--usage', - '--scope=foo', - 'ls', - ] - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() + mockGlobals(t, { + 'process.argv': [ + process.execPath, + process.argv[1], + '--usage', + '--scope=foo', + 'ls', + ], + }) + const { npm } = await loadMockNpm(t) t.equal(npm.title, 'npm ls') t.equal(process.title, 'npm ls') }) t.test('do not expose token being revoked', async t => { - process.argv = [ - process.execPath, - process.argv[1], - '--usage', - '--scope=foo', - 'token', - 'revoke', - 'deadbeefcafebad', - ] - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() + mockGlobals(t, { + 'process.argv': [ + process.execPath, + process.argv[1], + '--usage', + '--scope=foo', + 'token', + 'revoke', + 'deadbeefcafebad', + ], + }) + const { npm } = await loadMockNpm(t) t.equal(npm.title, 'npm token revoke ***') t.equal(process.title, 'npm token revoke ***') }) t.test('do show *** unless a token is actually being revoked', async t => { - process.argv = [ - process.execPath, - process.argv[1], - '--usage', - '--scope=foo', - 'token', - 'revoke', - ] - const { Npm } = mockNpm(t) - const npm = new Npm() - await npm.load() + mockGlobals(t, { + 'process.argv': [ + process.execPath, + process.argv[1], + '--usage', + '--scope=foo', + 'token', + 'revoke', + ], + }) + const { npm } = await loadMockNpm(t) t.equal(npm.title, 'npm token revoke') t.equal(process.title, 'npm token revoke') }) }) -t.test('timings', t => { - const { Npm, logs } = mockNpm(t) - const npm = new Npm() - process.emit('time', 'foo') - process.emit('time', 'bar') - t.match(npm.timers.get('foo'), Number, 'foo timer is a number') - t.match(npm.timers.get('bar'), Number, 'foo timer is a number') - process.emit('timeEnd', 'foo') - process.emit('timeEnd', 'bar') - process.emit('timeEnd', 'baz') - t.match(logs, [ - ['timing', 'foo', /Completed in [0-9]+ms/], - ['timing', 'bar', /Completed in [0-9]+ms/], - [ - 'silly', +t.test('debug-log', async t => { + const { npm, debugFile } = await loadMockNpm(t, { load: false }) + + const log1 = ['silly', 'test', 'before load'] + const log2 = ['silly', 'test', 'after load'] + + process.emit('log', ...log1) + await npm.load() + process.emit('log', ...log2) + + const debug = await debugFile() + t.equal(npm.logFiles.length, 1, 'one debug file') + t.match(debug, log1.join(' '), 'before load appears') + t.match(debug, log2.join(' '), 'after load log appears') +}) + +t.test('timings', async t => { + t.test('gets/sets timers', async t => { + const { npm, logs } = await loadMockNpm(t, { load: false }) + process.emit('time', 'foo') + process.emit('time', 'bar') + t.match(npm.unfinishedTimers.get('foo'), Number, 'foo timer is a number') + t.match(npm.unfinishedTimers.get('bar'), Number, 'foo timer is a number') + process.emit('timeEnd', 'foo') + process.emit('timeEnd', 'bar') + process.emit('timeEnd', 'baz') + // npm timer is started by default + process.emit('timeEnd', 'npm') + t.match(logs.timing, [ + ['foo', /Completed in [0-9]+ms/], + ['bar', /Completed in [0-9]+ms/], + ['npm', /Completed in [0-9]+ms/], + ]) + t.match(logs.silly, [[ 'timing', "Tried to end timer that doesn't exist:", 'baz', - ], - ]) - t.notOk(npm.timers.has('foo'), 'foo timer is gone') - t.notOk(npm.timers.has('bar'), 'bar timer is gone') - t.match(npm.timings, { foo: Number, bar: Number }) - t.end() + ]]) + t.notOk(npm.unfinishedTimers.has('foo'), 'foo timer is gone') + t.notOk(npm.unfinishedTimers.has('bar'), 'bar timer is gone') + t.match(npm.finishedTimers, { foo: Number, bar: Number, npm: Number }) + t.end() + }) + + t.test('writes timings file', async t => { + const { npm, timingFile } = await loadMockNpm(t, { + config: { timing: true }, + }) + process.emit('time', 'foo') + process.emit('timeEnd', 'foo') + process.emit('time', 'bar') + npm.unload() + const timings = await timingFile() + t.match(timings, { + command: [], + logfile: String, + logfiles: [String], + version: String, + unfinished: { + bar: [Number, Number], + npm: [Number, Number], + }, + foo: Number, + 'npm:load': Number, + }) + }) + + t.test('does not write timings file with timers:false', async t => { + const { npm, timingFile } = await loadMockNpm(t, { + config: { false: true }, + }) + npm.unload() + await t.rejects(() => timingFile()) + }) }) -t.test('output clears progress and console.logs the message', t => { - const mock = mockNpm(t) - const { Npm, logs } = mock - const npm = new Npm() - npm.output = mock.npmOutput - const { log } = console - const { log: { clearProgress, showProgress } } = npm +t.test('output clears progress and console.logs the message', async t => { + t.plan(2) let showingProgress = true - npm.log.clearProgress = () => showingProgress = false - npm.log.showProgress = () => showingProgress = true - console.log = (...args) => { - t.equal(showingProgress, false, 'should not be showing progress right now') - logs.push(args) - } - t.teardown(() => { - console.log = log - npm.log.showProgress = showProgress - npm.log.clearProgress = clearProgress + const logs = [] + mockGlobals(t, { + 'console.log': (...args) => { + t.equal(showingProgress, false, 'should not be showing progress right now') + logs.push(args) + }, }) - - npm.output('hello') - t.strictSame(logs, [['hello']]) + const { npm } = await loadMockNpm(t, { + load: false, + mocks: { + npmlog: { + clearProgress: () => showingProgress = false, + showProgress: () => showingProgress = true, + }, + }, + }) + npm.originalOutput('hello') + t.match(logs, [['hello']]) t.end() }) t.test('unknown command', async t => { - const mock = mockNpm(t) - const { Npm } = mock - const npm = new Npm() + const { npm } = await loadMockNpm(t, { load: false }) await t.rejects( npm.cmd('thisisnotacommand'), { code: 'EUNKNOWNCOMMAND' } diff --git a/test/lib/utils/audit-error.js b/test/lib/utils/audit-error.js index c683053cbf787..bcb7d8c16dd7b 100644 --- a/test/lib/utils/audit-error.js +++ b/test/lib/utils/audit-error.js @@ -3,14 +3,15 @@ const t = require('tap') const LOGS = [] const OUTPUT = [] const output = (...msg) => OUTPUT.push(msg) -const auditError = require('../../../lib/utils/audit-error.js') +const auditError = t.mock('../../../lib/utils/audit-error.js', { + 'proc-log': { + warn: (...msg) => LOGS.push(msg), + }, +}) const npm = { command: null, flatOptions: {}, - log: { - warn: (...msg) => LOGS.push(msg), - }, output, } t.afterEach(() => { diff --git a/test/lib/utils/cleanup-log-files.js b/test/lib/utils/cleanup-log-files.js deleted file mode 100644 index e97cf36b55dec..0000000000000 --- a/test/lib/utils/cleanup-log-files.js +++ /dev/null @@ -1,79 +0,0 @@ -const t = require('tap') - -const glob = require('glob') -const rimraf = require('rimraf') -const mocks = { glob, rimraf } -const cleanup = t.mock('../../../lib/utils/cleanup-log-files.js', { - glob: (...args) => mocks.glob(...args), - rimraf: (...args) => mocks.rimraf(...args), -}) -const { basename } = require('path') - -const fs = require('fs') - -t.test('clean up those files', t => { - const cache = t.testdir({ - _logs: { - '1-debug.log': 'hello', - '2-debug.log': 'hello', - '3-debug.log': 'hello', - '4-debug.log': 'hello', - '5-debug.log': 'hello', - }, - }) - const warn = (...warning) => t.fail('failed cleanup', { warning }) - return cleanup(cache, 3, warn).then(() => { - t.strictSame(fs.readdirSync(cache + '/_logs').sort(), [ - '3-debug.log', - '4-debug.log', - '5-debug.log', - ]) - }) -}) - -t.test('nothing to clean up', t => { - const cache = t.testdir({ - _logs: { - '4-debug.log': 'hello', - '5-debug.log': 'hello', - }, - }) - const warn = (...warning) => t.fail('failed cleanup', { warning }) - return cleanup(cache, 3, warn).then(() => { - t.strictSame(fs.readdirSync(cache + '/_logs').sort(), [ - '4-debug.log', - '5-debug.log', - ]) - }) -}) - -t.test('glob fail', t => { - mocks.glob = (pattern, cb) => cb(new Error('no globbity')) - t.teardown(() => mocks.glob = glob) - const cache = t.testdir({}) - const warn = (...warning) => t.fail('failed cleanup', { warning }) - return cleanup(cache, 3, warn) -}) - -t.test('rimraf fail', t => { - mocks.rimraf = (file, cb) => cb(new Error('youll never rimraf me!')) - t.teardown(() => mocks.rimraf = rimraf) - - const cache = t.testdir({ - _logs: { - '1-debug.log': 'hello', - '2-debug.log': 'hello', - '3-debug.log': 'hello', - '4-debug.log': 'hello', - '5-debug.log': 'hello', - }, - }) - const warnings = [] - const warn = (...warning) => warnings.push(basename(warning[2])) - return cleanup(cache, 3, warn).then(() => { - t.strictSame(warnings.sort((a, b) => a.localeCompare(b, 'en')), [ - '1-debug.log', - '2-debug.log', - ]) - }) -}) diff --git a/test/lib/utils/config/definitions.js b/test/lib/utils/config/definitions.js index 7af0b683987e4..c19d971faba20 100644 --- a/test/lib/utils/config/definitions.js +++ b/test/lib/utils/config/definitions.js @@ -1,11 +1,9 @@ const t = require('tap') - const { resolve } = require('path') +const mockGlobals = require('../../../fixtures/mock-globals') // have to fake the node version, or else it'll only pass on this one -Object.defineProperty(process, 'version', { - value: 'v14.8.0', -}) +mockGlobals(t, { 'process.version': 'v14.8.0', 'process.env.NODE_ENV': undefined }) // also fake the npm version, so that it doesn't get reset every time const pkg = require('../../../../package.json') @@ -13,8 +11,6 @@ const pkg = require('../../../../package.json') // this is a pain to keep typing const defpath = '../../../../lib/utils/config/definitions.js' -// set this in the test when we need it -delete process.env.NODE_ENV const definitions = require(defpath) // Tie the definitions to a snapshot so that if they change we are forced to @@ -43,22 +39,19 @@ t.test('basic flattening function camelCases from css-case', t => { t.test('editor', t => { t.test('has EDITOR and VISUAL, use EDITOR', t => { - process.env.EDITOR = 'vim' - process.env.VISUAL = 'mate' + mockGlobals(t, { 'process.env': { EDITOR: 'vim', VISUAL: 'mate' } }) const defs = t.mock(defpath) t.equal(defs.editor.default, 'vim') t.end() }) t.test('has VISUAL but no EDITOR, use VISUAL', t => { - delete process.env.EDITOR - process.env.VISUAL = 'mate' + mockGlobals(t, { 'process.env': { EDITOR: undefined, VISUAL: 'mate' } }) const defs = t.mock(defpath) t.equal(defs.editor.default, 'mate') t.end() }) t.test('has neither EDITOR nor VISUAL, system specific', t => { - delete process.env.EDITOR - delete process.env.VISUAL + mockGlobals(t, { 'process.env': { EDITOR: undefined, VISUAL: undefined } }) const defsWin = t.mock(defpath, { [isWin]: true, }) @@ -74,12 +67,12 @@ t.test('editor', t => { t.test('shell', t => { t.test('windows, env.ComSpec then cmd.exe', t => { - process.env.ComSpec = 'command.com' + mockGlobals(t, { 'process.env.ComSpec': 'command.com' }) const defsComSpec = t.mock(defpath, { [isWin]: true, }) t.equal(defsComSpec.shell.default, 'command.com') - delete process.env.ComSpec + mockGlobals(t, { 'process.env.ComSpec': undefined }) const defsNoComSpec = t.mock(defpath, { [isWin]: true, }) @@ -88,12 +81,12 @@ t.test('shell', t => { }) t.test('nix, SHELL then sh', t => { - process.env.SHELL = '/usr/local/bin/bash' + mockGlobals(t, { 'process.env.SHELL': '/usr/local/bin/bash' }) const defsShell = t.mock(defpath, { [isWin]: false, }) t.equal(defsShell.shell.default, '/usr/local/bin/bash') - delete process.env.SHELL + mockGlobals(t, { 'process.env.SHELL': undefined }) const defsNoShell = t.mock(defpath, { [isWin]: false, }) @@ -136,43 +129,40 @@ t.test('local-address allowed types', t => { }) t.test('unicode allowed?', t => { - const { LC_ALL, LC_CTYPE, LANG } = process.env - t.teardown(() => Object.assign(process.env, { LC_ALL, LC_CTYPE, LANG })) + const setGlobal = (obj = {}) => mockGlobals(t, { 'process.env': obj }) - process.env.LC_ALL = 'utf8' - process.env.LC_CTYPE = 'UTF-8' - process.env.LANG = 'Unicode utf-8' + setGlobal({ LC_ALL: 'utf8', LC_CTYPE: 'UTF-8', LANG: 'Unicode utf-8' }) const lcAll = t.mock(defpath) t.equal(lcAll.unicode.default, true) - process.env.LC_ALL = 'no unicode for youUUUU!' + setGlobal({ LC_ALL: 'no unicode for youUUUU!' }) const noLcAll = t.mock(defpath) t.equal(noLcAll.unicode.default, false) - delete process.env.LC_ALL + setGlobal({ LC_ALL: undefined }) const lcCtype = t.mock(defpath) t.equal(lcCtype.unicode.default, true) - process.env.LC_CTYPE = 'something other than unicode version 8' + setGlobal({ LC_CTYPE: 'something other than unicode version 8' }) const noLcCtype = t.mock(defpath) t.equal(noLcCtype.unicode.default, false) - delete process.env.LC_CTYPE + setGlobal({ LC_CTYPE: undefined }) const lang = t.mock(defpath) t.equal(lang.unicode.default, true) - process.env.LANG = 'ISO-8859-1' + setGlobal({ LANG: 'ISO-8859-1' }) const noLang = t.mock(defpath) t.equal(noLang.unicode.default, false) t.end() }) t.test('cache', t => { - process.env.LOCALAPPDATA = 'app/data/local' + mockGlobals(t, { 'process.env.LOCALAPPDATA': 'app/data/local' }) const defsWinLocalAppData = t.mock(defpath, { [isWin]: true, }) t.equal(defsWinLocalAppData.cache.default, 'app/data/local/npm-cache') - delete process.env.LOCALAPPDATA + mockGlobals(t, { 'process.env.LOCALAPPDATA': undefined }) const defsWinNoLocalAppData = t.mock(defpath, { [isWin]: true, }) @@ -241,7 +231,7 @@ t.test('flatteners that populate flat.omit array', t => { definitions.omit.flatten('omit', obj, flat) t.strictSame(flat, { omit: ['optional'] }, 'do not omit what is included') - process.env.NODE_ENV = 'production' + mockGlobals(t, { 'process.env.NODE_ENV': 'production' }) const defProdEnv = t.mock(defpath) t.strictSame(defProdEnv.omit.default, ['dev'], 'omit dev in production') t.end() @@ -372,36 +362,47 @@ t.test('cache-min', t => { }) t.test('color', t => { - const { isTTY } = process.stdout - t.teardown(() => process.stdout.isTTY = isTTY) + const setTTY = (stream, value) => mockGlobals(t, { [`process.${stream}.isTTY`]: value }) const flat = {} const obj = { color: 'always' } definitions.color.flatten('color', obj, flat) - t.strictSame(flat, { color: true }, 'true when --color=always') + t.strictSame(flat, { color: true, logColor: true }, 'true when --color=always') obj.color = false definitions.color.flatten('color', obj, flat) - t.strictSame(flat, { color: false }, 'true when --no-color') + t.strictSame(flat, { color: false, logColor: false }, 'true when --no-color') - process.stdout.isTTY = false + setTTY('stdout', false) obj.color = true definitions.color.flatten('color', obj, flat) - t.strictSame(flat, { color: false }, 'no color when stdout not tty') - process.stdout.isTTY = true + t.strictSame(flat, { color: false, logColor: false }, 'no color when stdout not tty') + setTTY('stdout', true) definitions.color.flatten('color', obj, flat) - t.strictSame(flat, { color: true }, '--color turns on color when stdout is tty') + t.strictSame(flat, { color: true, logColor: false }, '--color turns on color when stdout is tty') + setTTY('stdout', false) + + setTTY('stderr', false) + obj.color = true + definitions.color.flatten('color', obj, flat) + t.strictSame(flat, { color: false, logColor: false }, 'no color when stderr not tty') + setTTY('stderr', true) + definitions.color.flatten('color', obj, flat) + t.strictSame(flat, { color: false, logColor: true }, '--color turns on color when stderr is tty') + setTTY('stderr', false) + + const setColor = (value) => mockGlobals(t, { 'process.env.NO_COLOR': value }) - delete process.env.NO_COLOR + setColor(undefined) const defsAllowColor = t.mock(defpath) t.equal(defsAllowColor.color.default, true, 'default true when no NO_COLOR env') - process.env.NO_COLOR = '0' + setColor('0') const defsNoColor0 = t.mock(defpath) t.equal(defsNoColor0.color.default, true, 'default true when no NO_COLOR=0') - process.env.NO_COLOR = '1' + setColor('1') const defsNoColor1 = t.mock(defpath) t.equal(defsNoColor1.color.default, false, 'default false when no NO_COLOR=1') diff --git a/test/lib/utils/did-you-mean.js b/test/lib/utils/did-you-mean.js index 185368d61f2ed..d3cb3a24f0ae5 100644 --- a/test/lib/utils/did-you-mean.js +++ b/test/lib/utils/did-you-mean.js @@ -1,11 +1,9 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm.js') -const { Npm } = mockNpm(t) -const npm = new Npm() +const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') const dym = require('../../../lib/utils/did-you-mean.js') t.test('did-you-mean', async t => { - await npm.load() + const { npm } = await loadMockNpm(t) t.test('with package.json', async t => { const testdir = t.testdir({ 'package.json': JSON.stringify({ diff --git a/test/lib/utils/display.js b/test/lib/utils/display.js new file mode 100644 index 0000000000000..30cd2cc270dc5 --- /dev/null +++ b/test/lib/utils/display.js @@ -0,0 +1,85 @@ +const t = require('tap') +const log = require('../../../lib/utils/log-shim') +const mockLogs = require('../../fixtures/mock-logs') +const mockGlobals = require('../../fixtures/mock-globals') + +const mockDisplay = (t, mocks) => { + const { logs, logMocks } = mockLogs(mocks) + const Display = t.mock('../../../lib/utils/display', { + ...mocks, + ...logMocks, + }) + const display = new Display() + t.teardown(() => display.off()) + return { display, logs } +} + +t.test('setup', async (t) => { + const { display } = mockDisplay(t) + + display.load({ timing: true, loglevel: 'notice' }) + t.equal(log.level, 'timing') + + display.load({ timing: false, loglevel: 'notice' }) + t.equal(log.level, 'notice') + + display.load({ color: true }) + t.equal(log.useColor(), true) + + display.load({ unicode: true }) + t.equal(log.gauge._theme.hasUnicode, true) + + display.load({ unicode: false }) + t.equal(log.gauge._theme.hasUnicode, false) + + mockGlobals(t, { 'process.stderr.isTTY': true }) + display.load({ progress: true }) + t.equal(log.progressEnabled, true) +}) + +t.test('can log', async (t) => { + const explains = [] + const { display, logs } = mockDisplay(t, { + npmlog: { + error: (...args) => logs.push(['error', ...args]), + warn: (...args) => logs.push(['warn', ...args]), + }, + '../../../lib/utils/explain-eresolve.js': { + explain: (...args) => { + explains.push(args) + return 'explanation' + }, + }, + }) + + display.log('error', 'test') + t.match(logs.error, [['test']]) + + display.log('warn', 'ERESOLVE', 'hello', { some: 'object' }) + t.match(logs.warn, [['ERESOLVE', 'hello']]) + t.match(explains, [[{ some: 'object' }, false, 2]]) +}) + +t.test('handles log throwing', async (t) => { + const errors = [] + mockGlobals(t, { + 'console.error': (...args) => errors.push(args), + }) + const { display } = mockDisplay(t, { + npmlog: { + verbose: () => { + throw new Error('verbose') + }, + }, + '../../../lib/utils/explain-eresolve.js': { + explain: () => { + throw new Error('explain') + }, + }, + }) + + display.log('warn', 'ERESOLVE', 'hello', { some: 'object' }) + t.match(errors, [ + [/attempt to log .* crashed/, Error('explain'), Error('verbose')], + ]) +}) diff --git a/test/lib/utils/error-message.js b/test/lib/utils/error-message.js index 1959b9217a7d0..ddc88c1d990b0 100644 --- a/test/lib/utils/error-message.js +++ b/test/lib/utils/error-message.js @@ -1,87 +1,51 @@ const t = require('tap') const path = require('path') -const { real: mockNpm } = require('../../fixtures/mock-npm.js') -const { Npm } = mockNpm(t, { - '../../package.json': { - version: '123.456.789-npm', +const { load: _loadMockNpm } = require('../../fixtures/mock-npm.js') +const mockGlobals = require('../../fixtures/mock-globals.js') +const { cleanCwd, cleanDate } = require('../../fixtures/clean-snapshot.js') + +t.cleanSnapshot = p => cleanDate(cleanCwd(p)) + +mockGlobals(t, { + process: { + getuid: () => 867, + getgid: () => 5309, + arch: 'x64', + version: '123.456.789-node', + platform: 'posix', }, }) -const npm = new Npm() -const { Npm: UnloadedNpm } = mockNpm(t, { - '../../package.json': { - version: '123.456.789-npm', - }, -}) -const unloadedNpm = new UnloadedNpm() - -// make a bunch of stuff consistent for snapshots - -process.getuid = () => 867 -process.getgid = () => 5309 - -Object.defineProperty(process, 'arch', { - value: 'x64', - configurable: true, -}) - -Object.defineProperty(process, 'version', { - value: '123.456.789-node', - configurable: true, -}) -const CACHE = '/some/cache/dir' -const testdir = t.testdir({}) -t.before(async () => { - await npm.load() - npm.localPrefix = testdir - unloadedNpm.localPrefix = testdir - npm.config.set('cache', CACHE) - npm.config.set('node-version', '99.99.99') - npm.version = '123.456.789-npm' - unloadedNpm.version = '123.456.789-npm' -}) - -const { resolve } = require('path') - -const npmlog = require('npmlog') -const verboseLogs = [] -npmlog.verbose = (...message) => { - verboseLogs.push(message) -} - -const EXPLAIN_CALLED = [] -const mocks = { - '../../../lib/utils/explain-eresolve.js': { - report: (...args) => { - EXPLAIN_CALLED.push(args) - return 'explanation' +const loadMockNpm = async (t, { load, command, testdir, config } = {}) => { + const { npm, ...rest } = await _loadMockNpm(t, { + load, + testdir, + config, + mocks: { + '../../package.json': { + version: '123.456.789-npm', + }, }, - }, - // XXX ??? - get '../../../lib/utils/is-windows.js' () { - return process.platform === 'win32' - }, -} -let errorMessage = t.mock('../../../lib/utils/error-message.js', { ...mocks }) - -const beWindows = () => { - Object.defineProperty(process, 'platform', { - value: 'win32', - configurable: true, }) - errorMessage = t.mock('../../../lib/utils/error-message.js', { ...mocks }) + if (command !== undefined) { + npm.command = command + } + return { + npm, + ...rest, + } } -const bePosix = () => { - Object.defineProperty(process, 'platform', { - value: 'posix', - configurable: true, - }) - errorMessage = t.mock('../../../lib/utils/error-message.js', { ...mocks }) -} +const errorMessage = (er, { mocks, logMocks, npm } = {}) => + t.mock('../../../lib/utils/error-message.js', { ...mocks, ...logMocks })(er, npm) -t.test('just simple messages', t => { - npm.command = 'audit' +t.test('just simple messages', async t => { + const npm = await loadMockNpm(t, { + command: 'audit', + config: { + 'node-version': '99.99.99', + }, + }) const codes = [ 'ENOAUDIT', 'ENOLOCK', @@ -108,7 +72,7 @@ t.test('just simple messages', t => { 'ERR_SOCKET_TIMEOUT', ] t.plan(codes.length) - codes.forEach(code => { + codes.forEach(async code => { const path = '/some/path' const pkgid = 'some@package' const file = '/some/file' @@ -124,8 +88,8 @@ t.test('just simple messages', t => { }) }) -t.test('replace message/stack sensistive info', t => { - npm.command = 'audit' +t.test('replace message/stack sensistive info', async t => { + const npm = await loadMockNpm(t, { command: 'audit' }) const path = '/some/path' const pkgid = 'some@package' const file = '/some/file' @@ -139,10 +103,10 @@ t.test('replace message/stack sensistive info', t => { stack, }) t.matchSnapshot(errorMessage(er, npm)) - t.end() }) -t.test('bad engine without config loaded', t => { +t.test('bad engine without config loaded', async t => { + const npm = await loadMockNpm(t, { load: false }) const path = '/some/path' const pkgid = 'some@package' const file = '/some/file' @@ -154,11 +118,11 @@ t.test('bad engine without config loaded', t => { file, stack, }) - t.matchSnapshot(errorMessage(er, unloadedNpm)) - t.end() + t.matchSnapshot(errorMessage(er, npm)) }) -t.test('enoent without a file', t => { +t.test('enoent without a file', async t => { + const npm = await loadMockNpm(t) const path = '/some/path' const pkgid = 'some@package' const stack = 'dummy stack trace' @@ -169,11 +133,10 @@ t.test('enoent without a file', t => { stack, }) t.matchSnapshot(errorMessage(er, npm)) - t.end() }) -t.test('enolock without a command', t => { - npm.command = null +t.test('enolock without a command', async t => { + const npm = await loadMockNpm(t, { command: null }) const path = '/some/path' const pkgid = 'some@package' const file = '/some/file' @@ -186,12 +149,12 @@ t.test('enolock without a command', t => { stack, }) t.matchSnapshot(errorMessage(er, npm)) - t.end() }) -t.test('default message', t => { +t.test('default message', async t => { + const npm = await loadMockNpm(t) t.matchSnapshot(errorMessage(new Error('error object'), npm)) - t.matchSnapshot(errorMessage('error string'), npm) + t.matchSnapshot(errorMessage('error string', npm)) t.matchSnapshot(errorMessage(Object.assign(new Error('cmd err'), { cmd: 'some command', signal: 'SIGYOLO', @@ -199,10 +162,10 @@ t.test('default message', t => { stdout: 'stdout', stderr: 'stderr', }), npm)) - t.end() }) -t.test('args are cleaned', t => { +t.test('args are cleaned', async t => { + const npm = await loadMockNpm(t) t.matchSnapshot(errorMessage(Object.assign(new Error('cmd err'), { cmd: 'some command', signal: 'SIGYOLO', @@ -210,35 +173,25 @@ t.test('args are cleaned', t => { stdout: 'stdout', stderr: 'stderr', }), npm)) - t.end() }) -t.test('eacces/eperm', t => { - const runTest = (windows, loaded, cachePath, cacheDest) => t => { +t.test('eacces/eperm', async t => { + const runTest = (windows, loaded, cachePath, cacheDest) => async t => { if (windows) { - beWindows() - } else { - bePosix() + mockGlobals(t, { 'process.platform': 'win32' }) } - - const path = `${cachePath ? CACHE : '/not/cache/dir'}/path` - const dest = `${cacheDest ? CACHE : '/not/cache/dir'}/dest` + const npm = await loadMockNpm(t, { windows, load: loaded }) + const path = `${cachePath ? npm.cache : '/not/cache/dir'}/path` + const dest = `${cacheDest ? npm.cache : '/not/cache/dir'}/dest` const er = Object.assign(new Error('whoopsie'), { code: 'EACCES', path, dest, stack: 'dummy stack trace', }) - verboseLogs.length = 0 - if (loaded) { - t.matchSnapshot(errorMessage(er, npm)) - } else { - t.matchSnapshot(errorMessage(er, unloadedNpm)) - } - t.matchSnapshot(verboseLogs) - t.end() - verboseLogs.length = 0 + t.matchSnapshot(errorMessage(er, npm)) + t.matchSnapshot(npm.logs.verbose) } for (const windows of [true, false]) { @@ -251,12 +204,13 @@ t.test('eacces/eperm', t => { } } } - t.end() }) t.test('json parse', t => { - t.test('merge conflict in package.json', t => { - const dir = t.testdir({ + mockGlobals(t, { 'process.argv': ['arg', 'v'] }) + + t.test('merge conflict in package.json', async t => { + const testdir = { 'package.json': ` { "array": [ @@ -295,59 +249,35 @@ t.test('json parse', t => { } } `, - }) - const { prefix } = npm - const { argv } = process - t.teardown(() => { - Object.defineProperty(npm, 'prefix', { - value: prefix, - configurable: true, - }) - process.argv = argv - }) - Object.defineProperty(npm, 'prefix', { value: dir, configurable: true }) - process.argv = ['arg', 'v'] + } + const npm = await loadMockNpm(t, { testdir }) t.matchSnapshot(errorMessage(Object.assign(new Error('conflicted'), { code: 'EJSONPARSE', - path: resolve(dir, 'package.json'), + path: path.resolve(npm.prefix, 'package.json'), }), npm)) t.end() }) - t.test('just regular bad json in package.json', t => { - const dir = t.testdir({ + t.test('just regular bad json in package.json', async t => { + const testdir = { 'package.json': 'not even slightly json', - }) - const { prefix } = npm - const { argv } = process - t.teardown(() => { - Object.defineProperty(npm, 'prefix', { - value: prefix, - configurable: true, - }) - process.argv = argv - }) - Object.defineProperty(npm, 'prefix', { value: dir, configurable: true }) - process.argv = ['arg', 'v'] + } + const npm = await loadMockNpm(t, { testdir }) t.matchSnapshot(errorMessage(Object.assign(new Error('not json'), { code: 'EJSONPARSE', - path: resolve(dir, 'package.json'), + path: path.resolve(npm.prefix, 'package.json'), }), npm)) t.end() }) - t.test('json somewhere else', t => { - const dir = t.testdir({ + t.test('json somewhere else', async t => { + const testdir = { 'blerg.json': 'not even slightly json', - }) - const { argv } = process - t.teardown(() => { - process.argv = argv - }) - process.argv = ['arg', 'v'] + } + const npm = await loadMockNpm(t, { testdir }) t.matchSnapshot(errorMessage(Object.assign(new Error('not json'), { code: 'EJSONPARSE', - path: `${dir}/blerg.json`, + path: path.resolve(npm.prefix, 'blerg.json'), }), npm)) t.end() }) @@ -355,7 +285,9 @@ t.test('json parse', t => { t.end() }) -t.test('eotp/e401', t => { +t.test('eotp/e401', async t => { + const npm = await loadMockNpm(t) + t.test('401, no auth headers', t => { t.matchSnapshot(errorMessage(Object.assign(new Error('nope'), { code: 'E401', @@ -406,11 +338,11 @@ t.test('eotp/e401', t => { }) } }) - - t.end() }) -t.test('404', t => { +t.test('404', async t => { + const npm = await loadMockNpm(t) + t.test('no package id', t => { const er = Object.assign(new Error('404 not found'), { code: 'E404' }) t.matchSnapshot(errorMessage(er, npm)) @@ -448,10 +380,11 @@ t.test('404', t => { t.matchSnapshot(errorMessage(er, npm)) t.end() }) - t.end() }) -t.test('bad platform', t => { +t.test('bad platform', async t => { + const npm = await loadMockNpm(t) + t.test('string os/arch', t => { const er = Object.assign(new Error('a bad plat'), { pkgid: 'lodash@1.0.0', @@ -484,19 +417,30 @@ t.test('bad platform', t => { t.matchSnapshot(errorMessage(er, npm)) t.end() }) - - t.end() }) -t.test('explain ERESOLVE errors', t => { +t.test('explain ERESOLVE errors', async t => { + const npm = await loadMockNpm(t) + const EXPLAIN_CALLED = [] + const er = Object.assign(new Error('could not resolve'), { code: 'ERESOLVE', }) - t.matchSnapshot(errorMessage(er, npm)) + + t.matchSnapshot(errorMessage(er, { + ...npm, + mocks: { + '../../../lib/utils/explain-eresolve.js': { + report: (...args) => { + EXPLAIN_CALLED.push(args) + return 'explanation' + }, + }, + }, + })) t.match(EXPLAIN_CALLED, [[ er, - undefined, + false, path.resolve(npm.cache, 'eresolve-report.txt'), ]]) - t.end() }) diff --git a/test/lib/utils/exit-handler.js b/test/lib/utils/exit-handler.js index adc7c3f4e90fd..54bf48f89b0dd 100644 --- a/test/lib/utils/exit-handler.js +++ b/test/lib/utils/exit-handler.js @@ -1,177 +1,213 @@ -/* eslint-disable no-extend-native */ -/* eslint-disable no-global-assign */ const t = require('tap') -const EventEmitter = require('events') const os = require('os') -const fs = require('fs') -const path = require('path') - -const { real: mockNpm } = require('../../fixtures/mock-npm') - -// generic error to be used in tests -const err = Object.assign(new Error('ERROR'), { code: 'ERROR' }) -err.stack = 'Error: ERROR' - -const redactCwd = (path) => { - const normalizePath = p => p - .replace(/\\+/g, '/') - .replace(/\r\n/g, '\n') - return normalizePath(path) - .replace(new RegExp(normalizePath(process.cwd()), 'g'), '{CWD}') +const EventEmitter = require('events') +const { format } = require('../../../lib/utils/log-file') +const { load: loadMockNpm } = require('../../fixtures/mock-npm') +const mockGlobals = require('../../fixtures/mock-globals') +const { cleanCwd, cleanDate } = require('../../fixtures/clean-snapshot') + +const pick = (obj, ...keys) => keys.reduce((acc, key) => { + acc[key] = obj[key] + return acc +}, {}) + +t.formatSnapshot = (obj) => { + if (Array.isArray(obj)) { + return obj + .map((i) => Array.isArray(i) ? i.join(' ') : i) + .join('\n') + } + return obj } -t.cleanSnapshot = (str) => redactCwd(str) - -const cacheFolder = t.testdir({}) -const logFile = path.resolve(cacheFolder, '_logs', 'expecteddate-debug.log') -const timingFile = path.resolve(cacheFolder, '_timing.json') - -const { Npm } = mockNpm(t, { - '../../package.json': { - version: '1.0.0', - }, -}) -const npm = new Npm() - -t.before(async () => { - await npm.load() - npm.config.set('cache', cacheFolder) -}) +t.cleanSnapshot = (path) => cleanDate(cleanCwd(path)) +// Config loading is dependent on env so strip those from snapshots + .replace(/.*timing config:load:.*\n/gm, '') + .replace(/(Completed in )\d+(ms)/g, '$1{TIME}$2') // cut off process from script so that it won't quit the test runner // while trying to run through the myriad of cases. need to make it // have all the functions signal-exit relies on so that it doesn't // nerf itself, thinking global.process is broken or gone. -const _process = process -process = Object.assign( - new EventEmitter(), - { - argv: ['/node', ..._process.argv.slice(1)], - cwd: _process.cwd, - env: _process.env, +mockGlobals(t, { + process: Object.assign(new EventEmitter(), { + ...pick(process, 'execPath', 'stdout', 'stderr', 'cwd', 'env'), + argv: ['/node', ...process.argv.slice(1)], version: 'v1.0.0', + kill: () => {}, + reallyExit: (code) => process.exit(code), + pid: 123456, exit: (code) => { process.exitCode = code || process.exitCode || 0 process.emit('exit', process.exitCode) }, - stdout: { write (_, cb) { - cb() - } }, - stderr: { write () {} }, - hrtime: _process.hrtime, - kill: () => {}, - reallyExit: (code) => process.exit(code), - pid: 123456, + }), +}, { replace: true }) + +const mockExitHandler = async (t, { init, load, testdir, config } = {}) => { + const errors = [] + mockGlobals(t, { 'console.error': (err) => errors.push(err) }) + + const { npm, logMocks, ...rest } = await loadMockNpm(t, { + init, + load, + testdir, + mocks: { + '../../package.json': { + version: '1.0.0', + }, + }, + config: { + loglevel: 'notice', + ...config, + }, + }) + + const exitHandler = t.mock('../../../lib/utils/exit-handler.js', { + '../../../lib/utils/error-message.js': (err) => ({ + ...err, + summary: [['ERR SUMMARY', err.message]], + detail: [['ERR DETAIL', err.message]], + }), + os: { + type: () => 'Foo', + release: () => '1.0.0', + }, + ...logMocks, + }) + + if (npm) { + exitHandler.setNpm(npm) } -) - -const osType = os.type -const osRelease = os.release -// overrides OS type/release for cross platform snapshots -os.type = () => 'Foo' -os.release = () => '1.0.0' - -// generates logfile name with mocked date -const _toISOString = Date.prototype.toISOString -Date.prototype.toISOString = () => 'expecteddate' - -const consoleError = console.error -const errors = [] -console.error = (err) => { - errors.push(err) -} -t.teardown(() => { - os.type = osType - os.release = osRelease - // needs to put process back in its place in order for tap to exit properly - process = _process - Date.prototype.toISOString = _toISOString - console.error = consoleError -}) -t.afterEach(() => { - errors.length = 0 - npm.log.level = 'silent' - // clear out the 'A complete log' message - npm.log.record.length = 0 - delete process.exitCode -}) + t.teardown(() => { + delete process.exitCode + process.removeAllListeners('exit') + }) -const mocks = { - '../../../lib/utils/error-message.js': (err) => ({ - ...err, - summary: [['ERR', err.message]], - detail: [['ERR', err.message]], - }), + return { + ...rest, + errors, + npm, + // // Make it async to make testing ergonomics a little + // // easier so we dont need to t.plan() every test to + // // make sure we get process.exit called + exitHandler: (...args) => new Promise(resolve => { + process.once('exit', resolve) + exitHandler(...args) + }), + } } -const exitHandler = t.mock('../../../lib/utils/exit-handler.js', mocks) -exitHandler.setNpm(npm) - -t.test('exit handler never called - loglevel silent', (t) => { - npm.log.level = 'silent' - process.emit('exit', 1) - const logData = fs.readFileSync(logFile, 'utf8') - t.match(logData, 'Exit handler never called!') - t.match(errors, [''], 'logs one empty string to console.error') - t.end() -}) +// Create errors with properties to be used in tests +const err = (message = '', options = {}, noStack = false) => { + const e = Object.assign( + new Error(message), + typeof options !== 'object' ? { code: options } : options + ) + e.stack = options.stack || `Error: ${message}` + if (noStack) { + delete e.stack + } + return e +} -t.test('exit handler never called - loglevel notice', (t) => { - npm.log.level = 'notice' - process.emit('exit', 1) - const logData = fs.readFileSync(logFile, 'utf8') - t.match(logData, 'Exit handler never called!') - t.match(errors, ['', ''], 'logs two empty strings to console.error') - t.end() -}) +t.test('handles unknown error with logs and debug file', async (t) => { + const { exitHandler, debugFile, logs } = await mockExitHandler(t) -t.test('handles unknown error', (t) => { - t.plan(2) + await exitHandler(err('Unknown error', 'ECODE')) - npm.log.level = 'notice' + const debugContent = await debugFile() - process.once('timeEnd', (msg) => { - t.equal(msg, 'npm', 'should trigger timeEnd for npm') + t.equal(process.exitCode, 1) + logs.forEach((logItem, i) => { + const logLines = format(i, ...logItem).trim().split(os.EOL) + logLines.forEach((line) => { + t.match(debugContent.trim(), line, 'log appears in debug file') + }) }) - exitHandler(err) - const logData = fs.readFileSync(logFile, 'utf8') - t.matchSnapshot( - logData, - 'should have expected log contents for unknown error' - ) - t.end() + const lastLog = debugContent + .split('\n') + .reduce((__, l) => parseInt(l.match(/^(\d+)\s/)[1])) + t.equal(logs.length, lastLog + 1) + t.match(logs.error, [ + ['code', 'ECODE'], + ['ERR SUMMARY', 'Unknown error'], + ['ERR DETAIL', 'Unknown error'], + ]) + t.match(debugContent, /\d+ error code ECODE/) + t.match(debugContent, /\d+ error ERR SUMMARY Unknown error/) + t.match(debugContent, /\d+ error ERR DETAIL Unknown error/) + t.matchSnapshot(logs, 'logs') + t.matchSnapshot(debugContent, 'debug file contents') }) -t.test('fail to write logfile', (t) => { - t.plan(1) - - t.teardown(() => { - npm.config.set('cache', cacheFolder) +t.test('exit handler never called - loglevel silent', async (t) => { + const { logs, errors } = await mockExitHandler(t, { + config: { loglevel: 'silent' }, }) + process.emit('exit', 1) + t.match(logs.error, [ + ['', /Exit handler never called/], + ['', /error with npm itself/], + ]) + t.strictSame(errors, [''], 'logs one empty string to console.error') +}) - const badDir = t.testdir({ - _logs: 'is a file', - }) +t.test('exit handler never called - loglevel notice', async (t) => { + const { logs, errors } = await mockExitHandler(t) + process.emit('exit', 1) + t.equal(process.exitCode, 1) + t.match(logs.error, [ + ['', /Exit handler never called/], + ['', /error with npm itself/], + ]) + t.strictSame(errors, ['', ''], 'logs two empty strings to console.error') +}) + +t.test('exit handler never called - no npm', async (t) => { + const { logs, errors } = await mockExitHandler(t, { init: false }) + process.emit('exit', 1) + t.equal(process.exitCode, 1) + t.match(logs.error, [ + ['', /Exit handler never called/], + ['', /error with npm itself/], + ]) + t.strictSame(errors, [''], 'logs one empty string to console.error') +}) - npm.config.set('cache', badDir) +t.test('exit handler called - no npm', async (t) => { + const { exitHandler, errors } = await mockExitHandler(t, { init: false }) + await exitHandler() + t.equal(process.exitCode, 1) + t.match(errors, [/Error: Exit prior to setting npm in exit handler/]) +}) - t.doesNotThrow( - () => exitHandler(err), - 'should not throw on cache write failure' - ) +t.test('exit handler called - no npm with error', async (t) => { + const { exitHandler, errors } = await mockExitHandler(t, { init: false }) + await exitHandler(err('something happened')) + t.equal(process.exitCode, 1) + t.match(errors, [/Error: something happened/]) }) -t.test('console.log output using --json', (t) => { - t.plan(1) +t.test('exit handler called - no npm with error without stack', async (t) => { + const { exitHandler, errors } = await mockExitHandler(t, { init: false }) + await exitHandler(err('something happened', {}, true)) + t.equal(process.exitCode, 1) + t.match(errors, [/something happened/]) +}) - npm.config.set('json', true) - t.teardown(() => { - npm.config.set('json', false) +t.test('console.log output using --json', async (t) => { + const { exitHandler, errors } = await mockExitHandler(t, { + config: { + json: true, + }, }) - exitHandler(new Error('Error: EBADTHING Something happened')) + await exitHandler(err('Error: EBADTHING Something happened')) + + t.equal(process.exitCode, 1) t.same( JSON.parse(errors[0]), { @@ -185,213 +221,223 @@ t.test('console.log output using --json', (t) => { ) }) -t.test('throw a non-error obj', (t) => { - t.plan(2) +t.test('throw a non-error obj', async (t) => { + const { exitHandler, logs } = await mockExitHandler(t) - const weirdError = { + await exitHandler({ code: 'ESOMETHING', message: 'foo bar', - } - - process.once('exit', code => { - t.equal(code, 1, 'exits with exitCode 1') }) - exitHandler(weirdError) - t.match( - npm.log.record.find(r => r.level === 'error'), - { message: 'foo bar' } - ) + + t.equal(process.exitCode, 1) + t.match(logs.error, [ + ['weird error', { code: 'ESOMETHING', message: 'foo bar' }], + ]) }) -t.test('throw a string error', (t) => { - t.plan(2) - const error = 'foo bar' +t.test('throw a string error', async (t) => { + const { exitHandler, logs } = await mockExitHandler(t) - process.once('exit', code => { - t.equal(code, 1, 'exits with exitCode 1') - }) - exitHandler(error) - t.match( - npm.log.record.find(r => r.level === 'error'), - { message: 'foo bar' } - ) + await exitHandler('foo bar') + + t.equal(process.exitCode, 1) + t.match(logs.error, [ + ['', 'foo bar'], + ]) }) -t.test('update notification', (t) => { - const updateMsg = 'you should update npm!' - npm.updateNotification = updateMsg - npm.log.level = 'silent' +t.test('update notification', async (t) => { + const { exitHandler, logs, npm } = await mockExitHandler(t) + npm.updateNotification = 'you should update npm!' - t.teardown(() => { - delete npm.updateNotification - }) + await exitHandler() - exitHandler() - t.match( - npm.log.record.find(r => r.level === 'notice'), - { message: 'you should update npm!' } - ) - t.end() + t.match(logs.notice, [ + ['', 'you should update npm!'], + ]) }) -t.test('npm.config not ready', (t) => { - t.plan(1) +t.test('npm.config not ready', async (t) => { + const { exitHandler, logs, errors } = await mockExitHandler(t, { + load: false, + }) - const { Npm: Unloaded } = mockNpm(t) - const unloaded = new Unloaded() + await exitHandler() - t.teardown(() => { - exitHandler.setNpm(npm) + t.equal(process.exitCode, 1) + t.match(errors, [ + /Error: Exit prior to config file resolving./, + ], 'should exit with config error msg') + t.match(logs.verbose, [ + ['stack', /Error: Exit prior to config file resolving./], + ], 'should exit with config error msg') +}) + +t.test('timing with no error', async (t) => { + const { exitHandler, timingFile, npm, logs } = await mockExitHandler(t, { + config: { + timing: true, + }, }) - exitHandler.setNpm(unloaded) + await exitHandler() + const timingFileData = await timingFile() + + t.equal(process.exitCode, 0) + + t.match(logs.error, [ + ['', /A complete log of this run can be found in:[\s\S]*-debug-\d\.log/], + ]) - exitHandler() t.match( - errors[0], - /Error: Exit prior to config file resolving./, - 'should exit with config error msg' + timingFileData, + Object.keys(npm.finishedTimers).reduce((acc, k) => { + acc[k] = Number + return acc + }, {}) ) - t.end() + t.strictSame(npm.unfinishedTimers, new Map()) + t.match(timingFileData, { + command: [], + version: '1.0.0', + npm: Number, + logfile: String, + logfiles: [String], + }) }) -t.test('timing', (t) => { - npm.config.set('timing', true) - - t.teardown(() => { - fs.unlinkSync(timingFile) - npm.config.set('timing', false) +t.test('unfinished timers', async (t) => { + const { exitHandler, timingFile, npm } = await mockExitHandler(t, { + config: { + timing: true, + }, }) - exitHandler() - const timingData = JSON.parse(fs.readFileSync(timingFile, 'utf8')) - t.match(timingData, { version: '1.0.0', 'config:load:defaults': Number }) - t.end() -}) + process.emit('time', 'foo') + process.emit('time', 'bar') -t.test('timing - with error', (t) => { - npm.config.set('timing', true) + await exitHandler() + const timingFileData = await timingFile() - t.teardown(() => { - fs.unlinkSync(timingFile) - npm.config.set('timing', false) + t.equal(process.exitCode, 0) + t.match(npm.unfinishedTimers, new Map([['foo', Number], ['bar', Number]])) + t.match(timingFileData, { + command: [], + version: '1.0.0', + npm: Number, + logfile: String, + logfiles: [String], + unfinished: { + foo: [Number, Number], + bar: [Number, Number], + }, }) - - exitHandler(err) - const timingData = JSON.parse(fs.readFileSync(timingFile, 'utf8')) - t.match(timingData, { version: '1.0.0', 'config:load:defaults': Number }) - t.end() }) -t.test('uses code from errno', (t) => { - t.plan(1) +t.test('uses code from errno', async (t) => { + const { exitHandler, logs } = await mockExitHandler(t) - process.once('exit', code => { - t.equal(code, 127, 'should set exitCode from errno') - }) - exitHandler(Object.assign( - new Error('Error with errno'), - { - errno: 127, - } - )) + await exitHandler(err('Error with errno', { errno: 127 })) + t.equal(process.exitCode, 127) + t.match(logs.error, [['errno', 127]]) }) -t.test('uses code from number', (t) => { - t.plan(1) +t.test('uses code from number', async (t) => { + const { exitHandler, logs } = await mockExitHandler(t) - process.once('exit', code => { - t.equal(code, 404, 'should set exitCode from a number') - }) - exitHandler(Object.assign( - new Error('Error with code type number'), - { - code: 404, - } - )) + await exitHandler(err('Error with code type number', 404)) + t.equal(process.exitCode, 404) + t.match(logs.error, [['code', 404]]) }) -t.test('call exitHandler with no error', (t) => { - t.plan(1) - process.once('exit', code => { - t.equal(code, 0, 'should end up with exitCode 0 (default)') - }) - exitHandler() +t.test('uses all err special properties', async t => { + const { exitHandler, logs } = await mockExitHandler(t) + + const keys = ['code', 'syscall', 'file', 'path', 'dest', 'errno'] + const properties = keys.reduce((acc, k) => { + acc[k] = `${k}-hey` + return acc + }, {}) + + await exitHandler(err('Error with code type number', properties)) + t.equal(process.exitCode, 1) + t.match(logs.error, keys.map((k) => [k, `${k}-hey`]), 'all special keys get logged') }) -t.test('defaults to log error msg if stack is missing', (t) => { - const { Npm: Unloaded } = mockNpm(t) - const unloaded = new Unloaded() +t.test('verbose logs replace info on err props', async t => { + const { exitHandler, logs } = await mockExitHandler(t) - t.teardown(() => { - exitHandler.setNpm(npm) - }) + const keys = ['type', 'stack', 'statusCode', 'pkgid'] + const properties = keys.reduce((acc, k) => { + acc[k] = `${k}-https://user:pass@registry.npmjs.org/` + return acc + }, {}) - exitHandler.setNpm(unloaded) - const noStackErr = Object.assign( - new Error('Error with no stack'), - { - code: 'ENOSTACK', - errno: 127, - } + await exitHandler(err('Error with code type number', properties)) + t.equal(process.exitCode, 1) + t.match( + logs.verbose.filter(([p]) => p !== 'logfile'), + keys.map((k) => [k, `${k}-https://user:***@registry.npmjs.org/`]), + 'all special keys get replaced' ) - delete noStackErr.stack +}) - exitHandler(noStackErr) - t.equal(errors[0], 'Error with no stack', 'should use error msg') - t.end() +t.test('call exitHandler with no error', async (t) => { + const { exitHandler, logs } = await mockExitHandler(t) + + await exitHandler() + + t.equal(process.exitCode, 0) + t.match(logs.error, []) +}) + +t.test('defaults to log error msg if stack is missing when unloaded', async (t) => { + const { exitHandler, logs, errors } = await mockExitHandler(t, { load: false }) + + await exitHandler(err('Error with no stack', { code: 'ENOSTACK', errno: 127 }, true)) + t.equal(process.exitCode, 127) + t.same(errors, ['Error with no stack'], 'should use error msg') + t.match(logs.error, [ + ['code', 'ENOSTACK'], + ['errno', 127], + ]) }) -t.test('exits uncleanly when only emitting exit event', (t) => { - t.plan(2) +t.test('exits uncleanly when only emitting exit event', async (t) => { + const { logs } = await mockExitHandler(t) - npm.log.level = 'silent' process.emit('exit') - const logData = fs.readFileSync(logFile, 'utf8') - t.match(logData, 'Exit handler never called!') - t.match(process.exitCode, 1, 'exitCode coerced to 1') + + t.match(logs.error, [['', 'Exit handler never called!']]) + t.equal(process.exitCode, 1, 'exitCode coerced to 1') t.end() }) -t.test('do no fancy handling for shellouts', t => { - const { command } = npm - const LOG_RECORD = [] - npm.command = 'exec' +t.test('do no fancy handling for shellouts', async t => { + const { exitHandler, npm, logs } = await mockExitHandler(t) - t.teardown(() => { - npm.command = command - }) - t.beforeEach(() => LOG_RECORD.length = 0) + npm.command = 'exec' - const loudNoises = () => npm.log.record - .filter(({ level }) => ['warn', 'error'].includes(level)) + const loudNoises = () => + logs.filter(([level]) => ['warn', 'error'].includes(level)) - t.test('shellout with a numeric error code', t => { - t.plan(2) - process.once('exit', code => { - t.equal(code, 5, 'got expected exit code') - }) - exitHandler(Object.assign(new Error(), { code: 5 })) + t.test('shellout with a numeric error code', async t => { + await exitHandler(err('', 5)) + t.equal(process.exitCode, 5, 'got expected exit code') t.strictSame(loudNoises(), [], 'no noisy warnings') }) - t.test('shellout without a numeric error code (something in npm)', t => { - t.plan(2) - process.once('exit', code => { - t.equal(code, 1, 'got expected exit code') - }) - exitHandler(Object.assign(new Error(), { code: 'banana stand' })) + t.test('shellout without a numeric error code (something in npm)', async t => { + await exitHandler(err('', 'banana stand')) + t.equal(process.exitCode, 1, 'got expected exit code') // should log some warnings and errors, because something weird happened t.strictNotSame(loudNoises(), [], 'bring the noise') t.end() }) - t.test('shellout with code=0 (extra weird?)', t => { - t.plan(2) - process.once('exit', code => { - t.equal(code, 1, 'got expected exit code') - }) - exitHandler(Object.assign(new Error(), { code: 0 })) + t.test('shellout with code=0 (extra weird?)', async t => { + await exitHandler(Object.assign(new Error(), { code: 0 })) + t.equal(process.exitCode, 1, 'got expected exit code') t.strictNotSame(loudNoises(), [], 'bring the noise') }) diff --git a/test/lib/utils/is-windows-bash.js b/test/lib/utils/is-windows-bash.js index 94fde0ace17ce..0fbebdf8e3d53 100644 --- a/test/lib/utils/is-windows-bash.js +++ b/test/lib/utils/is-windows-bash.js @@ -1,4 +1,5 @@ const t = require('tap') +const mockGlobal = require('../../fixtures/mock-globals.js') const isWindowsBash = () => { delete require.cache[require.resolve('../../../lib/utils/is-windows-bash.js')] @@ -6,23 +7,24 @@ const isWindowsBash = () => { return require('../../../lib/utils/is-windows-bash.js') } -Object.defineProperty(process, 'platform', { - value: 'posix', - configurable: true, -}) -t.equal(isWindowsBash(), false, 'false when not windows') +t.test('posix', (t) => { + mockGlobal(t, { 'process.platform': 'posix' }) + t.equal(isWindowsBash(), false, 'false when not windows') -Object.defineProperty(process, 'platform', { - value: 'win32', - configurable: true, + t.end() }) -process.env.MSYSTEM = 'not ming' -process.env.TERM = 'dumb' -t.equal(isWindowsBash(), false, 'false when not mingw or cygwin') -process.env.TERM = 'cygwin' -t.equal(isWindowsBash(), true, 'true when cygwin') +t.test('win32', (t) => { + mockGlobal(t, { 'process.platform': 'win32' }) + + mockGlobal(t, { 'process.env': { TERM: 'dumb', MSYSTEM: undefined } }) + t.equal(isWindowsBash(), false, 'false when not mingw or cygwin') + + mockGlobal(t, { 'process.env.TERM': 'cygwin' }) + t.equal(isWindowsBash(), true, 'true when cygwin') -process.env.MSYSTEM = 'MINGW64' -process.env.TERM = 'dumb' -t.equal(isWindowsBash(), true, 'true when mingw') + mockGlobal(t, { 'process.env': { TERM: 'dumb', MSYSTEM: 'MINGW64' } }) + t.equal(isWindowsBash(), true, 'true when mingw') + + t.end() +}) diff --git a/test/lib/utils/log-file.js b/test/lib/utils/log-file.js new file mode 100644 index 0000000000000..adc1a2e03ff3d --- /dev/null +++ b/test/lib/utils/log-file.js @@ -0,0 +1,333 @@ +const t = require('tap') +const _fs = require('fs') +const fs = _fs.promises +const path = require('path') +const os = require('os') +const fsMiniPass = require('fs-minipass') +const rimraf = require('rimraf') +const LogFile = require('../../../lib/utils/log-file.js') +const { cleanCwd } = require('../../fixtures/clean-snapshot') + +t.cleanSnapshot = (path) => cleanCwd(path) + +const last = arr => arr[arr.length - 1] +const range = (n) => Array.from(Array(n).keys()) +const makeOldLogs = (count) => { + const d = new Date() + d.setHours(-1) + d.setSeconds(0) + return range(count / 2).reduce((acc, i) => { + const cloneDate = new Date(d.getTime()) + cloneDate.setSeconds(i) + acc[LogFile.fileName(LogFile.logId(cloneDate), 0)] = 'hello' + acc[LogFile.fileName(LogFile.logId(cloneDate), 1)] = 'hello' + return acc + }, {}) +} + +const cleanErr = (message) => { + const err = new Error(message) + const stack = err.stack.split('\n') + err.stack = stack[0] + '\n' + range(10) + .map((__, i) => stack[1].replace(/^(\s+at\s).*/, `$1stack trace line ${i}`)) + .join('\n') + return err +} + +const loadLogFile = async (t, { buffer = [], mocks, testdir = {}, ...options } = {}) => { + const root = t.testdir(testdir) + const MockLogFile = t.mock('../../../lib/utils/log-file.js', mocks) + const logFile = new MockLogFile(Object.keys(options).length ? options : undefined) + buffer.forEach((b) => logFile.log(...b)) + await logFile.load({ dir: root, ...options }) + t.teardown(() => logFile.off()) + return { + root, + logFile, + LogFile, + readLogs: async () => { + const logDir = await fs.readdir(root) + const logFiles = logDir.map((f) => path.join(root, f)) + .filter((f) => _fs.existsSync(f)) + return Promise.all(logFiles.map(async (f) => { + const content = await fs.readFile(f, 'utf8') + const rawLogs = content.split(os.EOL) + return { + filename: f, + content, + rawLogs, + logs: rawLogs.filter(Boolean), + } + })) + }, + } +} + +t.test('init', async t => { + const maxLogsPerFile = 10 + const { root, logFile, readLogs } = await loadLogFile(t, { + maxLogsPerFile, + maxFilesPerProcess: 20, + buffer: [['error', 'buffered']], + }) + + for (const i of range(50)) { + logFile.log('error', `log ${i}`) + } + + // Ignored + logFile.log('pause') + logFile.log('resume') + logFile.log('pause') + + for (const i of range(50)) { + logFile.log('verb', `log ${i}`) + } + + logFile.off() + logFile.log('error', 'ignored') + + const logs = await readLogs() + t.equal(logs.length, 11, 'total log files') + t.ok(logs.slice(0, 10).every(f => f.logs.length === maxLogsPerFile), 'max logs per file') + t.ok(last(logs).logs.length, 1, 'last file has remaining logs') + t.ok(logs.every(f => last(f.rawLogs) === ''), 'all logs end with newline') + t.strictSame( + logFile.files, + logs.map((l) => path.resolve(root, l.filename)) + ) +}) + +t.test('max files per process', async t => { + const maxLogsPerFile = 10 + const maxFilesPerProcess = 5 + const { logFile, readLogs } = await loadLogFile(t, { + maxLogsPerFile, + maxFilesPerProcess, + }) + + for (const i of range(maxLogsPerFile * maxFilesPerProcess)) { + logFile.log('error', `log ${i}`) + } + + for (const i of range(5)) { + logFile.log('verbose', `log ${i}`) + } + + const logs = await readLogs() + t.equal(logs.length, maxFilesPerProcess, 'total log files') + t.equal(last(last(logs).logs), '49 error log 49') +}) + +t.test('stream error', async t => { + let times = 0 + const { logFile, readLogs } = await loadLogFile(t, { + maxLogsPerFile: 1, + maxFilesPerProcess: 99, + mocks: { + 'fs-minipass': { + WriteStreamSync: class { + constructor (...args) { + if (times >= 5) { + throw new Error('bad stream') + } + times++ + return new fsMiniPass.WriteStreamSync(...args) + } + }, + }, + }, + }) + + for (const i of range(10)) { + logFile.log('verbose', `log ${i}`) + } + + const logs = await readLogs() + t.equal(logs.length, 5, 'total log files') +}) + +t.test('initial stream error', async t => { + const { logFile, readLogs } = await loadLogFile(t, { + mocks: { + 'fs-minipass': { + WriteStreamSync: class { + constructor (...args) { + throw new Error('no stream') + } + }, + }, + }, + }) + + for (const i of range(10)) { + logFile.log('verbose', `log ${i}`) + } + + const logs = await readLogs() + t.equal(logs.length, 0, 'total log files') +}) + +t.test('turns off', async t => { + const { logFile, readLogs } = await loadLogFile(t) + + logFile.log('error', 'test') + logFile.off() + logFile.log('error', 'test2') + logFile.load() + + const logs = await readLogs() + t.equal(logs.length, 1) + t.equal(logs[0].logs[0], '0 error test') +}) + +t.test('cleans logs', async t => { + const logsMax = 5 + const { readLogs } = await loadLogFile(t, { + logsMax, + testdir: makeOldLogs(10), + }) + + const logs = await readLogs() + t.equal(logs.length, logsMax + 1) +}) + +t.test('doesnt clean current log by default', async t => { + const logsMax = 0 + const { readLogs, logFile } = await loadLogFile(t, { + logsMax, + testdir: makeOldLogs(10), + }) + + logFile.log('error', 'test') + + const logs = await readLogs() + t.equal(logs.length, 1) + t.match(last(logs).content, /\d+ error test/) +}) + +t.test('negative logs max', async t => { + const logsMax = -10 + const { readLogs, logFile } = await loadLogFile(t, { + logsMax, + testdir: makeOldLogs(10), + }) + + logFile.log('error', 'test') + + const logs = await readLogs() + t.equal(logs.length, 1) + t.match(last(logs).content, /\d+ error test/) +}) + +t.test('doesnt need to clean', async t => { + const logsMax = 20 + const oldLogs = 10 + const { readLogs } = await loadLogFile(t, { + logsMax, + testdir: makeOldLogs(oldLogs), + }) + + const logs = await readLogs() + t.equal(logs.length, oldLogs + 1) +}) + +t.test('glob error', async t => { + const { readLogs } = await loadLogFile(t, { + logsMax: 5, + mocks: { + glob: () => { + throw new Error('bad glob') + }, + }, + }) + + const logs = await readLogs() + t.equal(logs.length, 1) + t.match(last(logs).content, /error cleaning log files .* bad glob/) +}) + +t.test('rimraf error', async t => { + const logsMax = 5 + const oldLogs = 10 + let count = 0 + const { readLogs } = await loadLogFile(t, { + logsMax, + testdir: makeOldLogs(oldLogs), + mocks: { + rimraf: (...args) => { + if (count >= 3) { + throw new Error('bad rimraf') + } + count++ + return rimraf(...args) + }, + }, + }) + + const logs = await readLogs() + t.equal(logs.length, oldLogs - 3 + 1) + t.match(last(logs).content, /error removing log file .* bad rimraf/) +}) + +t.test('delete log file while open', async t => { + const { logFile, root, readLogs } = await loadLogFile(t) + + logFile.log('error', '', 'log 1') + const [log] = await readLogs(true) + t.match(log.content, /\d+ error log 1/) + + await fs.unlink(path.resolve(root, log.filename)) + + logFile.log('error', '', 'log 2') + const logs = await readLogs() + + // XXX: do some retry logic after error? + t.strictSame(logs, [], 'logs arent written after error') +}) + +t.test('snapshot', async t => { + const { logFile, readLogs } = await loadLogFile(t) + + logFile.log('error', '', 'no prefix') + logFile.log('error', 'prefix', 'with prefix') + logFile.log('error', 'prefix', 1, 2, 3) + + const nestedObj = { obj: { with: { many: { props: 1 } } } } + logFile.log('verbose', '', nestedObj) + logFile.log('verbose', '', JSON.stringify(nestedObj)) + logFile.log('verbose', '', JSON.stringify(nestedObj, null, 2)) + + const arr = ['test', 'with', 'an', 'array'] + logFile.log('verbose', '', arr) + logFile.log('verbose', '', JSON.stringify(arr)) + logFile.log('verbose', '', JSON.stringify(arr, null, 2)) + + const nestedArr = ['test', ['with', ['an', ['array']]]] + logFile.log('verbose', '', nestedArr) + logFile.log('verbose', '', JSON.stringify(nestedArr)) + logFile.log('verbose', '', JSON.stringify(nestedArr, null, 2)) + + // XXX: multiple errors are hard to parse visually + // the second error should start on a newline + logFile.log(...[ + 'error', + 'pre', + 'has', + 'many', + 'errors', + cleanErr('message'), + cleanErr('message2'), + ]) + + const err = new Error('message') + delete err.stack + logFile.log(...[ + 'error', + 'nostack', + err, + ]) + + const logs = await readLogs() + t.matchSnapshot(logs.map(l => l.content).join('\n')) +}) diff --git a/test/lib/utils/log-shim.js b/test/lib/utils/log-shim.js new file mode 100644 index 0000000000000..dee4efbaa4552 --- /dev/null +++ b/test/lib/utils/log-shim.js @@ -0,0 +1,100 @@ +const t = require('tap') + +const makeShim = (mocks) => t.mock('../../../lib/utils/log-shim.js', mocks) + +const loggers = [ + 'notice', + 'error', + 'warn', + 'info', + 'verbose', + 'http', + 'silly', + 'pause', + 'resume', +] + +t.test('has properties', (t) => { + const shim = makeShim() + + t.match(shim, { + level: String, + levels: {}, + gauge: {}, + stream: {}, + heading: undefined, + enableColor: Function, + disableColor: Function, + enableUnicode: Function, + disableUnicode: Function, + enableProgress: Function, + disableProgress: Function, + ...loggers.reduce((acc, l) => { + acc[l] = Function + return acc + }, {}), + }) + + t.match(Object.keys(shim).sort(), [ + 'level', + 'heading', + 'levels', + 'gauge', + 'stream', + 'tracker', + 'useColor', + 'enableColor', + 'disableColor', + 'enableUnicode', + 'disableUnicode', + 'enableProgress', + 'disableProgress', + 'progressEnabled', + 'clearProgress', + 'showProgress', + 'newItem', + 'newGroup', + ...loggers, + ].sort()) + + t.end() +}) + +t.test('works with npmlog/proclog proxy', t => { + const procLog = { silly: () => 'SILLY' } + const npmlog = { level: 'woo', enableColor: () => true } + const shim = makeShim({ npmlog, 'proc-log': procLog }) + + t.equal(shim.level, 'woo', 'can get a property') + + npmlog.level = 'hey' + t.strictSame( + [shim.level, npmlog.level], + ['hey', 'hey'], + 'can get a property after update on npmlog' + ) + + shim.level = 'test' + t.strictSame( + [shim.level, npmlog.level], + ['test', 'test'], + 'can get a property after update on shim' + ) + + t.ok(shim.enableColor(), 'can call method on shim to call npmlog') + t.equal(shim.silly(), 'SILLY', 'can call method on proclog') + t.notOk(shim.LEVELS, 'only includes levels from npmlog') + t.throws(() => shim.gauge = 100, 'cant set getters properies') + + t.end() +}) + +t.test('works with npmlog/proclog proxy', t => { + const shim = makeShim() + + loggers.forEach((k) => { + t.doesNotThrow(() => shim[k]('test')) + }) + + t.end() +}) diff --git a/test/lib/utils/npm-usage.js b/test/lib/utils/npm-usage.js index 77254a80d017d..035d4bbb21ef7 100644 --- a/test/lib/utils/npm-usage.js +++ b/test/lib/utils/npm-usage.js @@ -1,10 +1,8 @@ const t = require('tap') -const { real: mockNpm } = require('../../fixtures/mock-npm.js') -const { Npm } = mockNpm(t) -const npm = new Npm() +const { load: loadMockNpm } = require('../../fixtures/mock-npm.js') t.test('usage', async t => { - await npm.load() + const { npm } = await loadMockNpm(t) t.afterEach(() => { npm.config.set('viewer', null) npm.config.set('long', false) diff --git a/test/lib/utils/proc-log-listener.js b/test/lib/utils/proc-log-listener.js deleted file mode 100644 index d580defa8a98f..0000000000000 --- a/test/lib/utils/proc-log-listener.js +++ /dev/null @@ -1,41 +0,0 @@ -const t = require('tap') -const { inspect } = require('util') - -const logs = [] -const npmlog = { - warn: (...args) => logs.push(['warn', ...args]), - verbose: (...args) => logs.push(['verbose', ...args]), -} - -t.mock('../../../lib/utils/proc-log-listener.js', { - npmlog, -})() - -process.emit('log', 'warn', 'hello', 'i am a warning') -t.strictSame(logs, [['warn', 'hello', 'i am a warning']]) -logs.length = 0 - -const nopeError = new Error('nope') -npmlog.warn = () => { - throw nopeError -} - -process.emit('log', 'warn', 'fail') -t.strictSame(logs, [[ - 'verbose', - `attempt to log ${inspect(['warn', 'fail'])} crashed`, - nopeError, -]]) -logs.length = 0 - -npmlog.verbose = () => { - throw nopeError -} -const consoleErrors = [] -console.error = (...args) => consoleErrors.push(args) -process.emit('log', 'warn', 'fail2') -t.strictSame(logs, []) -t.strictSame(consoleErrors, [[ - `attempt to log ${inspect(['warn', 'fail2'])} crashed`, - nopeError, -]]) diff --git a/test/lib/utils/pulse-till-done.js b/test/lib/utils/pulse-till-done.js index acbf66396a702..9f7a94614d3bb 100644 --- a/test/lib/utils/pulse-till-done.js +++ b/test/lib/utils/pulse-till-done.js @@ -1,18 +1,17 @@ const t = require('tap') let pulseStarted = null -const npmlog = { - gauge: { - pulse: () => { - if (pulseStarted) { - pulseStarted() - } - }, - }, -} const pulseTillDone = t.mock('../../../lib/utils/pulse-till-done.js', { - npmlog, + npmlog: { + gauge: { + pulse: () => { + if (pulseStarted) { + pulseStarted() + } + }, + }, + }, }) t.test('pulses (with promise)', async (t) => { diff --git a/test/lib/utils/read-user-info.js b/test/lib/utils/read-user-info.js index 35101f1d7029a..be805a2a87c6a 100644 --- a/test/lib/utils/read-user-info.js +++ b/test/lib/utils/read-user-info.js @@ -7,11 +7,6 @@ const read = (opts, cb) => { return cb(null, readResult) } -const npmlog = { - clearProgress: () => {}, - showProgress: () => {}, -} - const npmUserValidate = { username: (username) => { if (username === 'invalid') { @@ -29,12 +24,23 @@ const npmUserValidate = { }, } +let logMsg = null const readUserInfo = t.mock('../../../lib/utils/read-user-info.js', { read, - npmlog, + npmlog: { + clearProgress: () => {}, + showProgress: () => {}, + }, + 'proc-log': { + warn: (msg) => logMsg = msg, + }, 'npm-user-validate': npmUserValidate, }) +t.beforeEach(() => { + logMsg = null +}) + t.test('otp', async (t) => { readResult = '1234' t.teardown(() => { @@ -75,11 +81,7 @@ t.test('username - invalid warns and retries', async (t) => { readOpts = null }) - let logMsg - const log = { - warn: (msg) => logMsg = msg, - } - const pResult = readUserInfo.username(null, null, { log }) + const pResult = readUserInfo.username(null, null) // have to swap it to a valid username after execution starts // or it will loop forever readResult = 'valid' @@ -105,11 +107,7 @@ t.test('email - invalid warns and retries', async (t) => { readOpts = null }) - let logMsg - const log = { - warn: (msg) => logMsg = msg, - } - const pResult = readUserInfo.email(null, null, { log }) + const pResult = readUserInfo.email(null, null) readResult = 'foo@bar.baz' const result = await pResult t.equal(result, 'foo@bar.baz', 'received the email') diff --git a/test/lib/utils/reify-output.js b/test/lib/utils/reify-output.js index 9a1bffb4033f9..4e9ed7133c18c 100644 --- a/test/lib/utils/reify-output.js +++ b/test/lib/utils/reify-output.js @@ -1,7 +1,9 @@ const t = require('tap') +const log = require('../../../lib/utils/log-shim') -const log = require('npmlog') -log.level = 'warn' +const _level = log.level +t.beforeEach(() => log.level = 'warn') +t.teardown(() => log.level = _level) t.cleanSnapshot = str => str.replace(/in [0-9]+m?s/g, 'in {TIME}') @@ -237,7 +239,6 @@ t.test('showing and not showing audit report', async t => { npm.output = out => { t.fail('should not get output when silent', { actual: out }) } - t.teardown(() => log.level = 'warn') log.level = 'silent' reifyOutput(npm, { actualTree: { inventory: { size: 999 }, children: [] }, diff --git a/test/lib/utils/setup-log.js b/test/lib/utils/setup-log.js deleted file mode 100644 index 7f907bc7e4148..0000000000000 --- a/test/lib/utils/setup-log.js +++ /dev/null @@ -1,296 +0,0 @@ -const t = require('tap') - -const settings = { - level: 'warn', -} -t.afterEach(() => { - Object.keys(settings).forEach(k => { - delete settings[k] - }) -}) - -const WARN_CALLED = [] -const npmlog = { - warn: (...args) => { - WARN_CALLED.push(args) - }, - levels: { - silly: -Infinity, - verbose: 1000, - info: 2000, - timing: 2500, - http: 3000, - notice: 3500, - warn: 4000, - error: 5000, - silent: Infinity, - }, - settings, - enableColor: () => { - settings.color = true - }, - disableColor: () => { - settings.color = false - }, - enableUnicode: () => { - settings.unicode = true - }, - disableUnicode: () => { - settings.unicode = false - }, - enableProgress: () => { - settings.progress = true - }, - disableProgress: () => { - settings.progress = false - }, - get heading () { - return settings.heading - }, - set heading (h) { - settings.heading = h - }, - get level () { - return settings.level - }, - set level (l) { - settings.level = l - }, -} - -const EXPLAIN_CALLED = [] -const setupLog = t.mock('../../../lib/utils/setup-log.js', { - '../../../lib/utils/explain-eresolve.js': { - explain: (...args) => { - EXPLAIN_CALLED.push(args) - return 'explanation' - }, - }, - npmlog, -}) - -const config = obj => ({ - get (k) { - return obj[k] - }, - set (k, v) { - obj[k] = v - }, -}) - -t.test('setup with color=always and unicode', t => { - npmlog.warn('ERESOLVE', 'hello', { some: 'object' }) - t.strictSame(EXPLAIN_CALLED, [], 'log.warn() not patched yet') - t.strictSame(WARN_CALLED, [['ERESOLVE', 'hello', { some: 'object' }]]) - WARN_CALLED.length = 0 - - setupLog(config({ - loglevel: 'warn', - color: 'always', - unicode: true, - progress: false, - })) - - npmlog.warn('ERESOLVE', 'hello', { some: { other: 'object' } }) - t.strictSame(EXPLAIN_CALLED, [[{ some: { other: 'object' } }, true, 2]], - 'log.warn(ERESOLVE) patched to call explainEresolve()') - t.strictSame(WARN_CALLED, [ - ['ERESOLVE', 'hello'], - ['', 'explanation'], - ], 'warn the explanation') - EXPLAIN_CALLED.length = 0 - WARN_CALLED.length = 0 - npmlog.warn('some', 'other', 'thing') - t.strictSame(EXPLAIN_CALLED, [], 'do not try to explain other things') - t.strictSame(WARN_CALLED, [['some', 'other', 'thing']], 'warnings passed through') - - t.strictSame(settings, { - level: 'warn', - color: true, - unicode: true, - progress: false, - heading: 'npm', - }) - - t.end() -}) - -t.test('setup with color=true, no unicode, and non-TTY terminal', t => { - const { isTTY: stderrIsTTY } = process.stderr - const { isTTY: stdoutIsTTY } = process.stdout - t.teardown(() => { - process.stderr.isTTY = stderrIsTTY - process.stdout.isTTY = stdoutIsTTY - }) - process.stderr.isTTY = false - process.stdout.isTTY = false - - setupLog(config({ - loglevel: 'warn', - color: false, - progress: false, - heading: 'asdf', - })) - - t.strictSame(settings, { - level: 'warn', - color: false, - unicode: false, - progress: false, - heading: 'asdf', - }) - - t.end() -}) - -t.test('setup with color=true, no unicode, and dumb TTY terminal', t => { - const { isTTY: stderrIsTTY } = process.stderr - const { isTTY: stdoutIsTTY } = process.stdout - const { TERM } = process.env - t.teardown(() => { - process.stderr.isTTY = stderrIsTTY - process.stdout.isTTY = stdoutIsTTY - process.env.TERM = TERM - }) - process.stderr.isTTY = true - process.stdout.isTTY = true - process.env.TERM = 'dumb' - - setupLog(config({ - loglevel: 'warn', - color: true, - progress: false, - heading: 'asdf', - })) - - t.strictSame(settings, { - level: 'warn', - color: true, - unicode: false, - progress: false, - heading: 'asdf', - }) - - t.end() -}) - -t.test('setup with color=true, no unicode, and non-dumb TTY terminal', t => { - const { isTTY: stderrIsTTY } = process.stderr - const { isTTY: stdoutIsTTY } = process.stdout - const { TERM } = process.env - t.teardown(() => { - process.stderr.isTTY = stderrIsTTY - process.stdout.isTTY = stdoutIsTTY - process.env.TERM = TERM - }) - process.stderr.isTTY = true - process.stdout.isTTY = true - process.env.TERM = 'totes not dum' - - setupLog(config({ - loglevel: 'warn', - color: true, - progress: true, - heading: 'asdf', - })) - - t.strictSame(settings, { - level: 'warn', - color: true, - unicode: false, - progress: true, - heading: 'asdf', - }) - - t.end() -}) - -t.test('setup with non-TTY stdout, TTY stderr', t => { - const { isTTY: stderrIsTTY } = process.stderr - const { isTTY: stdoutIsTTY } = process.stdout - const { TERM } = process.env - t.teardown(() => { - process.stderr.isTTY = stderrIsTTY - process.stdout.isTTY = stdoutIsTTY - process.env.TERM = TERM - }) - process.stderr.isTTY = true - process.stdout.isTTY = false - process.env.TERM = 'definitely not a dummy' - - setupLog(config({ - loglevel: 'warn', - color: true, - progress: true, - heading: 'asdf', - })) - - t.strictSame(settings, { - level: 'warn', - color: true, - unicode: false, - progress: true, - heading: 'asdf', - }) - - t.end() -}) - -t.test('setup with TTY stdout, non-TTY stderr', t => { - const { isTTY: stderrIsTTY } = process.stderr - const { isTTY: stdoutIsTTY } = process.stdout - const { TERM } = process.env - t.teardown(() => { - process.stderr.isTTY = stderrIsTTY - process.stdout.isTTY = stdoutIsTTY - process.env.TERM = TERM - }) - process.stderr.isTTY = false - process.stdout.isTTY = true - - setupLog(config({ - loglevel: 'warn', - color: true, - progress: true, - heading: 'asdf', - })) - - t.strictSame(settings, { - level: 'warn', - color: false, - unicode: false, - progress: false, - heading: 'asdf', - }) - - t.end() -}) - -t.test('set loglevel to timing', t => { - setupLog(config({ - timing: true, - loglevel: 'notice', - })) - t.equal(settings.level, 'timing') - t.end() -}) - -t.test('silent has no logging', t => { - const { isTTY: stderrIsTTY } = process.stderr - const { isTTY: stdoutIsTTY } = process.stdout - const { TERM } = process.env - t.teardown(() => { - process.stderr.isTTY = stderrIsTTY - process.stdout.isTTY = stdoutIsTTY - process.env.TERM = TERM - }) - process.stderr.isTTY = true - process.stdout.isTTY = true - process.env.TERM = 'totes not dum' - - setupLog(config({ - loglevel: 'silent', - })) - t.equal(settings.progress, false, 'progress disabled when silent') - t.end() -}) diff --git a/test/lib/utils/tar.js b/test/lib/utils/tar.js index 19d94916945db..adc5cb364997f 100644 --- a/test/lib/utils/tar.js +++ b/test/lib/utils/tar.js @@ -2,18 +2,20 @@ const t = require('tap') const pack = require('libnpmpack') const ssri = require('ssri') -const { logTar, getContents } = require('../../../lib/utils/tar.js') +const { getContents } = require('../../../lib/utils/tar.js') -const printLogs = (tarball, unicode) => { +const mockTar = ({ notice }) => t.mock('../../../lib/utils/tar.js', { + 'proc-log': { + notice, + }, +}) + +const printLogs = (tarball, options) => { const logs = [] - logTar(tarball, { - log: { - notice: (...args) => { - args.map(el => logs.push(el)) - }, - }, - unicode, + const { logTar } = mockTar({ + notice: (...args) => args.map(el => logs.push(el)), }) + logTar(tarball, options) return logs.join('\n') } @@ -41,16 +43,14 @@ t.test('should log tarball contents', async (t) => { version: '1.0.0', }, tarball) - t.matchSnapshot(printLogs(tarballContents, false)) + t.matchSnapshot(printLogs(tarballContents)) }) t.test('should log tarball contents with unicode', async (t) => { - const { logTar } = t.mock('../../../lib/utils/tar.js', { - npmlog: { - notice: (str) => { - t.ok(true, 'defaults to npmlog') - return str - }, + const { logTar } = mockTar({ + notice: (str) => { + t.ok(true, 'defaults to proc-log') + return str }, }) @@ -64,26 +64,6 @@ t.test('should log tarball contents with unicode', async (t) => { t.end() }) -t.test('should default to npmlog', async (t) => { - const { logTar } = t.mock('../../../lib/utils/tar.js', { - npmlog: { - notice: (str) => { - t.ok(true, 'defaults to npmlog') - return str - }, - }, - }) - - logTar({ - files: [], - bundled: [], - size: 0, - unpackedSize: 0, - integrity: '', - }) - t.end() -}) - t.test('should getContents of a tarball', async (t) => { const testDir = t.testdir({ 'package.json': JSON.stringify({ diff --git a/test/lib/utils/timers.js b/test/lib/utils/timers.js new file mode 100644 index 0000000000000..6127f346b179e --- /dev/null +++ b/test/lib/utils/timers.js @@ -0,0 +1,82 @@ +const t = require('tap') +const { resolve } = require('path') +const fs = require('graceful-fs') +const mockLogs = require('../../fixtures/mock-logs') + +const mockTimers = (t, options) => { + const { logs, logMocks } = mockLogs() + const Timers = t.mock('../../../lib/utils/timers', { + ...logMocks, + }) + const timers = new Timers(options) + t.teardown(() => timers.off()) + return { timers, logs } +} + +t.test('getters', async (t) => { + const { timers } = mockTimers(t) + t.match(timers.unfinished, new Map()) + t.match(timers.finished, {}) +}) + +t.test('listens/stops on process', async (t) => { + const { timers } = mockTimers(t) + process.emit('time', 'foo') + process.emit('time', 'bar') + process.emit('timeEnd', 'bar') + t.match(timers.unfinished, new Map([['foo', Number]])) + t.match(timers.finished, { bar: Number }) + timers.off() + process.emit('time', 'baz') + t.notOk(timers.unfinished.get('baz')) +}) + +t.test('initial timer', async (t) => { + const { timers } = mockTimers(t, { start: 'foo' }) + process.emit('timeEnd', 'foo') + t.match(timers.finished, { foo: Number }) +}) + +t.test('initial listener', async (t) => { + const events = [] + const listener = (...args) => events.push(args) + const { timers } = mockTimers(t, { listener }) + process.emit('time', 'foo') + process.emit('time', 'bar') + process.emit('timeEnd', 'bar') + timers.off(listener) + process.emit('timeEnd', 'foo') + t.equal(events.length, 1) + t.match(events, [['bar', Number]]) +}) + +t.test('finish unstarted timer', async (t) => { + const { logs } = mockTimers(t) + process.emit('timeEnd', 'foo') + t.match(logs.silly, [['timing', /^Tried to end timer/, 'foo']]) +}) + +t.test('writes file', async (t) => { + const { timers } = mockTimers(t) + const dir = t.testdir() + process.emit('time', 'foo') + process.emit('timeEnd', 'foo') + timers.load({ dir }) + timers.writeFile({ some: 'data' }) + const data = JSON.parse(fs.readFileSync(resolve(dir, '_timing.json'))) + t.match(data, { + some: 'data', + foo: Number, + unfinished: { + npm: [Number, Number], + }, + }) +}) + +t.test('fails to write file', async (t) => { + const { logs, timers } = mockTimers(t) + timers.writeFile() + t.match(logs.warn, [ + ['timing', 'could not write timing file', Error], + ]) +}) diff --git a/test/lib/utils/unsupported.js b/test/lib/utils/unsupported.js index 4d806cefc4e52..2703044a227d3 100644 --- a/test/lib/utils/unsupported.js +++ b/test/lib/utils/unsupported.js @@ -1,5 +1,6 @@ const t = require('tap') const unsupported = require('../../../lib/utils/unsupported.js') +const mockGlobals = require('../../fixtures/mock-globals.js') const versions = [ // broken unsupported @@ -55,42 +56,30 @@ t.test('checkForBrokenNode', t => { // run it once to not fail unsupported.checkForBrokenNode() - const { exit } = process - const { error } = console - const versionPropDesc = Object.getOwnPropertyDescriptor(process, 'version') - - t.teardown(() => { - process.exit = exit - Object.defineProperty(process, 'version', versionPropDesc) - console.error = error - }) - - // then make it a thing that fails - process.exit = code => { - t.equal(code, 1) - t.strictSame(logs, expectLogs) - t.end() - } - Object.defineProperty(process, 'version', { value: '1.2.3', configurable: true }) const logs = [] const expectLogs = [ 'ERROR: npm is known not to run on Node.js 1.2.3', "You'll need to upgrade to a newer Node.js version in order to use this", 'version of npm. You can find the latest version at https://nodejs.org/', ] - console.error = msg => logs.push(msg) + + // then make it a thing that fails + mockGlobals(t, { + 'console.error': msg => logs.push(msg), + 'process.version': '1.2.3', + 'process.exit': (code) => { + t.equal(code, 1) + t.strictSame(logs, expectLogs) + t.end() + }, + }) + unsupported.checkForBrokenNode() }) t.test('checkForUnsupportedNode', t => { - const npmlog = require('npmlog') - const { warn } = npmlog - const versionPropDesc = Object.getOwnPropertyDescriptor(process, 'version') - - t.teardown(() => { - Object.defineProperty(process, 'version', versionPropDesc) - npmlog.warn = warn - }) + // run it once to not fail or warn + unsupported.checkForUnsupportedNode() const logs = [] const expectLogs = [ @@ -99,14 +88,15 @@ t.test('checkForUnsupportedNode', t => { "can't make any promises that npm will work with this version.", 'You can find the latest version at https://nodejs.org/', ] - npmlog.warn = (section, msg) => logs.push(msg) - - // run it once to not fail or warn - unsupported.checkForUnsupportedNode() // then make it a thing that fails - Object.defineProperty(process, 'version', { value: '8.0.0' }) + mockGlobals(t, { + 'console.error': msg => logs.push(msg), + 'process.version': '8.0.0', + }) + unsupported.checkForUnsupportedNode() + t.strictSame(logs, expectLogs) t.end() }) diff --git a/test/lib/utils/update-notifier.js b/test/lib/utils/update-notifier.js index 78ff93825e489..a7a800c602fd6 100644 --- a/test/lib/utils/update-notifier.js +++ b/test/lib/utils/update-notifier.js @@ -36,18 +36,13 @@ const pacote = { }, } -const npm = { +const defaultNpm = { flatOptions, - log: { useColor: () => true }, version: CURRENT_VERSION, config: { get: k => k !== 'global' }, command: 'view', argv: ['npm'], } -const npmNoColor = { - ...npm, - log: { useColor: () => false }, -} const { basename } = require('path') @@ -80,12 +75,6 @@ const fs = { }, } -const updateNotifier = t.mock('../../../lib/utils/update-notifier.js', { - '@npmcli/ci-detect': () => ciMock, - pacote, - fs, -}) - t.afterEach(() => { MANIFEST_REQUEST.length = 0 STAT_ERROR = null @@ -94,16 +83,21 @@ t.afterEach(() => { WRITE_ERROR = null }) -const runUpdateNotifier = async npm => { - await updateNotifier(npm) - return npm.updateNotification +const runUpdateNotifier = async ({ color = true, ...npmOptions } = {}) => { + const _npm = { ...defaultNpm, ...npmOptions } + await t.mock('../../../lib/utils/update-notifier.js', { + '@npmcli/ci-detect': () => ciMock, + pacote, + fs, + npmlog: { useColor: () => color }, + })(_npm) + return _npm.updateNotification } t.test('situations in which we do not notify', t => { t.test('nothing to do if notifier disabled', async t => { t.equal( await runUpdateNotifier({ - ...npm, config: { get: k => k !== 'update-notifier' }, }), null @@ -114,7 +108,6 @@ t.test('situations in which we do not notify', t => { t.test('do not suggest update if already updating', async t => { t.equal( await runUpdateNotifier({ - ...npm, flatOptions: { ...flatOptions, global: true }, command: 'install', argv: ['npm'], @@ -127,7 +120,6 @@ t.test('situations in which we do not notify', t => { t.test('do not suggest update if already updating with spec', async t => { t.equal( await runUpdateNotifier({ - ...npm, flatOptions: { ...flatOptions, global: true }, command: 'install', argv: ['npm@latest'], @@ -138,31 +130,31 @@ t.test('situations in which we do not notify', t => { }) t.test('do not update if same as latest', async t => { - t.equal(await runUpdateNotifier(npm), null) + t.equal(await runUpdateNotifier(), null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('check if stat errors (here for coverage)', async t => { STAT_ERROR = new Error('blorg') - t.equal(await runUpdateNotifier(npm), null) + t.equal(await runUpdateNotifier(), null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('ok if write errors (here for coverage)', async t => { WRITE_ERROR = new Error('grolb') - t.equal(await runUpdateNotifier(npm), null) + t.equal(await runUpdateNotifier(), null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('ignore pacote failures (here for coverage)', async t => { PACOTE_ERROR = new Error('pah-KO-tchay') - t.equal(await runUpdateNotifier(npm), null) + t.equal(await runUpdateNotifier(), null) t.strictSame(MANIFEST_REQUEST, ['npm@latest'], 'requested latest version') }) t.test('do not update if newer than latest, but same as next', async t => { - t.equal(await runUpdateNotifier({ ...npm, version: NEXT_VERSION }), null) + t.equal(await runUpdateNotifier({ version: NEXT_VERSION }), null) const reqs = ['npm@latest', `npm@^${NEXT_VERSION}`] t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') }) t.test('do not update if on the latest beta', async t => { - t.equal(await runUpdateNotifier({ ...npm, version: CURRENT_BETA }), null) + t.equal(await runUpdateNotifier({ version: CURRENT_BETA }), null) const reqs = [`npm@^${CURRENT_BETA}`] t.strictSame(MANIFEST_REQUEST, reqs, 'requested latest and next versions') }) @@ -172,21 +164,21 @@ t.test('situations in which we do not notify', t => { ciMock = null }) ciMock = 'something' - t.equal(await runUpdateNotifier(npm), null) + t.equal(await runUpdateNotifier(), null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('only check weekly for GA releases', async t => { // One week (plus five minutes to account for test environment fuzziness) STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 * 7 + 1000 * 60 * 5 - t.equal(await runUpdateNotifier(npm), null) + t.equal(await runUpdateNotifier(), null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) t.test('only check daily for betas', async t => { // One day (plus five minutes to account for test environment fuzziness) STAT_MTIME = Date.now() - 1000 * 60 * 60 * 24 + 1000 * 60 * 5 - t.equal(await runUpdateNotifier({ ...npm, version: HAVE_BETA }), null) + t.equal(await runUpdateNotifier({ version: HAVE_BETA }), null) t.strictSame(MANIFEST_REQUEST, [], 'no requests for manifests') }) @@ -196,9 +188,9 @@ t.test('situations in which we do not notify', t => { t.test('notification situations', t => { t.test('new beta available', async t => { const version = HAVE_BETA - t.matchSnapshot(await runUpdateNotifier({ ...npm, version }), 'color') + t.matchSnapshot(await runUpdateNotifier({ version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ ...npmNoColor, version }), + await runUpdateNotifier({ version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, [`npm@^${version}`, `npm@^${version}`]) @@ -206,9 +198,9 @@ t.test('notification situations', t => { t.test('patch to next version', async t => { const version = NEXT_PATCH - t.matchSnapshot(await runUpdateNotifier({ ...npm, version }), 'color') + t.matchSnapshot(await runUpdateNotifier({ version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ ...npmNoColor, version }), + await runUpdateNotifier({ version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, [ @@ -221,9 +213,9 @@ t.test('notification situations', t => { t.test('minor to next version', async t => { const version = NEXT_MINOR - t.matchSnapshot(await runUpdateNotifier({ ...npm, version }), 'color') + t.matchSnapshot(await runUpdateNotifier({ version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ ...npmNoColor, version }), + await runUpdateNotifier({ version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, [ @@ -236,9 +228,9 @@ t.test('notification situations', t => { t.test('patch to current', async t => { const version = CURRENT_PATCH - t.matchSnapshot(await runUpdateNotifier({ ...npm, version }), 'color') + t.matchSnapshot(await runUpdateNotifier({ version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ ...npmNoColor, version }), + await runUpdateNotifier({ version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest']) @@ -246,9 +238,9 @@ t.test('notification situations', t => { t.test('minor to current', async t => { const version = CURRENT_MINOR - t.matchSnapshot(await runUpdateNotifier({ ...npm, version }), 'color') + t.matchSnapshot(await runUpdateNotifier({ version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ ...npmNoColor, version }), + await runUpdateNotifier({ version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest']) @@ -256,9 +248,9 @@ t.test('notification situations', t => { t.test('major to current', async t => { const version = CURRENT_MAJOR - t.matchSnapshot(await runUpdateNotifier({ ...npm, version }), 'color') + t.matchSnapshot(await runUpdateNotifier({ version }), 'color') t.matchSnapshot( - await runUpdateNotifier({ ...npmNoColor, version }), + await runUpdateNotifier({ version, color: false }), 'no color' ) t.strictSame(MANIFEST_REQUEST, ['npm@latest', 'npm@latest'])