GeezerVison is showing github.com — test_runner: add --test-name-pattern CLI flag · nodejs/node@d762a34
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/cli.md +11Lines changed: 11 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -1187,6 +1187,16 @@ Starts the Node.js command line test runner. This flag cannot be combined with | 1187 | 1187 | `--check`, `--eval`, `--interactive`, or the inspector. See the documentation | 1188 | 1188 | on [running tests from the command line][] for more details. | 1189 | 1189 |
| | 1190 | +### `--test-name-pattern` | | 1191 | + | | 1192 | +<!-- YAML | | 1193 | +added: REPLACEME | | 1194 | +--> | | 1195 | + | | 1196 | +A regular expression that configures the test runner to only execute tests | | 1197 | +whose name matches the provided pattern. See the documentation on | | 1198 | +[filtering tests by name][] for more details. | | 1199 | + | 1190 | 1200 | ### `--test-only` | 1191 | 1201 |
| 1192 | 1202 | <!-- YAML | @@ -2262,6 +2272,7 @@ done | 2262 | 2272 | [debugger]: debugger.md | 2263 | 2273 | [debugging security implications]: https://nodejs.org/en/docs/guides/debugging-getting-started/#security-implications | 2264 | 2274 | [emit_warning]: process.md#processemitwarningwarning-options | | 2275 | +[filtering tests by name]: test.md#filtering-tests-by-name | 2265 | 2276 | [jitless]: https://v8.dev/blog/jitless | 2266 | 2277 | [libuv threadpool documentation]: https://docs.libuv.org/en/latest/threadpool.html | 2267 | 2278 | [remote code execution]: https://www.owasp.org/index.php/Code_Injection | |
Collapse file Copy file name to clipboardExpand all lines: doc/api/test.md +37Lines changed: 37 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -220,6 +220,42 @@ test('this test is not run', () => { | 220 | 220 | }); | 221 | 221 | ``` | 222 | 222 |
| | 223 | +## Filtering tests by name | | 224 | + | | 225 | +The [`--test-name-pattern`][] command-line option can be used to only run tests | | 226 | +whose name matches the provided pattern. Test name patterns are interpreted as | | 227 | +JavaScript regular expressions. The `--test-name-pattern` option can be | | 228 | +specified multiple times in order to run nested tests. For each test that is | | 229 | +executed, any corresponding test hooks, such as `beforeEach()`, are also | | 230 | +run. | | 231 | + | | 232 | +Given the following test file, starting Node.js with the | | 233 | +`--test-name-pattern="test [1-3]"` option would cause the test runner to execute | | 234 | +`test 1`, `test 2`, and `test 3`. If `test 1` did not match the test name | | 235 | +pattern, then its subtests would not execute, despite matching the pattern. The | | 236 | +same set of tests could also be executed by passing `--test-name-pattern` | | 237 | +multiple times (e.g. `--test-name-pattern="test 1"`, | | 238 | +`--test-name-pattern="test 2"`, etc.). | | 239 | + | | 240 | +```js | | 241 | +test('test 1', async (t) => { | | 242 | + await t.test('test 2'); | | 243 | + await t.test('test 3'); | | 244 | +}); | | 245 | + | | 246 | +test('Test 4', async (t) => { | | 247 | + await t.test('Test 5'); | | 248 | + await t.test('test 6'); | | 249 | +}); | | 250 | +``` | | 251 | + | | 252 | +Test name patterns can also be specified using regular expression literals. This | | 253 | +allows regular expression flags to be used. In the previous example, starting | | 254 | +Node.js with `--test-name-pattern="/test [4-5]/i"` would match `Test 4` and | | 255 | +`Test 5` because the pattern is case-insensitive. | | 256 | + | | 257 | +Test name patterns do not change the set of files that the test runner executes. | | 258 | + | 223 | 259 | ## Extraneous asynchronous activity | 224 | 260 |
| 225 | 261 | Once a test function finishes executing, the TAP results are output as quickly | @@ -896,6 +932,7 @@ added: v18.7.0 | 896 | 932 | aborted. | 897 | 933 |
| 898 | 934 | [TAP]: https://testanything.org/ | | 935 | +[`--test-name-pattern`]: cli.md#--test-name-pattern | 899 | 936 | [`--test-only`]: cli.md#--test-only | 900 | 937 | [`--test`]: cli.md#--test | 901 | 938 | [`SuiteContext`]: #class-suitecontext | |
Collapse file Copy file name to clipboardExpand all lines: doc/node.1 +4Lines changed: 4 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -387,6 +387,10 @@ Specify the minimum allocation from the OpenSSL secure heap. The default is 2. T | 387 | 387 | .It Fl -test | 388 | 388 | Starts the Node.js command line test runner. | 389 | 389 | . | | 390 | +.It Fl -test-name-pattern | | 391 | +A regular expression that configures the test runner to only execute tests | | 392 | +whose name matches the provided pattern. | | 393 | +. | 390 | 394 | .It Fl -test-only | 391 | 395 | Configures the test runner to only execute top level tests that have the `only` | 392 | 396 | option set. | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/test.js +28-3Lines changed: 28 additions & 3 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | 2 | const { | | 3 | + ArrayPrototypeMap, | 3 | 4 | ArrayPrototypePush, | 4 | 5 | ArrayPrototypeReduce, | 5 | 6 | ArrayPrototypeShift, | 6 | 7 | ArrayPrototypeSlice, | | 8 | + ArrayPrototypeSome, | 7 | 9 | ArrayPrototypeUnshift, | 8 | 10 | FunctionPrototype, | 9 | 11 | MathMax, | @@ -12,6 +14,7 @@ const { | 12 | 14 | PromisePrototypeThen, | 13 | 15 | PromiseResolve, | 14 | 16 | ReflectApply, | | 17 | + RegExpPrototypeExec, | 15 | 18 | SafeMap, | 16 | 19 | SafeSet, | 17 | 20 | SafePromiseAll, | @@ -30,7 +33,11 @@ const { | 30 | 33 | } = require('internal/errors'); | 31 | 34 | const { getOptionValue } = require('internal/options'); | 32 | 35 | const { TapStream } = require('internal/test_runner/tap_stream'); | 33 | | -const { createDeferredCallback, isTestFailureError } = require('internal/test_runner/utils'); | | 36 | +const { | | 37 | + convertStringToRegExp, | | 38 | + createDeferredCallback, | | 39 | + isTestFailureError, | | 40 | +} = require('internal/test_runner/utils'); | 34 | 41 | const { | 35 | 42 | createDeferredPromise, | 36 | 43 | kEmptyObject, | @@ -58,6 +65,13 @@ const kDefaultTimeout = null; | 58 | 65 | const noop = FunctionPrototype; | 59 | 66 | const isTestRunner = getOptionValue('--test'); | 60 | 67 | const testOnlyFlag = !isTestRunner && getOptionValue('--test-only'); | | 68 | +const testNamePatternFlag = isTestRunner ? null : | | 69 | + getOptionValue('--test-name-pattern'); | | 70 | +const testNamePatterns = testNamePatternFlag?.length > 0 ? | | 71 | + ArrayPrototypeMap( | | 72 | + testNamePatternFlag, | | 73 | + (re) => convertStringToRegExp(re, '--test-name-pattern') | | 74 | + ) : null; | 61 | 75 | const kShouldAbort = Symbol('kShouldAbort'); | 62 | 76 | const kRunHook = Symbol('kRunHook'); | 63 | 77 | const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']); | @@ -195,6 +209,18 @@ class Test extends AsyncResource { | 195 | 209 | this.timeout = timeout; | 196 | 210 | } | 197 | 211 |
| | 212 | + if (testNamePatterns !== null) { | | 213 | + // eslint-disable-next-line no-use-before-define | | 214 | + const match = this instanceof TestHook || ArrayPrototypeSome( | | 215 | + testNamePatterns, | | 216 | + (re) => RegExpPrototypeExec(re, name) !== null | | 217 | + ); | | 218 | + | | 219 | + if (!match) { | | 220 | + skip = 'test name does not match pattern'; | | 221 | + } | | 222 | + } | | 223 | + | 198 | 224 | if (testOnlyFlag && !this.only) { | 199 | 225 | skip = '\'only\' option not set'; | 200 | 226 | } | @@ -210,7 +236,6 @@ class Test extends AsyncResource { | 210 | 236 | validateAbortSignal(signal, 'options.signal'); | 211 | 237 | this.#outerSignal?.addEventListener('abort', this.#abortHandler); | 212 | 238 |
| 213 | | - | 214 | 239 | this.fn = fn; | 215 | 240 | this.name = name; | 216 | 241 | this.parent = parent; | @@ -669,6 +694,7 @@ class ItTest extends Test { | 669 | 694 | return { ctx: { signal: this.signal, name: this.name }, args: [] }; | 670 | 695 | } | 671 | 696 | } | | 697 | + | 672 | 698 | class Suite extends Test { | 673 | 699 | constructor(options) { | 674 | 700 | super(options); | @@ -704,7 +730,6 @@ class Suite extends Test { | 704 | 730 | return; | 705 | 731 | } | 706 | 732 |
| 707 | | - | 708 | 733 | const hookArgs = this.getRunArgs(); | 709 | 734 | await this[kRunHook]('before', hookArgs); | 710 | 735 | const stopPromise = stopTest(this.timeout, this.signal); | |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/test_runner/utils.js +22-1Lines changed: 22 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
| 1 | 1 | 'use strict'; | 2 | | -const { RegExpPrototypeExec } = primordials; | | 2 | +const { RegExp, RegExpPrototypeExec } = primordials; | 3 | 3 | const { basename } = require('path'); | 4 | 4 | const { createDeferredPromise } = require('internal/util'); | 5 | 5 | const { | 6 | 6 | codes: { | | 7 | + ERR_INVALID_ARG_VALUE, | 7 | 8 | ERR_TEST_FAILURE, | 8 | 9 | }, | 9 | 10 | kIsNodeError, | 10 | 11 | } = require('internal/errors'); | 11 | 12 |
| 12 | 13 | const kMultipleCallbackInvocations = 'multipleCallbackInvocations'; | | 14 | +const kRegExpPattern = /^\/(.*)\/([a-z]*)$/; | 13 | 15 | const kSupportedFileExtensions = /\.[cm]?js$/; | 14 | 16 | const kTestFilePattern = /((^test(-.+)?)|(.+[.\-_]test))\.[cm]?js$/; | 15 | 17 |
| @@ -54,7 +56,26 @@ function isTestFailureError(err) { | 54 | 56 | return err?.code === 'ERR_TEST_FAILURE' && kIsNodeError in err; | 55 | 57 | } | 56 | 58 |
| | 59 | +function convertStringToRegExp(str, name) { | | 60 | + const match = RegExpPrototypeExec(kRegExpPattern, str); | | 61 | + const pattern = match?.[1] ?? str; | | 62 | + const flags = match?.[2] || ''; | | 63 | + | | 64 | + try { | | 65 | + return new RegExp(pattern, flags); | | 66 | + } catch (err) { | | 67 | + const msg = err?.message; | | 68 | + | | 69 | + throw new ERR_INVALID_ARG_VALUE( | | 70 | + name, | | 71 | + str, | | 72 | + `is an invalid regular expression.${msg ? ` ${msg}` : ''}` | | 73 | + ); | | 74 | + } | | 75 | +} | | 76 | + | 57 | 77 | module.exports = { | | 78 | + convertStringToRegExp, | 58 | 79 | createDeferredCallback, | 59 | 80 | doesPathMatchFilter, | 60 | 81 | isSupportedFileType, | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.cc +3Lines changed: 3 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -553,6 +553,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() { | 553 | 553 | AddOption("--test", | 554 | 554 | "launch test runner on startup", | 555 | 555 | &EnvironmentOptions::test_runner); | | 556 | + AddOption("--test-name-pattern", | | 557 | + "run tests whose name matches this regular expression", | | 558 | + &EnvironmentOptions::test_name_pattern); | 556 | 559 | AddOption("--test-only", | 557 | 560 | "run tests with 'only' option set", | 558 | 561 | &EnvironmentOptions::test_only, | |
Collapse file Copy file name to clipboardExpand all lines: src/node_options.h +1Lines changed: 1 addition & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -153,6 +153,7 @@ class EnvironmentOptions : public Options { | 153 | 153 | std::string redirect_warnings; | 154 | 154 | std::string diagnostic_dir; | 155 | 155 | bool test_runner = false; | | 156 | + std::vector<std::string> test_name_pattern; | 156 | 157 | bool test_only = false; | 157 | 158 | bool test_udp_no_try_send = false; | 158 | 159 | bool throw_deprecation = false; | |
Collapse file Copy file name to clipboard +47Lines changed: 47 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +// Flags: --no-warnings --test-name-pattern=enabled --test-name-pattern=/pattern/i | | 2 | +'use strict'; | | 3 | +const common = require('../common'); | | 4 | +const { | | 5 | + after, | | 6 | + afterEach, | | 7 | + before, | | 8 | + beforeEach, | | 9 | + describe, | | 10 | + it, | | 11 | + test, | | 12 | +} = require('node:test'); | | 13 | + | | 14 | +test('top level test disabled', common.mustNotCall()); | | 15 | +test('top level skipped test disabled', { skip: true }, common.mustNotCall()); | | 16 | +test('top level skipped test enabled', { skip: true }, common.mustNotCall()); | | 17 | +it('top level it enabled', common.mustCall()); | | 18 | +it('top level it disabled', common.mustNotCall()); | | 19 | +it.skip('top level skipped it disabled', common.mustNotCall()); | | 20 | +it.skip('top level skipped it enabled', common.mustNotCall()); | | 21 | +describe('top level describe disabled', common.mustNotCall()); | | 22 | +describe.skip('top level skipped describe disabled', common.mustNotCall()); | | 23 | +describe.skip('top level skipped describe enabled', common.mustNotCall()); | | 24 | +test('top level runs because name includes PaTtErN', common.mustCall()); | | 25 | + | | 26 | +test('top level test enabled', common.mustCall(async (t) => { | | 27 | + t.beforeEach(common.mustCall()); | | 28 | + t.afterEach(common.mustCall()); | | 29 | + await t.test( | | 30 | + 'nested test runs because name includes PATTERN', | | 31 | + common.mustCall() | | 32 | + ); | | 33 | +})); | | 34 | + | | 35 | +describe('top level describe enabled', () => { | | 36 | + before(common.mustCall()); | | 37 | + beforeEach(common.mustCall(2)); | | 38 | + afterEach(common.mustCall(2)); | | 39 | + after(common.mustCall()); | | 40 | + | | 41 | + it('nested it disabled', common.mustNotCall()); | | 42 | + it('nested it enabled', common.mustCall()); | | 43 | + describe('nested describe disabled', common.mustNotCall()); | | 44 | + describe('nested describe enabled', common.mustCall(() => { | | 45 | + it('is enabled', common.mustCall()); | | 46 | + })); | | 47 | +}); |
Collapse file Copy file name to clipboard +107Lines changed: 107 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 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 * |
|