Skip to content

Commit c5fd64c

Browse files
cjihrigMoLow
authored andcommitted
feat: add --test-name-pattern CLI flag
This commit adds support for running tests that match a regular expression. Fixes: nodejs/node#42984 (cherry picked from commit 87170c3f9271da947a7b33d0696ec4cf8aab6eb6)
1 parent 1950b38 commit c5fd64c

13 files changed

+335
-6
lines changed

README.md

+35
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,41 @@ test('this test is not run', () => {
228228
})
229229
```
230230

231+
## Filtering tests by name
232+
233+
The [`--test-name-pattern`][] command-line option can be used to only run tests
234+
whose name matches the provided pattern. Test name patterns are interpreted as
235+
JavaScript regular expressions. The `--test-name-pattern` option can be
236+
specified multiple times in order to run nested tests. For each test that is
237+
executed, any corresponding test hooks, such as `beforeEach()`, are also
238+
run.
239+
240+
Given the following test file, starting Node.js with the
241+
`--test-name-pattern="test [1-3]"` option would cause the test runner to execute
242+
`test 1`, `test 2`, and `test 3`. If `test 1` did not match the test name
243+
pattern, then its subtests would not execute, despite matching the pattern. The
244+
same set of tests could also be executed by passing `--test-name-pattern`
245+
multiple times (e.g. `--test-name-pattern="test 1"`,
246+
`--test-name-pattern="test 2"`, etc.).
247+
248+
```js
249+
test('test 1', async (t) => {
250+
await t.test('test 2');
251+
await t.test('test 3');
252+
});
253+
test('Test 4', async (t) => {
254+
await t.test('Test 5');
255+
await t.test('test 6');
256+
});
257+
```
258+
259+
Test name patterns can also be specified using regular expression literals. This
260+
allows regular expression flags to be used. In the previous example, starting
261+
Node.js with `--test-name-pattern="/test [4-5]/i"` would match `Test 4` and
262+
`Test 5` because the pattern is case-insensitive.
263+
264+
Test name patterns do not change the set of files that the test runner executes.
265+
231266
## Extraneous asynchronous activity
232267

233268
Once a test function finishes executing, the TAP results are output as quickly

bin/node--test-name-pattern.js

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
#!/usr/bin/env node
2+
3+
const { argv } = require('#internal/options')
4+
5+
argv['test-name-pattern'] = true
6+
7+
require('./node-core-test.js')

bin/node-core-test.js

+5
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,14 @@ const { argv } = require('#internal/options')
1010

1111
Object.assign(argv, minimist(process.argv.slice(2), {
1212
boolean: ['test', 'test-only'],
13+
string: ['test-name-pattern'],
1314
default: Object.prototype.hasOwnProperty.call(argv, 'test') ? { test: argv.test } : undefined
1415
}))
1516

17+
if (typeof argv['test-name-pattern'] === 'string') {
18+
argv['test-name-pattern'] = [argv['test-name-pattern']]
19+
}
20+
1621
process.argv.splice(1, Infinity, ...argv._)
1722
if (argv.test) {
1823
require('#internal/main/test_runner')

lib/internal/test_runner/test.js

+31-2
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
// https://github.com/nodejs/node/blob/cb7e0c59df10a42cd6930ca7f99d3acee1ce7627/lib/internal/test_runner/test.js
1+
// https://github.com/nodejs/node/blob/87170c3f9271da947a7b33d0696ec4cf8aab6eb6/lib/internal/test_runner/test.js
22

33
'use strict'
44

55
const {
6+
ArrayPrototypeMap,
67
ArrayPrototypePush,
78
ArrayPrototypeReduce,
89
ArrayPrototypeShift,
910
ArrayPrototypeSlice,
11+
ArrayPrototypeSome,
1012
ArrayPrototypeUnshift,
1113
FunctionPrototype,
1214
MathMax,
@@ -15,6 +17,7 @@ const {
1517
PromisePrototypeThen,
1618
PromiseResolve,
1719
ReflectApply,
20+
RegExpPrototypeExec,
1821
SafeMap,
1922
SafeSet,
2023
SafePromiseAll,
@@ -33,7 +36,11 @@ const {
3336
} = require('#internal/errors')
3437
const { getOptionValue } = require('#internal/options')
3538
const { TapStream } = require('#internal/test_runner/tap_stream')
36-
const { createDeferredCallback, isTestFailureError } = require('#internal/test_runner/utils')
39+
const {
40+
convertStringToRegExp,
41+
createDeferredCallback,
42+
isTestFailureError
43+
} = require('#internal/test_runner/utils')
3744
const {
3845
createDeferredPromise,
3946
kEmptyObject
@@ -61,6 +68,15 @@ const kDefaultTimeout = null
6168
const noop = FunctionPrototype
6269
const isTestRunner = getOptionValue('--test')
6370
const testOnlyFlag = !isTestRunner && getOptionValue('--test-only')
71+
const testNamePatternFlag = isTestRunner
72+
? null
73+
: getOptionValue('--test-name-pattern')
74+
const testNamePatterns = testNamePatternFlag?.length > 0
75+
? ArrayPrototypeMap(
76+
testNamePatternFlag,
77+
(re) => convertStringToRegExp(re, '--test-name-pattern')
78+
)
79+
: null
6480
const kShouldAbort = Symbol('kShouldAbort')
6581
const kRunHook = Symbol('kRunHook')
6682
const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach'])
@@ -196,6 +212,18 @@ class Test extends AsyncResource {
196212
this.timeout = timeout
197213
}
198214

215+
if (testNamePatterns !== null) {
216+
// eslint-disable-next-line no-use-before-define
217+
const match = this instanceof TestHook || ArrayPrototypeSome(
218+
testNamePatterns,
219+
(re) => RegExpPrototypeExec(re, name) !== null
220+
)
221+
222+
if (!match) {
223+
skip = 'test name does not match pattern'
224+
}
225+
}
226+
199227
if (testOnlyFlag && !this.only) {
200228
skip = '\'only\' option not set'
201229
}
@@ -673,6 +701,7 @@ class ItTest extends Test {
673701
return { ctx: { signal: this.signal, name: this.name }, args: [] }
674702
}
675703
}
704+
676705
class Suite extends Test {
677706
constructor (options) {
678707
super(options)

lib/internal/test_runner/utils.js

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
// https://github.com/nodejs/node/blob/659dc126932f986fc33c7f1c878cb2b57a1e2fac/lib/internal/test_runner/utils.js
1+
// https://github.com/nodejs/node/blob/87170c3f9271da947a7b33d0696ec4cf8aab6eb6/lib/internal/test_runner/utils.js
22
'use strict'
33
const { RegExpPrototypeExec } = require('#internal/per_context/primordials')
44
const { basename } = require('path')
55
const { createDeferredPromise } = require('#internal/util')
66
const {
77
codes: {
8+
ERR_INVALID_ARG_VALUE,
89
ERR_TEST_FAILURE
910
},
1011
kIsNodeError
1112
} = require('#internal/errors')
1213

1314
const kMultipleCallbackInvocations = 'multipleCallbackInvocations'
15+
const kRegExpPattern = /^\/(.*)\/([a-z]*)$/
1416
const kSupportedFileExtensions = /\.[cm]?js$/
1517
const kTestFilePattern = /((^test(-.+)?)|(.+[.\-_]test))\.[cm]?js$/
1618

@@ -55,7 +57,26 @@ function isTestFailureError (err) {
5557
return err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err
5658
}
5759

60+
function convertStringToRegExp (str, name) {
61+
const match = RegExpPrototypeExec(kRegExpPattern, str)
62+
const pattern = match?.[1] ?? str
63+
const flags = match?.[2] || ''
64+
65+
try {
66+
return new RegExp(pattern, flags)
67+
} catch (err) {
68+
const msg = err?.message
69+
70+
throw new ERR_INVALID_ARG_VALUE(
71+
name,
72+
str,
73+
`is an invalid regular expression.${msg ? ` ${msg}` : ''}`
74+
)
75+
}
76+
}
77+
5878
module.exports = {
79+
convertStringToRegExp,
5980
createDeferredCallback,
6081
doesPathMatchFilter,
6182
isSupportedFileType,

package-lock.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"bin": {
1010
"node--test": "./bin/node--test.js",
1111
"node--test-only": "./bin/node--test-only.js",
12+
"node--test-name-pattern": "./bin/node--test-name-pattern.js",
1213
"test": "./bin/node-core-test.js"
1314
},
1415
"imports": {

test/message.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const binPath = resolve(__dirname, '..', bin.test)
1313
const MESSAGE_FOLDER = join(__dirname, './message/')
1414
const WAIT_FOR_ELLIPSIS = Symbol('wait for ellispis')
1515

16-
const TEST_RUNNER_FLAGS = ['--test', '--test-only']
16+
const TEST_RUNNER_FLAGS = ['--test', '--test-only', '--test-name-pattern']
1717

1818
function readLines (file) {
1919
return createInterface({
@@ -109,8 +109,8 @@ const main = async () => {
109109
)
110110
.toString().split(' ')
111111

112-
const nodeFlags = flags.filter(flag => !TEST_RUNNER_FLAGS.includes(flag)).join(' ')
113-
const testRunnerFlags = flags.filter(flag => TEST_RUNNER_FLAGS.includes(flag)).join(' ')
112+
const nodeFlags = flags.filter(flag => !TEST_RUNNER_FLAGS.find(f => flag.startsWith(f))).join(' ')
113+
const testRunnerFlags = flags.filter(flag => TEST_RUNNER_FLAGS.find(f => flag.startsWith(f))).join(' ')
114114

115115
const command = testRunnerFlags.length
116116
? `${process.execPath} ${nodeFlags} ${binPath} ${testRunnerFlags} ${filePath}`
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// https://github.com/nodejs/node/blob/87170c3f9271da947a7b33d0696ec4cf8aab6eb6/test/message/test_runner_test_name_pattern.js
2+
// Flags: --no-warnings --test-name-pattern=enabled --test-name-pattern=/pattern/i
3+
'use strict'
4+
const common = require('../common')
5+
const {
6+
after,
7+
afterEach,
8+
before,
9+
beforeEach,
10+
describe,
11+
it,
12+
test
13+
} = require('#node:test')
14+
15+
test('top level test disabled', common.mustNotCall())
16+
test('top level skipped test disabled', { skip: true }, common.mustNotCall())
17+
test('top level skipped test enabled', { skip: true }, common.mustNotCall())
18+
it('top level it enabled', common.mustCall())
19+
it('top level it disabled', common.mustNotCall())
20+
it.skip('top level skipped it disabled', common.mustNotCall())
21+
it.skip('top level skipped it enabled', common.mustNotCall())
22+
describe('top level describe disabled', common.mustNotCall())
23+
describe.skip('top level skipped describe disabled', common.mustNotCall())
24+
describe.skip('top level skipped describe enabled', common.mustNotCall())
25+
test('top level runs because name includes PaTtErN', common.mustCall())
26+
27+
test('top level test enabled', common.mustCall(async (t) => {
28+
t.beforeEach(common.mustCall())
29+
t.afterEach(common.mustCall())
30+
await t.test(
31+
'nested test runs because name includes PATTERN',
32+
common.mustCall()
33+
)
34+
}))
35+
36+
describe('top level describe enabled', () => {
37+
before(common.mustCall())
38+
beforeEach(common.mustCall(2))
39+
afterEach(common.mustCall(2))
40+
after(common.mustCall())
41+
42+
it('nested it disabled', common.mustNotCall())
43+
it('nested it enabled', common.mustCall())
44+
describe('nested describe disabled', common.mustNotCall())
45+
describe('nested describe enabled', common.mustCall(() => {
46+
it('is enabled', common.mustCall())
47+
}))
48+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
TAP version 13
2+
# Subtest: top level test disabled
3+
ok 1 - top level test disabled # SKIP test name does not match pattern
4+
---
5+
duration_ms: *
6+
...
7+
# Subtest: top level skipped test disabled
8+
ok 2 - top level skipped test disabled # SKIP test name does not match pattern
9+
---
10+
duration_ms: *
11+
...
12+
# Subtest: top level skipped test enabled
13+
ok 3 - top level skipped test enabled # SKIP
14+
---
15+
duration_ms: *
16+
...
17+
# Subtest: top level it enabled
18+
ok 4 - top level it enabled
19+
---
20+
duration_ms: *
21+
...
22+
# Subtest: top level it disabled
23+
ok 5 - top level it disabled # SKIP test name does not match pattern
24+
---
25+
duration_ms: *
26+
...
27+
# Subtest: top level skipped it disabled
28+
ok 6 - top level skipped it disabled # SKIP test name does not match pattern
29+
---
30+
duration_ms: *
31+
...
32+
# Subtest: top level skipped it enabled
33+
ok 7 - top level skipped it enabled # SKIP
34+
---
35+
duration_ms: *
36+
...
37+
# Subtest: top level describe disabled
38+
ok 8 - top level describe disabled # SKIP test name does not match pattern
39+
---
40+
duration_ms: *
41+
...
42+
# Subtest: top level skipped describe disabled
43+
ok 9 - top level skipped describe disabled # SKIP test name does not match pattern
44+
---
45+
duration_ms: *
46+
...
47+
# Subtest: top level skipped describe enabled
48+
ok 10 - top level skipped describe enabled # SKIP
49+
---
50+
duration_ms: *
51+
...
52+
# Subtest: top level runs because name includes PaTtErN
53+
ok 11 - top level runs because name includes PaTtErN
54+
---
55+
duration_ms: *
56+
...
57+
# Subtest: top level test enabled
58+
# Subtest: nested test runs because name includes PATTERN
59+
ok 1 - nested test runs because name includes PATTERN
60+
---
61+
duration_ms: *
62+
...
63+
1..1
64+
ok 12 - top level test enabled
65+
---
66+
duration_ms: *
67+
...
68+
# Subtest: top level describe enabled
69+
# Subtest: nested it disabled
70+
ok 1 - nested it disabled # SKIP test name does not match pattern
71+
---
72+
duration_ms: *
73+
...
74+
# Subtest: nested it enabled
75+
ok 2 - nested it enabled
76+
---
77+
duration_ms: *
78+
...
79+
# Subtest: nested describe disabled
80+
ok 3 - nested describe disabled # SKIP test name does not match pattern
81+
---
82+
duration_ms: *
83+
...
84+
# Subtest: nested describe enabled
85+
# Subtest: is enabled
86+
ok 1 - is enabled
87+
---
88+
duration_ms: *
89+
...
90+
1..1
91+
ok 4 - nested describe enabled
92+
---
93+
duration_ms: *
94+
...
95+
1..4
96+
ok 13 - top level describe enabled
97+
---
98+
duration_ms: *
99+
...
100+
1..13
101+
# tests 13
102+
# pass 4
103+
# fail 0
104+
# cancelled 0
105+
# skipped 9
106+
# todo 0
107+
# duration_ms *
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// https://github.com/nodejs/node/blob/87170c3f9271da947a7b33d0696ec4cf8aab6eb6/test/message/test_runner_test_name_pattern_with_only.js
2+
// Flags: --no-warnings --test-only --test-name-pattern=enabled
3+
'use strict'
4+
const common = require('../common')
5+
const { test } = require('#node:test')
6+
7+
test('enabled and only', { only: true }, common.mustCall(async (t) => {
8+
await t.test('enabled', common.mustCall())
9+
await t.test('disabled', common.mustNotCall())
10+
}))
11+
12+
test('enabled but not only', common.mustNotCall())
13+
test('only does not match pattern', { only: true }, common.mustNotCall())
14+
test('not only and does not match pattern', common.mustNotCall())

0 commit comments

Comments
 (0)