Skip to content

Commit 54819f0

Browse files
authored
test_runner: support 'only' tests
This commit introduces a CLI flag and test runner functionality to support running a subset of tests that are indicated by an 'only' option passed to the test. PR-URL: #42514 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
1 parent 0c9273d commit 54819f0

10 files changed

+283
-14
lines changed

doc/api/cli.md

+10
Original file line numberDiff line numberDiff line change
@@ -1052,6 +1052,15 @@ minimum allocation from the secure heap. The minimum value is `2`.
10521052
The maximum value is the lesser of `--secure-heap` or `2147483647`.
10531053
The value given must be a power of two.
10541054

1055+
### `--test-only`
1056+
1057+
<!-- YAML
1058+
added: REPLACEME
1059+
-->
1060+
1061+
Configures the test runner to only execute top level tests that have the `only`
1062+
option set.
1063+
10551064
### `--throw-deprecation`
10561065

10571066
<!-- YAML
@@ -1641,6 +1650,7 @@ Node.js options that are allowed are:
16411650
* `--require`, `-r`
16421651
* `--secure-heap-min`
16431652
* `--secure-heap`
1653+
* `--test-only`
16441654
* `--throw-deprecation`
16451655
* `--title`
16461656
* `--tls-cipher-list`

doc/api/test.md

