Skip to content

Commit

Permalink
doc,lib,src,test: add --test-skip-pattern cli option
Browse files Browse the repository at this point in the history
  • Loading branch information
avivkeller committed Apr 14, 2024
1 parent f8e325e commit 122298b
Show file tree
Hide file tree
Showing 7 changed files with 94 additions and 40 deletions.
17 changes: 17 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1969,6 +1969,9 @@ A regular expression that configures the test runner to only execute tests
whose name matches the provided pattern. See the documentation on
[filtering tests by name][] for more details.

If both `--test-name-pattern` and `--test-skip-pattern` are supplied,
`--test-name-pattern` will take precedence.

### `--test-only`

<!-- YAML
Expand Down Expand Up @@ -2037,6 +2040,20 @@ node --test --test-shard=2/3
node --test --test-shard=3/3
```

### `--test-skip-pattern`

<!-- YAML
added:
- REPLACEME
-->

A regular expression that configures the test runner to only execute tests
whose name don't match the provided pattern. See the documentation on
[filtering tests by name][] for more details.

If both `--test-name-pattern` and `--test-skip-pattern` are supplied,
`--test-name-pattern` will take precedence.

### `--test-timeout`

<!-- YAML
Expand Down
64 changes: 40 additions & 24 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,20 +298,20 @@ describe.only('a suite', () => {

## Filtering tests by name

The [`--test-name-pattern`][] command-line option can be used to only run tests
whose name matches the provided pattern. Test name patterns are interpreted as
JavaScript regular expressions. The `--test-name-pattern` option can be
specified multiple times in order to run nested tests. For each test that is
executed, any corresponding test hooks, such as `beforeEach()`, are also
run. Tests that are not executed are omitted from the test runner output.

Given the following test file, starting Node.js with the
`--test-name-pattern="test [1-3]"` option would cause the test runner to execute
`test 1`, `test 2`, and `test 3`. If `test 1` did not match the test name
pattern, then its subtests would not execute, despite matching the pattern. The
same set of tests could also be executed by passing `--test-name-pattern`
multiple times (e.g. `--test-name-pattern="test 1"`,
`--test-name-pattern="test 2"`, etc.).
The [`--test-name-pattern`][] and [`--test-skip-pattern`][] command-line
options provide flexibility in selecting which tests to run and which to
skip based on their names.

### Using `--test-name-pattern`

The `--test-name-pattern` option filters tests based on their names by
matching them against the provided pattern, which is interpreted as a
JavaScript regular expression. This option can be specified multiple times
to include nested tests. When a test matches the pattern and is executed,
any corresponding test hooks, such as `beforeEach()`, are also run. Tests
that do not match the pattern are omitted from the test runner output.

Consider the following test file:

```js
test('test 1', async (t) => {
Expand All @@ -325,14 +325,19 @@ test('Test 4', async (t) => {
});
```

Test name patterns can also be specified using regular expression literals. This
allows regular expression flags to be used. In the previous example, starting
Node.js with `--test-name-pattern="/test [4-5]/i"` would match `Test 4` and
`Test 5` because the pattern is case-insensitive.
Using Node.js with the `--test-name-pattern="test [1-3]"` option would
execute `test 1`, `test 2`, and `test 3`. If `test 1` did not match the
pattern, its subtests would not execute, even if they match the pattern.
Alternatively, specifying the pattern multiple times (e.g.,
`--test-name-pattern="test 1"`, `--test-name-pattern="test 2"`, etc.)
achieves the same result.

Regular expression literals can also be used, allowing regular expression flags.
For instance, using `--test-name-pattern="/test [4-5]/i"` would match `Test 4`
and `Test 5` due to the case-insensitive flag.

To match a single test with a pattern, you can prefix it with all its ancestor
test names separated by space, to ensure it is unique.
For example, given the following test file:
To match a single test uniquely, prefix its name with all its ancestor test
names separated by spaces. For example:

```js
describe('test 1', (t) => {
Expand All @@ -344,10 +349,20 @@ describe('test 2', (t) => {
});
```

Starting Node.js with `--test-name-pattern="test 1 some test"` would match
only `some test` in `test 1`.
Using `--test-name-pattern="test 1 some test"` would match only `some test` in `test 1`.

### Using `--test-skip-pattern`

Similarly, the `--test-skip-pattern` option allows skipping tests based on their
names by matching them against the provided pattern, interpreted as JavaScript
regular expressions. If a test's name matches the skip pattern, it will be
excluded from execution.

If both `--test-name-pattern` and `--test-skip-pattern` are
supplied, `--test-name-pattern` will take precedence.

Test name patterns do not change the set of files that the test runner executes.
Test name patterns and skip patterns do not alter the set of files executed by
the test runner.

## Extraneous asynchronous activity

Expand Down Expand Up @@ -3157,6 +3172,7 @@ Can be used to abort test subtasks when the test has been aborted.
[`--test-only`]: cli.md#--test-only
[`--test-reporter-destination`]: cli.md#--test-reporter-destination
[`--test-reporter`]: cli.md#--test-reporter
[`--test-skip-pattern`]: cli.md#--test-name-pattern
[`--test`]: cli.md#--test
[`MockFunctionContext`]: #class-mockfunctioncontext
[`MockTimers`]: #class-mocktimers
Expand Down
5 changes: 4 additions & 1 deletion lib/internal/test_runner/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ function filterExecArgv(arg, i, arr) {
!ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`));
}

function getRunArgs(path, { forceExit, inspectPort, testNamePatterns, only }) {
function getRunArgs(path, { forceExit, inspectPort, testNamePatterns, testSkipPatterns, only }) {
const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
if (forceExit === true) {
ArrayPrototypePush(argv, '--test-force-exit');
Expand All @@ -124,6 +124,9 @@ function getRunArgs(path, { forceExit, inspectPort, testNamePatterns, only }) {
if (testNamePatterns != null) {
ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(argv, `--test-name-pattern=${pattern}`));
}
if (testSkipPatterns != null) {
ArrayPrototypeForEach(testSkipPatterns, (pattern) => ArrayPrototypePush(argv, `--test-skip-pattern=${pattern}`));
}
if (only === true) {
ArrayPrototypePush(argv, '--test-only');
}
Expand Down
39 changes: 24 additions & 15 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ const {
forceExit,
sourceMaps,
testNamePatterns,
testSkipPatterns,
testOnlyFlag,
} = parseCommandLine();
let kResistStopPropagation;
Expand Down Expand Up @@ -300,8 +301,7 @@ class Test extends AsyncResource {
ownAfterEachCount: 0,
};

if ((testNamePatterns !== null && !this.matchesTestNamePatterns()) ||
(testOnlyFlag && !this.only)) {
if (!this.testMatchesCriteria() || (testOnlyFlag && !this.only)) {
this.filtered = true;
this.parent.filteredSubtestCount++;
}
Expand Down Expand Up @@ -408,18 +408,22 @@ class Test extends AsyncResource {
}
}

matchesTestNamePatterns() {
const matchesByNameOrParent = ArrayPrototypeSome(testNamePatterns, (re) =>
RegExpPrototypeExec(re, this.name) !== null,
) ||
this.parent?.matchesTestNamePatterns();
testMatchesCriteria() {
const patterns = testNamePatterns ?? testSkipPatterns;
if (patterns === null) return true;

if (matchesByNameOrParent) return true;
const isSkipping = !testNamePatterns; // Name takes precedence over skip
const matchesByNameOrParent = ArrayPrototypeSome(patterns, (re) =>
RegExpPrototypeExec(re, this.name) !== null,
) || this.parent?.testMatchesCriteria();
if (matchesByNameOrParent) {
return !isSkipping;
}

const testNameWithAncestors = StringPrototypeTrim(this.getTestNameWithAncestors());
if (!testNameWithAncestors) return false;
if (!testNameWithAncestors) return !isSkipping;

return ArrayPrototypeSome(testNamePatterns, (re) => RegExpPrototypeExec(re, testNameWithAncestors) !== null);
return ArrayPrototypeSome(patterns, (re) => RegExpPrototypeExec(re, testNameWithAncestors) !== null) === isSkipping;
}

/**
Expand Down Expand Up @@ -897,7 +901,7 @@ class Test extends AsyncResource {
this.finished = true;

if (this.parent === this.root &&
this.root.waitingOn > this.root.subtests.length) {
this.root.waitingOn > this.root.subtests.length) {
// At this point all of the tests have finished running. However, there
// might be ref'ed handles keeping the event loop alive. This gives the
// global after() hook a chance to clean them up. The user may also
Expand Down Expand Up @@ -987,15 +991,15 @@ class TestHook extends Test {
getRunArgs() {
return this.#args;
}
matchesTestNamePatterns() {
testMatchesCriteria() {
return true;
}
postRun() {
const { error, loc, parentTest: parent } = this;

// Report failures in the root test's after() hook.
if (error && parent !== null &&
parent === parent.root && this.hookType === 'after') {
parent === parent.root && this.hookType === 'after') {

if (isTestFailureError(error)) {
error.failureType = kHookFailure;
Expand All @@ -1016,7 +1020,7 @@ class Suite extends Test {
constructor(options) {
super(options);

if (testNamePatterns !== null && !options.skip) {
if ((testNamePatterns !== null || testSkipPatterns !== null) && !options.skip) {
this.fn = options.fn || this.fn;
this.skipped = false;
}
Expand Down Expand Up @@ -1050,7 +1054,12 @@ class Suite extends Test {
// tests that it contains - in case of children matching patterns.
this.filtered = false;
this.parent.filteredSubtestCount--;
} else if (testOnlyFlag && testNamePatterns == null && this.filteredSubtestCount === this.subtests.length) {
} else if (
testOnlyFlag &&
testNamePatterns == null &&
testSkipPatterns == null &&
this.filteredSubtestCount === this.subtests.length
) {
// If no subtests are marked as "only", run them all
this.filteredSubtestCount = 0;
}
Expand Down
5 changes: 5 additions & 0 deletions lib/internal/test_runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ function parseCommandLine() {
let destinations;
let reporters;
let testNamePatterns;
let testSkipPatterns;
let testOnlyFlag;

if (isChildProcessV8) {
Expand Down Expand Up @@ -240,6 +241,9 @@ function parseCommandLine() {
testNamePatternFlag,
(re) => convertStringToRegExp(re, '--test-name-pattern'),
) : null;
const testSkipPatternFlag = getOptionValue('--test-skip-pattern');
testSkipPatterns = testSkipPatternFlag?.length > 0 ?
ArrayPrototypeMap(testSkipPatternFlag, (re) => convertStringToRegExp(re, '--test-skip-pattern')) : null;
}

globalTestOptions = {
Expand All @@ -250,6 +254,7 @@ function parseCommandLine() {
sourceMaps,
testOnlyFlag,
testNamePatterns,
testSkipPatterns,
reporters,
destinations,
};
Expand Down
3 changes: 3 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"run test at specific shard",
&EnvironmentOptions::test_shard,
kAllowedInEnvvar);
AddOption("--test-skip-pattern",
"run tests whose name don't match this regular expression",
&EnvironmentOptions::test_skip_pattern);
AddOption("--test-udp-no-try-send", "", // For testing only.
&EnvironmentOptions::test_udp_no_try_send);
AddOption("--throw-deprecation",
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class EnvironmentOptions : public Options {
bool test_only = false;
bool test_udp_no_try_send = false;
std::string test_shard;
std::vector<std::string> test_skip_pattern;
bool throw_deprecation = false;
bool trace_atomics_wait = false;
bool trace_deprecation = false;
Expand Down

0 comments on commit 122298b

Please sign in to comment.