diff --git a/doc/api/test.md b/doc/api/test.md index 0cdf37e42c4cec..0b66d79370301f 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -709,6 +709,10 @@ unless a destination is explicitly provided. * `options` {Object} Configuration options for running tests. The following @@ -734,6 +738,12 @@ added: v18.9.0 number. If a nullish value is provided, each process gets its own port, incremented from the primary's `process.debugPort`. **Default:** `undefined`. + * `testNamePatterns` {string|RegExp|Array} A String, RegExp or a RegExp Array, + that can be used to only run tests whose name matches the provided pattern. + Test name patterns are interpreted as JavaScript regular expressions. + For each test that is executed, any corresponding test hooks, such as + `beforeEach()`, are also run. + **Default:** `undefined`. * Returns: {TestsStream} ```mjs diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index decaf544efdd07..9be9883ab0adad 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -1,10 +1,12 @@ 'use strict'; const { ArrayFrom, + ArrayIsArray, ArrayPrototypeFilter, ArrayPrototypeForEach, ArrayPrototypeIncludes, ArrayPrototypeIndexOf, + ArrayPrototypeMap, ArrayPrototypePush, ArrayPrototypeSlice, ArrayPrototypeSome, @@ -33,11 +35,13 @@ const { FilesWatcher } = require('internal/watch_mode/files_watcher'); const console = require('internal/console/global'); const { codes: { + ERR_INVALID_ARG_TYPE, ERR_TEST_FAILURE, }, } = require('internal/errors'); const { validateArray, validateBoolean, validateFunction } = require('internal/validators'); const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector'); +const { isRegExp } = require('internal/util/types'); const { kEmptyObject } = require('internal/util'); const { createTestTree } = require('internal/test_runner/harness'); const { @@ -53,6 +57,7 @@ const { YAMLToJs } = require('internal/test_runner/yaml_to_js'); const { TokenKind } = require('internal/test_runner/tap_lexer'); const { + convertStringToRegExp, countCompletedTest, doesPathMatchFilter, isSupportedFileType, @@ -137,11 +142,14 @@ function filterExecArgv(arg, i, arr) { !ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`)); } -function getRunArgs({ path, inspectPort }) { +function getRunArgs({ path, inspectPort, testNamePatterns }) { const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv); if (isUsingInspector()) { ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`); } + if (testNamePatterns) { + ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(argv, `--test-name-pattern=${pattern}`)); + } ArrayPrototypePush(argv, path); return argv; @@ -255,9 +263,9 @@ class FileTest extends Test { const runningProcesses = new SafeMap(); const runningSubtests = new SafeMap(); -function runTestFile(path, root, inspectPort, filesWatcher) { +function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { const subtest = root.createSubtest(FileTest, path, async (t) => { - const args = getRunArgs({ path, inspectPort }); + const args = getRunArgs({ path, inspectPort, testNamePatterns }); const stdio = ['pipe', 'pipe', 'pipe']; const env = { ...process.env, NODE_TEST_CONTEXT: 'child' }; if (filesWatcher) { @@ -339,7 +347,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); @@ -353,7 +361,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 */); })); @@ -365,6 +373,7 @@ function run(options) { if (options === null || typeof options !== 'object') { options = kEmptyObject; } + let { testNamePatterns } = options; const { concurrency, timeout, signal, files, inspectPort, watch, setup } = options; if (files != null) { @@ -376,6 +385,22 @@ function run(options) { if (setup != null) { validateFunction(setup, 'options.setup'); } + if (testNamePatterns != null) { + if (!ArrayIsArray(testNamePatterns)) { + testNamePatterns = [testNamePatterns]; + } + validateArray(testNamePatterns, 'options.testNamePatterns'); + testNamePatterns = ArrayPrototypeMap(testNamePatterns, (value, i) => { + if (isRegExp(value)) { + return value; + } + const name = `options.testNamePatterns[${i}]`; + if (typeof value === 'string') { + return convertStringToRegExp(value, name); + } + throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value); + }); + } const root = createTestTree({ concurrency, timeout, signal }); const testFiles = files ?? createTestFileList(); @@ -383,13 +408,13 @@ 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 = () => { 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/test/fixtures/test-runner/test/skip_by_name.cjs b/test/fixtures/test-runner/test/skip_by_name.cjs new file mode 100644 index 00000000000000..14856df43e50f5 --- /dev/null +++ b/test/fixtures/test-runner/test/skip_by_name.cjs @@ -0,0 +1,5 @@ +'use strict'; +const test = require('node:test'); + +test('this should be skipped'); +test('this should be executed'); diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index b37bdf94f38c02..794d55ab1a51d1 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -101,4 +101,20 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { assert.strictEqual(result[11], '# todo 0\n'); assert.match(result[12], /# duration_ms \d+\.?\d*/); }); + + it('should skip tests not matching testNamePatterns - RegExp', async () => { + const result = await run({ files: [join(testFixtures, 'test/skip_by_name.cjs')], testNamePatterns: [/executed/] }) + .compose(tap) + .toArray(); + assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP test name does not match pattern\n'); + assert.strictEqual(result[5], 'ok 2 - this should be executed\n'); + }); + + it('should skip tests not matching testNamePatterns - string', async () => { + const result = await run({ files: [join(testFixtures, 'test/skip_by_name.cjs')], testNamePatterns: ['executed'] }) + .compose(tap) + .toArray(); + assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP test name does not match pattern\n'); + assert.strictEqual(result[5], 'ok 2 - this should be executed\n'); + }); });