+56
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,42 @@ test('skip() method with message', (t) => {
148148
});
149149
```
150150

151+
### `only` tests
152+
153+
If Node.js is started with the [`--test-only`][] command-line option, it is
154+
possible to skip all top level tests except for a selected subset by passing
155+
the `only` option to the tests that should be run. When a test with the `only`
156+
option set is run, all subtests are also run. The test context's `runOnly()`
157+
method can be used to implement the same behavior at the subtest level.
158+
159+
```js
160+
// Assume Node.js is run with the --test-only command-line option.
161+
// The 'only' option is set, so this test is run.
162+
test('this test is run', { only: true }, async (t) => {
163+
// Within this test, all subtests are run by default.
164+
await t.test('running subtest');
165+
166+
// The test context can be updated to run subtests with the 'only' option.
167+
t.runOnly(true);
168+
await t.test('this subtest is now skipped');
169+
await t.test('this subtest is run', { only: true });
170+
171+
// Switch the context back to execute all tests.
172+
t.runOnly(false);
173+
await t.test('this subtest is now run');
174+
175+
// Explicitly do not run these tests.
176+
await t.test('skipped subtest 3', { only: false });
177+
await t.test('skipped subtest 4', { skip: true });
178+
});
179+
180+
// The 'only' option is not set, so this test is skipped.
181+
test('this test is not run', () => {
182+
// This code is not run.
183+
throw new Error('fail');
184+
});
185+
```
186+
151187
## Extraneous asynchronous activity
152188

153189
Once a test function finishes executing, the TAP results are output as quickly
@@ -197,6 +233,9 @@ added: REPLACEME
197233
* `concurrency` {number} The number of tests that can be run at the same time.
198234
If unspecified, subtests inherit this value from their parent.
199235
**Default:** `1`.
236+
* `only` {boolean} If truthy, and the test context is configured to run
237+
`only` tests, then this test will be run. Otherwise, the test is skipped.
238+
**Default:** `false`.
200239
* `skip` {boolean|string} If truthy, the test is skipped. If a string is
201240
provided, that string is displayed in the test results as the reason for
202241
skipping the test. **Default:** `false`.
@@ -257,6 +296,19 @@ This function is used to write TAP diagnostics to the output. Any diagnostic
257296
information is included at the end of the test's results. This function does
258297
not return a value.
259298

299+
### `context.runOnly(shouldRunOnlyTests)`
300+
301+
<!-- YAML
302+
added: REPLACEME
303+
-->
304+
305+
* `shouldRunOnlyTests` {boolean} Whether or not to run `only` tests.
306+
307+
If `shouldRunOnlyTests` is truthy, the test context will only run tests that
308+
have the `only` option set. Otherwise, all tests are run. If Node.js was not
309+
started with the [`--test-only`][] command-line option, this function is a
310+
no-op.
311+
260312
### `context.skip([message])`
261313

262314
<!-- YAML
@@ -296,6 +348,9 @@ added: REPLACEME
296348
* `concurrency` {number} The number of tests that can be run at the same time.
297349
If unspecified, subtests inherit this value from their parent.
298350
**Default:** `1`.
351+
* `only` {boolean} If truthy, and the test context is configured to run
352+
`only` tests, then this test will be run. Otherwise, the test is skipped.
353+
**Default:** `false`.
299354
* `skip` {boolean|string} If truthy, the test is skipped. If a string is
300355
provided, that string is displayed in the test results as the reason for
301356
skipping the test. **Default:** `false`.
@@ -312,5 +367,6 @@ This function is used to create subtests under the current test. This function
312367
behaves in the same fashion as the top level [`test()`][] function.
313368

314369
[TAP]: https://testanything.org/
370+
[`--test-only`]: cli.md#--test-only
315371
[`TestContext`]: #class-testcontext
316372
[`test()`]: #testname-options-fn

doc/node.1

+4
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,10 @@ the secure heap. The default is 0. The value must be a power of two.
381381
.It Fl -secure-heap-min Ns = Ns Ar n
382382
Specify the minimum allocation from the OpenSSL secure heap. The default is 2. The value must be a power of two.
383383
.
384+
.It Fl -test-only
385+
Configures the test runner to only execute top level tests that have the `only`
386+
option set.
387+
.
384388
.It Fl -throw-deprecation
385389
Throw errors for deprecations.
386390
.

lib/internal/test_runner/test.js

+23-10
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const {
1313
ERR_TEST_FAILURE,
1414
},
1515
} = require('internal/errors');
16+
const { getOptionValue } = require('internal/options');
1617
const { TapStream } = require('internal/test_runner/tap_stream');
1718
const { createDeferredPromise } = require('internal/util');
1819
const { isPromise } = require('internal/util/types');
@@ -26,6 +27,7 @@ const kSubtestsFailed = 'subtestsFailed';
2627
const kTestCodeFailure = 'testCodeFailure';
2728
const kDefaultIndent = ' ';
2829
const noop = FunctionPrototype;
30+
const testOnlyFlag = getOptionValue('--test-only');
2931

3032
class TestContext {
3133
#test;
@@ -38,6 +40,10 @@ class TestContext {
3840
this.#test.diagnostic(message);
3941
}
4042

43+
runOnly(value) {
44+
this.#test.runOnlySubtests = !!value;
45+
}
46+
4147
skip(message) {
4248
this.#test.skip(message);
4349
}
@@ -57,8 +63,8 @@ class Test extends AsyncResource {
5763
constructor(options) {
5864
super('Test');
5965

60-
let { fn, name, parent } = options;
61-
const { concurrency, skip, todo } = options;
66+
let { fn, name, parent, skip } = options;
67+
const { concurrency, only, todo } = options;
6268

6369
if (typeof fn !== 'function') {
6470
fn = noop;
@@ -72,19 +78,13 @@ class Test extends AsyncResource {
7278
parent = null;
7379
}
7480

75-
if (skip) {
76-
fn = noop;
77-
}
78-
79-
this.fn = fn;
80-
this.name = name;
81-
this.parent = parent;
82-
8381
if (parent === null) {
8482
this.concurrency = 1;
8583
this.indent = '';
8684
this.indentString = kDefaultIndent;
85+
this.only = testOnlyFlag;
8786
this.reporter = new TapStream();
87+
this.runOnlySubtests = this.only;
8888
this.testNumber = 0;
8989
} else {
9090
const indent = parent.parent === null ? parent.indent :
@@ -93,14 +93,27 @@ class Test extends AsyncResource {
9393
this.concurrency = parent.concurrency;
9494
this.indent = indent;
9595
this.indentString = parent.indentString;
96+
this.only = only ?? !parent.runOnlySubtests;
9697
this.reporter = parent.reporter;
98+
this.runOnlySubtests = !this.only;
9799
this.testNumber = parent.subtests.length + 1;
98100
}
99101

100102
if (isUint32(concurrency) && concurrency !== 0) {
101103
this.concurrency = concurrency;
102104
}
103105

106+
if (testOnlyFlag && !this.only) {
107+
skip = '\'only\' option not set';
108+
}
109+
110+
if (skip) {
111+
fn = noop;
112+
}
113+
114+
this.fn = fn;
115+
this.name = name;
116+
this.parent = parent;
104117
this.cancelled = false;
105118
this.skipped = !!skip;
106119
this.isTodo = !!todo;

src/node_options.cc

+4
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
498498
"write warnings to file instead of stderr",
499499
&EnvironmentOptions::redirect_warnings,
500500
kAllowedInEnvironment);
501+
AddOption("--test-only",
502+
"run tests with 'only' option set",
503+
&EnvironmentOptions::test_only,
504+
kAllowedInEnvironment);
501505
AddOption("--test-udp-no-try-send", "", // For testing only.
502506
&EnvironmentOptions::test_udp_no_try_send);
503507
AddOption("--throw-deprecation",

src/node_options.h

+1
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ class EnvironmentOptions : public Options {
148148
#endif // HAVE_INSPECTOR
149149
std::string redirect_warnings;
150150
std::string diagnostic_dir;
151+
bool test_only = false;
151152
bool test_udp_no_try_send = false;
152153
bool throw_deprecation = false;
153154
bool trace_atomics_wait = false;
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Flags: --no-warnings --test-only
2+
'use strict';
3+
require('../common');
4+
const test = require('node:test');
5+
6+
// These tests should be skipped based on the 'only' option.
7+
test('only = undefined');
8+
test('only = undefined, skip = string', { skip: 'skip message' });
9+
test('only = undefined, skip = true', { skip: true });
10+
test('only = undefined, skip = false', { skip: false });
11+
test('only = false', { only: false });
12+
test('only = false, skip = string', { only: false, skip: 'skip message' });
13+
test('only = false, skip = true', { only: false, skip: true });
14+
test('only = false, skip = false', { only: false, skip: false });
15+
16+
// These tests should be skipped based on the 'skip' option.
17+
test('only = true, skip = string', { only: true, skip: 'skip message' });
18+
test('only = true, skip = true', { only: true, skip: true });
19+
20+
// An 'only' test with subtests.
21+
test('only = true, with subtests', { only: true }, async (t) => {
22+
// These subtests should run.
23+
await t.test('running subtest 1');
24+
await t.test('running subtest 2');
25+
26+
// Switch the context to only execute 'only' tests.
27+
t.runOnly(true);
28+
await t.test('skipped subtest 1');
29+
await t.test('skipped subtest 2');
30+
await t.test('running subtest 3', { only: true });
31+
32+
// Switch the context back to execute all tests.
33+
t.runOnly(false);
34+
await t.test('running subtest 4', async (t) => {
35+
// These subtests should run.
36+
await t.test('running sub-subtest 1');
37+
await t.test('running sub-subtest 2');
38+
39+
// Switch the context to only execute 'only' tests.
40+
t.runOnly(true);
41+
await t.test('skipped sub-subtest 1');
42+
await t.test('skipped sub-subtest 2');
43+
});
44+
45+
// Explicitly do not run these tests.
46+
await t.test('skipped subtest 3', { only: false });
47+
await t.test('skipped subtest 4', { skip: true });
48+
});
+102
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
TAP version 13
2+
ok 1 - only = undefined # SKIP 'only' option not set
3+
---
4+
duration_ms: *
5+
...
6+
ok 2 - only = undefined, skip = string # SKIP 'only' option not set
7+
---
8+
duration_ms: *
9+
...
10+
ok 3 - only = undefined, skip = true # SKIP 'only' option not set
11+
---
12+
duration_ms: *
13+
...
14+
ok 4 - only = undefined, skip = false # SKIP 'only' option not set
15+
---
16+
duration_ms: *
17+
...
18+
ok 5 - only = false # SKIP 'only' option not set
19+
---
20+
duration_ms: *
21+
...
22+
ok 6 - only = false, skip = string # SKIP 'only' option not set
23+
---
24+
duration_ms: *
25+
...
26+
ok 7 - only = false, skip = true # SKIP 'only' option not set
27+
---
28+
duration_ms: *
29+
...
30+
ok 8 - only = false, skip = false # SKIP 'only' option not set
31+
---
32+
duration_ms: *
33+
...
34+
ok 9 - only = true, skip = string # SKIP skip message
35+
---
36+
duration_ms: *
37+
...
38+
ok 10 - only = true, skip = true # SKIP
39+
---
40+
duration_ms: *
41+
...
42+
ok 1 - running subtest 1
43+
---
44+
duration_ms: *
45+
...
46+
ok 2 - running subtest 2
47+
---
48+
duration_ms: *
49+
...
50+
ok 3 - skipped subtest 1 # SKIP 'only' option not set
51+
---
52+
duration_ms: *
53+
...
54+
ok 4 - skipped subtest 2 # SKIP 'only' option not set
55+
---
56+
duration_ms: *
57+
...
58+
ok 5 - running subtest 3
59+
---
60+
duration_ms: *
61+
...
62+
ok 1 - running sub-subtest 1
63+
---
64+
duration_ms: *
65+
...
66+
ok 2 - running sub-subtest 2
67+
---
68+
duration_ms: *
69+
...
70+
ok 3 - skipped sub-subtest 1 # SKIP 'only' option not set
71+
---
72+
duration_ms: *
73+
...
74+
ok 4 - skipped sub-subtest 2 # SKIP 'only' option not set
75+
---
76+
duration_ms: *
77+
...
78+
1..4
79+
ok 6 - running subtest 4
80+
---
81+
duration_ms: *
82+
...
83+
ok 7 - skipped subtest 3 # SKIP 'only' option not set
84+
---
85+
duration_ms: *
86+
...
87+
ok 8 - skipped subtest 4 # SKIP
88+
---
89+
duration_ms: *
90+
...
91+
1..8
92+
ok 11 - only = true, with subtests
93+
---
94+
duration_ms: *
95+
...
96+
1..11
97+
# tests 11
98+
# pass 1
99+
# fail 0
100+
# skipped 10
101+
# todo 0
102+
# duration_ms *

test/message/test_runner_output.js

+10
Original file line numberDiff line numberDiff line change
@@ -286,3 +286,13 @@ test('callback async throw after done', (t, done) => {
286286

287287
done();
288288
});
289+
290+
test('only is set but not in only mode', { only: true }, async (t) => {
291+
// All of these subtests should run.
292+
await t.test('running subtest 1');
293+
t.runOnly(true);
294+
await t.test('running subtest 2');
295+
await t.test('running subtest 3', { only: true });
296+
t.runOnly(false);
297+
await t.test('running subtest 4');
298+
});

0 commit comments

Comments
 (0)