Skip to content

Commit

Permalink
test_runner: add testNamePatterns to run api
Browse files Browse the repository at this point in the history
Accept a `testNamePatterns` value in the `run` fn, and drill those
patterns to the spawned processes.

PR-URL: #47648
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
  • Loading branch information
atlowChemi authored and MoLow committed Jul 6, 2023
1 parent 27a696f commit fa18b17
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 7 deletions.
10 changes: 10 additions & 0 deletions doc/api/test.md
Expand Up @@ -709,6 +709,10 @@ unless a destination is explicitly provided.

<!-- YAML
added: v18.9.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 @@ -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
Expand Down
39 changes: 32 additions & 7 deletions lib/internal/test_runner/runner.js
@@ -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 @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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 */);
}));
Expand All @@ -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) {
Expand All @@ -376,20 +385,36 @@ 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();

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
@@ -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
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)
.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');
});
});

0 comments on commit fa18b17

Please sign in to comment.