diff --git a/lib/base-command.js b/lib/base-command.js index e763820cb052b..e4a7bbbec724a 100644 --- a/lib/base-command.js +++ b/lib/base-command.js @@ -5,6 +5,7 @@ const { relative } = require('path') const { definitions } = require('@npmcli/config/lib/definitions') const getWorkspaces = require('./workspaces/get-workspaces.js') const { aliases: cmdAliases } = require('./utils/cmd-list') +const log = require('./utils/log-shim.js') class BaseCommand { static workspaces = false @@ -142,6 +143,24 @@ class BaseCommand { return this.exec(args) } + // Compare the number of entries with what was expected + checkExpected (entries) { + if (!this.npm.config.isDefault('expect-results')) { + const expected = this.npm.config.get('expect-results') + if (!!entries !== !!expected) { + log.warn(this.name, `Expected ${expected ? '' : 'no '}results, got ${entries}`) + process.exitCode = 1 + } + } else if (!this.npm.config.isDefault('expect-result-count')) { + const expected = this.npm.config.get('expect-result-count') + if (expected !== entries) { + /* eslint-disable-next-line max-len */ + log.warn(this.name, `Expected ${expected} result${expected === 1 ? '' : 's'}, got ${entries}`) + process.exitCode = 1 + } + } + } + async setWorkspaces () { const includeWorkspaceRoot = this.isArboristCmd ? false diff --git a/lib/commands/query.js b/lib/commands/query.js index 68aa1fa2c06a5..17a55a446b086 100644 --- a/lib/commands/query.js +++ b/lib/commands/query.js @@ -50,6 +50,7 @@ class Query extends BaseCommand { 'workspaces', 'include-workspace-root', 'package-lock-only', + 'expect-results', ] get parsedResponse () { @@ -81,6 +82,7 @@ class Query extends BaseCommand { const items = await tree.querySelectorAll(args[0], this.npm.flatOptions) this.buildResponse(items) + this.checkExpected(this.#response.length) this.npm.output(this.parsedResponse) } @@ -104,6 +106,7 @@ class Query extends BaseCommand { } this.buildResponse(items) } + this.checkExpected(this.#response.length) this.npm.output(this.parsedResponse) } diff --git a/tap-snapshots/test/lib/commands/config.js.test.cjs b/tap-snapshots/test/lib/commands/config.js.test.cjs index 6c4a27ca5916b..7ee863b161acc 100644 --- a/tap-snapshots/test/lib/commands/config.js.test.cjs +++ b/tap-snapshots/test/lib/commands/config.js.test.cjs @@ -50,6 +50,7 @@ exports[`test/lib/commands/config.js TAP config list --json > output matches sna "dry-run": false, "editor": "{EDITOR}", "engine-strict": false, + "expect-entries": null, "fetch-retries": 2, "fetch-retry-factor": 10, "fetch-retry-maxtimeout": 60000, @@ -207,6 +208,7 @@ diff-unified = 3 dry-run = false editor = "{EDITOR}" engine-strict = false +expect-entries = null fetch-retries = 2 fetch-retry-factor = 10 fetch-retry-maxtimeout = 60000 diff --git a/test/lib/commands/query.js b/test/lib/commands/query.js index adf617316772e..ebe89ed0da278 100644 --- a/test/lib/commands/query.js +++ b/test/lib/commands/query.js @@ -61,6 +61,7 @@ t.test('recursive tree', async t => { await npm.exec('query', ['*']) t.matchSnapshot(joinedOutput(), 'should return everything in the tree, accounting for recursion') }) + t.test('workspace query', async t => { const { npm, joinedOutput } = await loadMockNpm(t, { config: { @@ -237,3 +238,85 @@ t.test('package-lock-only', t => { }) t.end() }) + +t.test('expect entries', t => { + const { exitCode } = process + t.afterEach(() => process.exitCode = exitCode) + const prefixDir = { + node_modules: { + a: { name: 'a', version: '1.0.0' }, + }, + 'package.json': JSON.stringify({ + name: 'project', + dependencies: { a: '^1.0.0' }, + }), + } + t.test('false, has entries', async t => { + const { logs, npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-results', false) + await npm.exec('query', ['#a']) + t.not(joinedOutput(), '[]', 'has entries') + t.same(logs.warn, [['query', 'Expected no results, got 1']]) + t.ok(process.exitCode, 'exits with code') + }) + t.test('false, no entries', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-results', false) + await npm.exec('query', ['#b']) + t.equal(joinedOutput(), '[]', 'does not have entries') + t.notOk(process.exitCode, 'exits without code') + }) + t.test('true, has entries', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-results', true) + await npm.exec('query', ['#a']) + t.not(joinedOutput(), '[]', 'has entries') + t.notOk(process.exitCode, 'exits without code') + }) + t.test('true, no entries', async t => { + const { logs, npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-results', true) + await npm.exec('query', ['#b']) + t.equal(joinedOutput(), '[]', 'does not have entries') + t.same(logs.warn, [['query', 'Expected results, got 0']]) + t.ok(process.exitCode, 'exits with code') + }) + t.test('count, matches', async t => { + const { npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-result-count', 1) + await npm.exec('query', ['#a']) + t.not(joinedOutput(), '[]', 'has entries') + t.notOk(process.exitCode, 'exits without code') + }) + t.test('count 1, does not match', async t => { + const { logs, npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-result-count', 1) + await npm.exec('query', ['#b']) + t.equal(joinedOutput(), '[]', 'does not have entries') + t.same(logs.warn, [['query', 'Expected 1 result, got 0']]) + t.ok(process.exitCode, 'exits with code') + }) + t.test('count 3, does not match', async t => { + const { logs, npm, joinedOutput } = await loadMockNpm(t, { + prefixDir, + }) + npm.config.set('expect-result-count', 3) + await npm.exec('query', ['#b']) + t.equal(joinedOutput(), '[]', 'does not have entries') + t.same(logs.warn, [['query', 'Expected 3 results, got 0']]) + t.ok(process.exitCode, 'exits with code') + }) + t.end() +}) diff --git a/workspaces/config/lib/definitions/definitions.js b/workspaces/config/lib/definitions/definitions.js index 6f8760fce1d3e..1f320dfc9d9b6 100644 --- a/workspaces/config/lib/definitions/definitions.js +++ b/workspaces/config/lib/definitions/definitions.js @@ -665,6 +665,26 @@ define('engine-strict', { flatten, }) +define('expect-results', { + default: null, + type: [null, Boolean], + exclusive: ['expect-result-count'], + description: ` + Tells npm whether or not to expect results from the command. + Can be either true (expect some results) or false (expect no results). + `, +}) + +define('expect-result-count', { + default: null, + type: [null, Number], + hint: '', + exclusive: ['expect-results'], + description: ` + Tells to expect a specific number of results from the command. + `, +}) + define('fetch-retries', { default: 2, type: Number,