Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test_runner: add testNamePatterns to run api #47628

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,10 @@ unless a destination is explicitly provided.
added:
- v18.9.0
- v16.19.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/47628
description: Add a testNamePatterns option.
-->

* `options` {Object} Configuration options for running tests. The following
Expand All @@ -751,6 +755,12 @@ added:
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
Expand Down
39 changes: 32 additions & 7 deletions lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
'use strict';
const {
ArrayFrom,
ArrayIsArray,
ArrayPrototypeFilter,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
ArrayPrototypeIndexOf,
ArrayPrototypeMap,
ArrayPrototypePush,
ArrayPrototypeSlice,
ArrayPrototypeSome,
Expand Down Expand Up @@ -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 {
Expand All @@ -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,
Expand Down Expand Up @@ -138,11 +143,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;
Expand Down Expand Up @@ -256,9 +264,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) {
Expand Down Expand Up @@ -340,7 +348,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);
Expand All @@ -354,7 +362,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 */);
}));
Expand All @@ -366,6 +374,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) {
Expand All @@ -377,20 +386,36 @@ function run(options) {
if (setup != null) {
validateFunction(setup, 'options.setup');
}
if (testNamePatterns != null) {
if (!ArrayIsArray(testNamePatterns)) {
testNamePatterns = [testNamePatterns];
}
atlowChemi marked this conversation as resolved.
Show resolved Hide resolved
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();

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;
});
Expand Down
5 changes: 5 additions & 0 deletions test/fixtures/test-runner/test/skip_by_name.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';
const test = require('node:test');

test('this should be skipped');
test('this should be executed');
16 changes: 16 additions & 0 deletions test/parallel/test-runner-run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
MoLow marked this conversation as resolved.
Show resolved Hide resolved
.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');
});
});