diff --git a/doc/api/errors.md b/doc/api/errors.md index 9139194719ade5..c94c1401df3620 100644 --- a/doc/api/errors.md +++ b/doc/api/errors.md @@ -697,6 +697,14 @@ message thrown by `block` because that usage suggests that the user believes `message` is the expected message rather than the message the `AssertionError` will display if `block` does not throw. + + +### `ERR_ARG_NOT_ALLOWED` + +A function argument that can't be accepted depending of some logic restriction. +e.g. The function `run(...)` from module `node:test` can't accept `testNamePatterns` +param from programatic function and from CLI at same time. + ### `ERR_ARG_NOT_ITERABLE` diff --git a/doc/api/test.md b/doc/api/test.md index 59b1647b12632a..750fad667a4a05 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -737,6 +737,10 @@ added: number. If a nullish value is provided, each process gets its own port, incremented from the primary's `process.debugPort`. **Default:** `undefined`. + * `testNamePatterns` {array|regex} A regex used to filter the tests + by name before run it. + This can be a regex, or a regex string. + **Default:** `undefined`. * Returns: {TestsStream} ```js diff --git a/lib/internal/errors.js b/lib/internal/errors.js index 742d723155e7fe..ce036e3be7d066 100644 --- a/lib/internal/errors.js +++ b/lib/internal/errors.js @@ -960,6 +960,7 @@ module.exports = { // Note: Node.js specific errors must begin with the prefix ERR_ E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError); +E('ERR_ARG_NOT_ALLOWED', '%s is already being assigned', Error); E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError); E('ERR_ASSERTION', '%s', Error); E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError); diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 5b772251be981e..59914b09796b1a 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -256,8 +256,9 @@ class FileTest extends Test { const runningProcesses = new SafeMap(); const runningSubtests = new SafeMap(); -function runTestFile(path, root, inspectPort, filesWatcher) { - const subtest = root.createSubtest(FileTest, path, async (t) => { +function runTestFile(options) { + const { path, root, inspectPort, filesWatcher, testNamePatterns } = options; + const subtest = root.createSubtest(FileTest, path, { testNamePatterns }, async (t) => { const args = getRunArgs({ path, inspectPort }); const stdio = ['pipe', 'pipe', 'pipe']; const env = { ...process.env }; @@ -340,7 +341,7 @@ function runTestFile(path, root, inspectPort, filesWatcher) { return promise; } -function watchFiles(testFiles, root, inspectPort) { +function watchFiles(testFiles, root, inspectPort, testNamePatterns) { const filesWatcher = new FilesWatcher({ throttle: 500, mode: 'filter' }); filesWatcher.on('changed', ({ owners }) => { filesWatcher.unfilterFilesOwnedBy(owners); @@ -354,7 +355,7 @@ function watchFiles(testFiles, root, inspectPort) { await once(runningProcess, 'exit'); } await runningSubtests.get(file); - runningSubtests.set(file, runTestFile(file, root, inspectPort, filesWatcher)); + runningSubtests.set(file, runTestFile({ file, root, inspectPort, filesWatcher, testNamePatterns })); }, undefined, (error) => { triggerUncaughtException(error, true /* fromPromise */); })); @@ -366,7 +367,7 @@ function run(options) { if (options === null || typeof options !== 'object') { options = kEmptyObject; } - const { concurrency, timeout, signal, files, inspectPort, watch, setup } = options; + const { concurrency, timeout, signal, files, inspectPort, watch, setup, testNamePatterns } = options; if (files != null) { validateArray(files, 'options.files'); @@ -384,13 +385,14 @@ function run(options) { let postRun = () => root.postRun(); let filesWatcher; if (watch) { - filesWatcher = watchFiles(testFiles, root, inspectPort); + filesWatcher = watchFiles(testFiles, root, inspectPort, testNamePatterns); postRun = undefined; } - const runFiles = () => { + + const runFiles = async () => { root.harness.bootstrapComplete = true; return SafePromiseAllSettledReturnVoid(testFiles, (path) => { - const subtest = runTestFile(path, root, inspectPort, filesWatcher); + const subtest = runTestFile({ path, root, inspectPort, filesWatcher, testNamePatterns }); runningSubtests.set(path, subtest); return subtest; }); diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index ffbf2d257aed62..29815b49e8003d 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -26,6 +26,7 @@ const { AbortController } = require('internal/abort_controller'); const { codes: { ERR_INVALID_ARG_TYPE, + ERR_ARG_NOT_ALLOWED, ERR_TEST_FAILURE, }, AbortError, @@ -42,6 +43,7 @@ const { kEmptyObject, once: runOnce, } = require('internal/util'); +const { ObjectHasOwn } = primordials; const { isPromise } = require('internal/util/types'); const { validateAbortSignal, @@ -163,9 +165,16 @@ class Test extends AsyncResource { constructor(options) { super('Test'); - let { fn, name, parent, skip } = options; + let { fn, name, parent, skip, testNamePatterns: _testNamePatterns } = options; const { concurrency, only, timeout, todo, signal } = options; + if (ObjectHasOwn(options, '_testNamePatterns') && testNamePatterns) { + throw new ERR_ARG_NOT_ALLOWED('testNamePatterns'); + } + if (!_testNamePatterns) { + _testNamePatterns = testNamePatterns; + } + if (typeof fn !== 'function') { fn = noop; } @@ -224,10 +233,10 @@ class Test extends AsyncResource { this.timeout = timeout; } - if (testNamePatterns !== null) { + if (_testNamePatterns !== null) { // eslint-disable-next-line no-use-before-define const match = this instanceof TestHook || ArrayPrototypeSome( - testNamePatterns, + _testNamePatterns, (re) => RegExpPrototypeExec(re, name) !== null, ); diff --git a/test/fixtures/test-runner/test/random.cjs b/test/fixtures/test-runner/test/random.cjs index 2a722c504b9fa5..277af025abee84 100644 --- a/test/fixtures/test-runner/test/random.cjs +++ b/test/fixtures/test-runner/test/random.cjs @@ -2,3 +2,5 @@ const test = require('node:test'); test('this should pass'); + +test('this should pass too'); diff --git a/test/parallel/test-runner-cli.js b/test/parallel/test-runner-cli.js index 8cfceedfe6a53a..d92e18a07b4195 100644 --- a/test/parallel/test-runner-cli.js +++ b/test/parallel/test-runner-cli.js @@ -18,6 +18,18 @@ const testFixtures = fixtures.path('test-runner'); assert.match(child.stderr.toString(), /^Could not find/); } + +{ + // Return only test that maches with the filter + const args = ['--test', '--test-name-pattern="too"', join(testFixtures, 'test/random.cjs')]; + const child = spawnSync('./node', args); + + assert.strictEqual(child.stderr.toString(), ''); + const stdout = child.stdout.toString(); + assert.match(stdout, /ok 1 - this should pass # SKIP test name does not match pattern/); + assert.match(stdout, /ok 2 - this should pass too/); +} + { // Default behavior. node_modules is ignored. Files that don't match the // pattern are ignored except in test/ directories. diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index 6ac007bfb5dd6c..8c5d2f21e04f15 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -8,6 +8,17 @@ const testFixtures = fixtures.path('test-runner'); describe('require(\'node:test\').run', { concurrency: true }, () => { + it('should not throw error', async () => { + const argv = process.argv; + process.argv = [null, `${process.cwd()}/test/fixtures/test-runner/test/random.cjs`]; + const stream = run(); + stream.on('test:pass', common.mustCall(2)); + stream.on('test:fail', common.mustNotCall()); + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + process.argv = argv; + }); + it('should run with no tests', async () => { const stream = run({ files: [] }); stream.on('test:fail', common.mustNotCall()); @@ -27,6 +38,14 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { it('should succeed with a file', async () => { const stream = run({ files: [join(testFixtures, 'test/random.cjs')] }); stream.on('test:fail', common.mustNotCall()); + stream.on('test:pass', common.mustCall(2)); + // eslint-disable-next-line no-unused-vars + for await (const _ of stream); + }); + + it('should succeed with a file and filter', async () => { + const stream = run({ files: [join(testFixtures, 'test/random.cjs')], testNamePatterns: [/too/] }); + stream.on('test:fail', common.mustNotCall()); stream.on('test:pass', common.mustCall(1)); // eslint-disable-next-line no-unused-vars for await (const _ of stream); @@ -35,7 +54,7 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { it('should run same file twice', async () => { const stream = run({ files: [join(testFixtures, 'test/random.cjs'), join(testFixtures, 'test/random.cjs')] }); stream.on('test:fail', common.mustNotCall()); - stream.on('test:pass', common.mustCall(2)); + stream.on('test:pass', common.mustCall(4)); // eslint-disable-next-line no-unused-vars for await (const _ of stream); });