diff options
145 files changed, 4631 insertions, 3286 deletions
diff --git a/.eslintrc.json b/.eslintrc.json index b39431d2c..2968a2ea3 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 2da82e361..7929ccc64 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 6fcfc30e5..795eb8972 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 9dcd9d04d..3d0c32d4b 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 6cd6d3001..1cf70fffb 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 8f5ae0cc5..bb700d45a 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 8ca8188cc..5dfd1eb91 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 b1c045bbf..ecb34cb89 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 e928a01d1..2c2f8da86 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 0cdcd576f..eb1d570c6 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 e1eafbe3b..cc4b119d0 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 3134f502e..d737a58dc 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 fa79b293c..bf2dffe91 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 9aba24205..19cd73564 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 6b8878b6f..47a522eb6 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 515ac910f..61a6d9659 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) diff --git a/lib/commands/explore.js b/lib/commands/explore.js index f94fff01c..90e6af69f 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 81c6d9a1b..47a51c33a 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 eaca2716e..7e8a8f7a5 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 @@ -125,8 +125,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 +147,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 02ccb5724..a92a5edc5 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 8755af6f6..e8e2c6b34 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 e17b2b879..4e6bab985 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 8f0b1f1ef..c027ad646 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 d84dde86e..0719fa3b8 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 a049d2412..993e029d4 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 0939013cc..e1102696e 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 403575e02..5831df628 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 88ddcae7b..ad538668b 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 cc68e8565..8ac4178f2 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 37140c8c5..cd877e0b3 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 ff533ebbd..bdeeffe81 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 58fd2726d..7c73ff01b 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 dfb3c8e38..05e3f6d27 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 1bbd25efd..ec1160589 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 1260655d0..f45ec846d 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 db2374203..df80f1afe 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 dba45e127..b40c59bda 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 3636dc58a..578890025 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 4bb74990b..a8bbc4c96 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 105ebc16d..4f7464ddd 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 0096e0ac5..4d22b531a 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, + progress: this.flatOptions.progress, + timing: this.config.get('timing'), + loglevel: this.config.get('loglevel'), + unicode: this.config.get('unicode'), + 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 b4ab26fd0..7feccc739 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 8fb0fa155..000000000 --- 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 e94136286..181406918 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 }, }) @@ -1533,6 +1536,10 @@ define('progress', { Set to \`false\` to suppress the progress bar. `, + flatten (key, obj, flatOptions) { + flatOptions.progress = !obj.progress ? false + : !!process.stderr.isTTY && process.env.TERM !== 'dumb' + }, }) define('proxy', { diff --git a/lib/utils/deref-command.js b/lib/utils/deref-command.js index dd89fb5a4..0a3c8c90b 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 000000000..aae51e880 --- /dev/null +++ b/lib/utils/display.js @@ -0,0 +1,119 @@ +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() + } + + // if it's more than error, don't show progress + const silent = log.levels[log.level] > log.levels.error + if (progress && !silent) { + 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 48ad4676f..4d584346d 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 5b2811468..324346624 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('', ' <https://github.com/npm/cli/issues>') - // 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('', ' <https://github.com/npm/cli/issues>') + 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 000000000..b37fd23e0 --- /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 000000000..9d5a36d96 --- /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 2cfe94ecb..000000000 --- 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 a88b8aacd..222941414 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 993aa886f..ac24396c6 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 7741b7220..b4114c1b2 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 05ca38c82..000000000 --- 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 26e7a98df..2f2773c6d 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 000000000..acff29eb0 --- /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 5f6a341a8..75aad5e78 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 2b45d54c8..44b6a5433 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 e23e50c51..39eaa45e4 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 000000000..481b5696d --- /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 646d14988..06a1c95ed 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 118ffe623..9c0cf0142 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 814f6de7c..da7a89bae 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 a0d579577..ddc80a935 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 10d38cb3f..72d09b44e 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: [1m[32mlatest[39m[22m: 1.0.0 -published [33myesterday[39m +published {TIME} ago[39m ` 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: [1m[32mlatest[39m[22m: 1.0.0 -published [33myesterday[39m +published {TIME} ago[39m ` 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: [1m[32mlatest[39m[22m: 1.0.0 -published [33myesterday[39m +published {TIME} ago[39m ` 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: [1m[32mlatest[39m[22m: 1.0.0 -published [33myesterday[39m +published {TIME} ago[39m [4m[1m[32mblue[39m@[32m1.0.1[39m[22m[24m | [1m[31mProprietary[39m[22m | deps: [32mnone[39m | versions: [33m2[39m 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 e4efb0eb9..3b82e3c05 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" ), ], ], @@ -527,6 +532,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" ), ], ], @@ -554,6 +563,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" ), ], ], @@ -581,6 +594,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 eb383c104..523aabca2 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 000000000..ecce9eafc --- /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/fixtures/clean-snapshot.js b/test/fixtures/clean-snapshot.js new file mode 100644 index 000000000..037155eea --- /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 000000000..29da2a48b --- /dev/null +++ b/test/fixtures/mock-globals.js @@ -0,0 +1,210 @@ +// An initial implementation for a feature that will hopefully exist in tap +// https://github.com/tapjs/node-tap/issues/789 +// This file is only used in tests but it is still tested itself. +// Hopefully it can be removed for a feature in tap in the future + +const sep = '.' +const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k) +const opd = (o, k) => Object.getOwnPropertyDescriptor(o, k) +const po = (o) => Object.getPrototypeOf(o) +const pojo = (o) => Object.prototype.toString.call(o) === '[object Object]' +const last = (arr) => arr[arr.length - 1] +const splitLast = (str) => str.split(new RegExp(`\\${sep}(?=[^${sep}]+$)`)) +const dupes = (arr) => arr.filter((k, i) => arr.indexOf(k) !== i) +const dupesStartsWith = (arr) => arr.filter((k1) => arr.some((k2) => k2.startsWith(k1 + sep))) + +// 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, key, childKey = '') => { + if (has(obj, key)) { + return childKey ? get(obj[key], childKey) : obj[key] + } else if (key.includes(sep)) { + const [parentKey, prefix] = splitLast(key) + return get( + obj, + parentKey, + prefix + (childKey && sep + childKey) + ) + } +} + +// Map an object to an array of nested keys separated by dots +// { a: 1, b: { c: 2, d: [1] } } => ['a', 'b.c', 'b.d'] +const getKeys = (values, p = '', acc = []) => + Object.entries(values).reduce((memo, [k, value]) => { + const key = p ? [p, k].join(sep) : k + return pojo(value) ? getKeys(value, key, memo) : memo.concat(key) + }, acc) + +// Walk prototype chain to get first available descriptor. This is necessary +// to get the current property descriptor for things like `process.on`. +// Since `opd(process, 'on') === undefined` but if you +// walk up the prototype chain you get the original descriptor +// `opd(po(po(process)), 'on') === { value, ... }` +const protoDescriptor = (obj, key) => { + let descriptor + // i always wanted to assign variables in a while loop's condition + // i thought it would feel better than this + while (!(descriptor = opd(obj, key))) { + if (!(obj = po(obj))) { + break + } + } + return descriptor +} + +// Path can be different cases across platform so get the original case +// of the path before anything is changed +// XXX: other special cases to handle? +const specialCaseKeys = (() => { + const originalKeys = { + PATH: process.env.PATH ? 'PATH' : process.env.Path ? 'Path' : 'path', + } + return (key) => { + switch (key.toLowerCase()) { + case 'process.env.path': + return originalKeys.PATH + } + } +})() + +const _setGlobal = Symbol('setGlobal') +const _nextDescriptor = Symbol('nextDescriptor') + +class DescriptorStack { + #stack = [] + #global = null + #valueKey = null + #defaultDescriptor = { configurable: true, writable: true, enumerable: true } + #delete = () => ({ DELETE: true }) + #isDelete = (o) => o && o.DELETE === true + + constructor (key) { + const keys = splitLast(key) + this.#global = keys.length === 1 ? global : get(global, keys[0]) + this.#valueKey = specialCaseKeys(key) || last(keys) + // If the global object doesnt return a descriptor for the key + // then we mark it for deletion on teardown + this.#stack = [ + protoDescriptor(this.#global, this.#valueKey) || this.#delete(), + ] + } + + add (value) { + // This must be a unique object so we can find it later via indexOf + // That's why delete/nextDescriptor create new objects + const nextDescriptor = this[_nextDescriptor](value) + this.#stack.push(this[_setGlobal](nextDescriptor)) + + return () => { + const index = this.#stack.indexOf(nextDescriptor) + // If the stack doesnt contain the descriptor anymore + // than do nothing. This keeps the reset function indempotent + if (index > -1) { + // Resetting removes a descriptor from the stack + this.#stack.splice(index, 1) + // But we always reset to what is now the most recent in case + // resets are being called manually out of order + this[_setGlobal](last(this.#stack)) + } + } + } + + reset () { + // Everything could be reset manually so only + // teardown if we have an initial descriptor left + // and then delete the rest of the stack + if (this.#stack.length) { + this[_setGlobal](this.#stack[0]) + this.#stack.length = 0 + } + } + + [_setGlobal] (d) { + if (this.#isDelete(d)) { + delete this.#global[this.#valueKey] + } else { + Object.defineProperty(this.#global, this.#valueKey, d) + } + return d + } + + [_nextDescriptor] (value) { + if (value === undefined) { + return this.#delete() + } + const d = last(this.#stack) + return { + // If the previous descriptor was one to delete the property + // then use the default descriptor as the base + ...(this.#isDelete(d) ? this.#defaultDescriptor : d), + ...(d && d.get ? { get: () => value } : { value }), + } + } +} + +class MockGlobals { + #descriptors = {} + + register (globals, { replace = false } = {}) { + // Replace means dont merge in object values but replace them instead + // so we only get top level keys instead of walking the obj + const keys = replace ? Object.keys(globals) : getKeys(globals) + + // An error state where due to object mode there are multiple global + // values to be set with the same key + const duplicates = dupes(keys) + if (duplicates.length) { + throw new Error(`mockGlobals was called with duplicate keys: ${duplicates}`) + } + + // Another error where when in replace mode overlapping keys are set like + // process and process.stdout which would cause unexpected behavior + const overlapping = dupesStartsWith(keys) + if (overlapping.length) { + const message = overlapping + .map((k) => `${k} -> ${keys.filter((kk) => kk.startsWith(k + sep))}`) + throw new Error(`mockGlobals was called with overlapping keys: ${message}`) + } + + // Set each property passed in and return fns to reset them + // Return an object with each path as a key for manually resetting in each test + return keys.reduce((acc, key) => { + const desc = this.#descriptors[key] || (this.#descriptors[key] = new DescriptorStack(key)) + acc[key] = desc.add(get(globals, key)) + return acc + }, {}) + } + + teardown (key) { + if (!key) { + Object.values(this.#descriptors).forEach((d) => d.reset()) + return + } + this.#descriptors[key].reset() + } +} + +// Each test has one instance of MockGlobals so it can be called multiple times per test +// Its a weak map so that it can be garbage collected along with the tap tests without +// needing to explicitly call cache.delete +const cache = new WeakMap() + +module.exports = (t, globals, options) => { + let instance = cache.get(t) + if (!instance) { + instance = cache.set(t, new MockGlobals()).get(t) + // Teardown only needs to be initialized once. The instance + // will keep track of its own state during the test + t.teardown(() => instance.teardown()) + } + + return { + // Reset contains only the functions to reset the globals + // set by this function call + reset: instance.register(globals, options), + // Teardown will reset across all calls tied to this test + teardown: () => instance.teardown(), + } +} diff --git a/test/fixtures/mock-logs.js b/test/fixtures/mock-logs.js new file mode 100644 index 000000000..80037c6ff --- /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 a51ec3e5b..751885531 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 b012790fb..701d9cea7 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 26db16e1f..081c89cee 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 7b61e9f6e..0c23f8ba6 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 d59220559..473c8cc24 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 d762943b4..f02c57d8c 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 fdf132aff..298897e4f 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 71d79ea93..8a9358f9a 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 3c87c76a8..05f268d6b 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 8c95dd57b..9156d3df0 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 70a8ba1b2..fc92facff 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 1091f9125..537d0784f 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 51212f06d..dd571baf7 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 8fc0be061..2e2fae238 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 811936fe6..ed0702e37 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 6b45dc116..756a09d7d 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 e3ad5cc72..51b6111a0 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 4ab26568f..3c75c1d8d 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 b2e7be213..d1355d767 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 c1b9c71df..06bd097b6 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 ba9e770e3..597cccc3f 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 74b33168a..215ebc581 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 994684596..d5db3af67 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 39ef86c84..ee01e7500 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 8645b349f..b5d4d1584 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 <user> <pkg> 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 <user> <pkg> 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 <user> <pkg> 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 <user> <pkg> 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 <user> <pkg> 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 <user> <pkg> 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 bc8877208..21057e207 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 7011c709b..f808e0ac3 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 6f059e73a..e8295cf6a 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 6554ca89e..0f16c1db1 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 <key> <value>', 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 49d5ab9be..a7f56547b 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 5f4fb4010..1178cd6ee 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 4e61047b4..93eb6d031 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 608de0331..7730f1a30 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 9871ddb25..a886b30c3 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 e421c655e..ea0227cda 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 592a2431c..2c4fe57d6 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 a57ea1a54..feeb90157 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 db4021abd..2b9e46c70 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 13838bb10..9a4903642 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 4ed643858..959739653 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 1f26f38ea..4f7dc366d 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 4f189449b..53d057b71 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 4e5ce289b..a3dbd3ff4 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 6d0dc9d7e..65a094a0b 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 6ac206753..1424adf5c 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 <pkg>@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 <pkg>@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 6ca6dbc87..aecb2c32b 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=<number>', 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=<number>', 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 6603b5810..980353897 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 728787ec4..035490a79 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 dc6144ec1..66c3f0c6b 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 000000000..02566e575 --- /dev/null +++ b/test/lib/fixtures/mock-globals.js @@ -0,0 +1,321 @@ +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, + env: process.env, + setInterval, +} + +t.test('console', async t => { + await t.test('mocks', async (t) => { + const errors = [] + mockGlobals(t, { + 'console.error': (...args) => errors.push(...args), + }) + + console.error(1) + console.error(2) + console.error(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('reset called multiple times', 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() + + mockGlobals(t, { + process: { + stderr: { + on: '1', + }, + env: { + HOME: home, + }, + }, + }) + + t.equal(process.stderr.on, '1') + 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('conflicting mixed object/string mode', async t => { + await t.test('same key', async t => { + t.throws( + () => mockGlobals(t, { + process: { + env: { + HOME: '1', + TEST: '1', + NODE_ENV: '1', + }, + stderr: { + write: '1', + }, + }, + 'process.env.HOME': '1', + 'process.stderr.write': '1', + }), + /process.env.HOME,process.stderr.write/ + ) + }) + + await t.test('partial overwrite with replace', async t => { + t.throws( + () => mockGlobals(t, { + process: { + env: { + HOME: '1', + TEST: '1', + NODE_ENV: '1', + }, + stderr: { + write: '1', + }, + }, + 'process.env.HOME': '1', + 'process.stderr.write': '1', + }, { replace: true }), + /process -> process.env.HOME,process.stderr.write/ + ) + }) +}) + +t.test('falsy values', async t => { + await t.test('undefined deletes', async t => { + mockGlobals(t, { 'process.platform': undefined }) + t.notOk(Object.prototype.hasOwnProperty.call(process, 'platform')) + t.equal(process.platform, undefined) + }) + + await t.test('null', async t => { + mockGlobals(t, { 'process.platform': null }) + t.ok(Object.prototype.hasOwnProperty.call(process, 'platform')) + t.equal(process.platform, null) + }) + + t.equal(process.platform, originals.platform) +}) + +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.test('replace', async (t) => { + await t.test('env', async t => { + mockGlobals(t, { 'process.env': { HOME: '1' } }, { replace: true }) + t.strictSame(process.env, { HOME: '1' }) + t.equal(Object.keys(process.env).length, 1) + }) + + await t.test('setInterval', async t => { + mockGlobals(t, { setInterval: 0 }, { replace: true }) + t.strictSame(setInterval, 0) + }) + + t.strictSame(setInterval, originals.setInterval) + t.strictSame(process.env, originals.env) +}) + +t.test('multiple mocks and resets', async (t) => { + const initial = 'a' + const platforms = ['b', 'c', 'd', 'e', 'f', 'g'] + + await t.test('first in, first out', async t => { + mockGlobals(t, { 'process.platform': initial }) + t.equal(process.platform, initial) + + await t.test('platforms', async (t) => { + const resets = platforms.map((platform) => { + const { reset } = mockGlobals(t, { 'process.platform': platform }) + t.equal(process.platform, platform) + return reset['process.platform'] + }).reverse() + + ;[...platforms.reverse()].forEach((platform, index) => { + const reset = resets[index] + const nextPlatform = index === platforms.length - 1 ? initial : platforms[index + 1] + t.equal(process.platform, platform) + reset() + t.equal(process.platform, nextPlatform, 'first reset') + reset() + reset() + t.equal(process.platform, nextPlatform, 'multiple resets are indempotent') + }) + }) + + t.equal(process.platform, initial) + }) + + await t.test('last in,first out', async t => { + mockGlobals(t, { 'process.platform': initial }) + t.equal(process.platform, initial) + + await t.test('platforms', async (t) => { + const resets = platforms.map((platform) => { + const { reset } = mockGlobals(t, { 'process.platform': platform }) + t.equal(process.platform, platform) + return reset['process.platform'] + }) + + resets.forEach((reset, index) => { + // Calling a reset out of order removes it from the stack + // but does not change the descriptor so it should still be the + // last in descriptor until there are none left + const lastPlatform = platforms[platforms.length - 1] + const nextPlatform = index === platforms.length - 1 ? initial : lastPlatform + t.equal(process.platform, lastPlatform) + reset() + t.equal(process.platform, nextPlatform, 'multiple resets are indempotent') + reset() + reset() + t.equal(process.platform, nextPlatform, 'multiple resets are indempotent') + }) + }) + + t.equal(process.platform, initial) + }) + + t.test('reset all', async (t) => { + const { teardown } = mockGlobals(t, { 'process.platform': initial }) + + await t.test('platforms', async (t) => { + const resets = platforms.map((p) => { + const { teardown, reset } = mockGlobals(t, { 'process.platform': p }) + t.equal(process.platform, p) + return [ + reset['process.platform'], + teardown, + ] + }) + + resets.forEach(r => r[1]()) + t.equal(process.platform, initial, 'teardown goes to initial value') + + resets.forEach((r) => r[0]()) + t.equal(process.platform, initial, 'calling resets after teardown does nothing') + }) + + t.equal(process.platform, initial) + teardown() + t.equal(process.platform, originals.platform) + }) +}) diff --git a/test/lib/load-all-commands.js b/test/lib/load-all-commands.js index f813e50b2..248c81a30 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 fb45331ba..e5d7b558c 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 1ccd26e37..2a0c5a89d 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 c683053cb..bcb7d8c16 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 e97cf36b5..000000000 --- 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 7af0b6839..bf4b48709 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,42 +362,79 @@ 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) - delete process.env.NO_COLOR + 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 }) + + 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') t.end() }) +t.test('progress', t => { + const setEnv = ({ tty, term } = {}) => mockGlobals(t, { + 'process.stderr.isTTY': tty, + 'process.env.TERM': term, + }) + + const flat = {} + + definitions.progress.flatten('progress', {}, flat) + t.strictSame(flat, { progress: false }) + + setEnv({ tty: true, term: 'notdumb' }) + definitions.progress.flatten('progress', { progress: true }, flat) + t.strictSame(flat, { progress: true }) + + setEnv({ tty: false, term: 'notdumb' }) + definitions.progress.flatten('progress', { progress: true }, flat) + t.strictSame(flat, { progress: false }) + + setEnv({ tty: true, term: 'dumb' }) + definitions.progress.flatten('progress', { progress: true }, flat) + t.strictSame(flat, { progress: false }) + + t.end() +}) + t.test('retry options', t => { const obj = {} // <config>: flat.retry[<option>] diff --git a/test/lib/utils/did-you-mean.js b/test/lib/utils/did-you-mean.js index 185368d61..d3cb3a24f 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 000000000..30cd2cc27 --- /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 1959b9217..ddc88c1d9 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 adc7c3f4e..54bf48f89 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 94fde0ace..0fbebdf8e 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 000000000..adc1a2e03 --- /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 000000000..dee4efbaa --- /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 77254a80d..035d4bbb2 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 d580defa8..000000000 --- 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 acbf66396..9f7a94614 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 35101f1d7..be805a2a8 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 9a1bffb40..4e9ed7133 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 7f907bc7e..000000000 --- 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 19d949169..adc5cb364 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 000000000..6127f346b --- /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 4d806cefc..2703044a2 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 78ff93825..a7a800c60 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']) |