Skip to content

Commit fa18b17

Browse files
atlowChemiMoLow
authored andcommitted
test_runner: add testNamePatterns to run api
Accept a `testNamePatterns` value in the `run` fn, and drill those patterns to the spawned processes. PR-URL: #47648 Reviewed-By: Moshe Atlow <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]>
1 parent 27a696f commit fa18b17

File tree

4 files changed

+63
-7
lines changed

4 files changed

+63
-7
lines changed

doc/api/test.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,10 @@ unless a destination is explicitly provided.
709709

710710
<!-- YAML
711711
added: v18.9.0
712+
changes:
713+
- version: REPLACEME
714+
pr-url: https://github.com/nodejs/node/pull/47628
715+
description: Add a testNamePatterns option.
712716
-->
713717

714718
* `options` {Object} Configuration options for running tests. The following
@@ -734,6 +738,12 @@ added: v18.9.0
734738
number. If a nullish value is provided, each process gets its own port,
735739
incremented from the primary's `process.debugPort`.
736740
**Default:** `undefined`.
741+
* `testNamePatterns` {string|RegExp|Array} A String, RegExp or a RegExp Array,
742+
that can be used to only run tests whose name matches the provided pattern.
743+
Test name patterns are interpreted as JavaScript regular expressions.
744+
For each test that is executed, any corresponding test hooks, such as
745+
`beforeEach()`, are also run.
746+
**Default:** `undefined`.
737747
* Returns: {TestsStream}
738748

739749
```mjs

lib/internal/test_runner/runner.js

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
'use strict';
22
const {
33
ArrayFrom,
4+
ArrayIsArray,
45
ArrayPrototypeFilter,
56
ArrayPrototypeForEach,
67
ArrayPrototypeIncludes,
78
ArrayPrototypeIndexOf,
9+
ArrayPrototypeMap,
810
ArrayPrototypePush,
911
ArrayPrototypeSlice,
1012
ArrayPrototypeSome,
@@ -33,11 +35,13 @@ const { FilesWatcher } = require('internal/watch_mode/files_watcher');
3335
const console = require('internal/console/global');
3436
const {
3537
codes: {
38+
ERR_INVALID_ARG_TYPE,
3639
ERR_TEST_FAILURE,
3740
},
3841
} = require('internal/errors');
3942
const { validateArray, validateBoolean, validateFunction } = require('internal/validators');
4043
const { getInspectPort, isUsingInspector, isInspectorMessage } = require('internal/util/inspector');
44+
const { isRegExp } = require('internal/util/types');
4145
const { kEmptyObject } = require('internal/util');
4246
const { createTestTree } = require('internal/test_runner/harness');
4347
const {
@@ -53,6 +57,7 @@ const { YAMLToJs } = require('internal/test_runner/yaml_to_js');
5357
const { TokenKind } = require('internal/test_runner/tap_lexer');
5458

5559
const {
60+
convertStringToRegExp,
5661
countCompletedTest,
5762
doesPathMatchFilter,
5863
isSupportedFileType,
@@ -137,11 +142,14 @@ function filterExecArgv(arg, i, arr) {
137142
!ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`));
138143
}
139144

140-
function getRunArgs({ path, inspectPort }) {
145+
function getRunArgs({ path, inspectPort, testNamePatterns }) {
141146
const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
142147
if (isUsingInspector()) {
143148
ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`);
144149
}
150+
if (testNamePatterns) {
151+
ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(argv, `--test-name-pattern=${pattern}`));
152+
}
145153
ArrayPrototypePush(argv, path);
146154

147155
return argv;
@@ -255,9 +263,9 @@ class FileTest extends Test {
255263
const runningProcesses = new SafeMap();
256264
const runningSubtests = new SafeMap();
257265

258-
function runTestFile(path, root, inspectPort, filesWatcher) {
266+
function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) {
259267
const subtest = root.createSubtest(FileTest, path, async (t) => {
260-
const args = getRunArgs({ path, inspectPort });
268+
const args = getRunArgs({ path, inspectPort, testNamePatterns });
261269
const stdio = ['pipe', 'pipe', 'pipe'];
262270
const env = { ...process.env, NODE_TEST_CONTEXT: 'child' };
263271
if (filesWatcher) {
@@ -339,7 +347,7 @@ function runTestFile(path, root, inspectPort, filesWatcher) {
339347
return promise;
340348
}
341349

342-
function watchFiles(testFiles, root, inspectPort) {
350+
function watchFiles(testFiles, root, inspectPort, testNamePatterns) {
343351
const filesWatcher = new FilesWatcher({ throttle: 500, mode: 'filter' });
344352
filesWatcher.on('changed', ({ owners }) => {
345353
filesWatcher.unfilterFilesOwnedBy(owners);
@@ -353,7 +361,7 @@ function watchFiles(testFiles, root, inspectPort) {
353361
await once(runningProcess, 'exit');
354362
}
355363
await runningSubtests.get(file);
356-
runningSubtests.set(file, runTestFile(file, root, inspectPort, filesWatcher));
364+
runningSubtests.set(file, runTestFile(file, root, inspectPort, filesWatcher, testNamePatterns));
357365
}, undefined, (error) => {
358366
triggerUncaughtException(error, true /* fromPromise */);
359367
}));
@@ -365,6 +373,7 @@ function run(options) {
365373
if (options === null || typeof options !== 'object') {
366374
options = kEmptyObject;
367375
}
376+
let { testNamePatterns } = options;
368377
const { concurrency, timeout, signal, files, inspectPort, watch, setup } = options;
369378

370379
if (files != null) {
@@ -376,20 +385,36 @@ function run(options) {
376385
if (setup != null) {
377386
validateFunction(setup, 'options.setup');
378387
}
388+
if (testNamePatterns != null) {
389+
if (!ArrayIsArray(testNamePatterns)) {
390+
testNamePatterns = [testNamePatterns];
391+
}
392+
validateArray(testNamePatterns, 'options.testNamePatterns');
393+
testNamePatterns = ArrayPrototypeMap(testNamePatterns, (value, i) => {
394+
if (isRegExp(value)) {
395+
return value;
396+
}
397+
const name = `options.testNamePatterns[${i}]`;
398+
if (typeof value === 'string') {
399+
return convertStringToRegExp(value, name);
400+
}
401+
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'RegExp'], value);
402+
});
403+
}
379404

380405
const root = createTestTree({ concurrency, timeout, signal });
381406
const testFiles = files ?? createTestFileList();
382407

383408
let postRun = () => root.postRun();
384409
let filesWatcher;
385410
if (watch) {
386-
filesWatcher = watchFiles(testFiles, root, inspectPort);
411+
filesWatcher = watchFiles(testFiles, root, inspectPort, testNamePatterns);
387412
postRun = undefined;
388413
}
389414
const runFiles = () => {
390415
root.harness.bootstrapComplete = true;
391416
return SafePromiseAllSettledReturnVoid(testFiles, (path) => {
392-
const subtest = runTestFile(path, root, inspectPort, filesWatcher);
417+
const subtest = runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns);
393418
runningSubtests.set(path, subtest);
394419
return subtest;
395420
});
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
'use strict';
2+
const test = require('node:test');
3+
4+
test('this should be skipped');
5+
test('this should be executed');

test/parallel/test-runner-run.mjs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,4 +101,20 @@ describe('require(\'node:test\').run', { concurrency: true }, () => {
101101
assert.strictEqual(result[11], '# todo 0\n');
102102
assert.match(result[12], /# duration_ms \d+\.?\d*/);
103103
});
104+
105+
it('should skip tests not matching testNamePatterns - RegExp', async () => {
106+
const result = await run({ files: [join(testFixtures, 'test/skip_by_name.cjs')], testNamePatterns: [/executed/] })
107+
.compose(tap)
108+
.toArray();
109+
assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP test name does not match pattern\n');
110+
assert.strictEqual(result[5], 'ok 2 - this should be executed\n');
111+
});
112+
113+
it('should skip tests not matching testNamePatterns - string', async () => {
114+
const result = await run({ files: [join(testFixtures, 'test/skip_by_name.cjs')], testNamePatterns: ['executed'] })
115+
.compose(tap)
116+
.toArray();
117+
assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP test name does not match pattern\n');
118+
assert.strictEqual(result[5], 'ok 2 - this should be executed\n');
119+
});
104120
});

0 commit comments

Comments
 (0)