Skip to content

Commit ac9e5e7

Browse files
authored
test_runner: improve describe.only behavior
PR-URL: #52296 Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Benjamin Gruenbaum <[email protected]> Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: Trivikram Kamat <[email protected]>
1 parent 2c024cd commit ac9e5e7

File tree

4 files changed

+193
-85
lines changed

4 files changed

+193
-85
lines changed

doc/api/test.md

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -230,15 +230,24 @@ const { describe, it } = require('node:test');
230230
## `only` tests
231231

232232
If Node.js is started with the [`--test-only`][] command-line option, it is
233-
possible to skip all top level tests except for a selected subset by passing
234-
the `only` option to the tests that should be run. When a test with the `only`
235-
option set is run, all subtests are also run. The test context's `runOnly()`
233+
possible to skip all tests except for a selected subset by passing
234+
the `only` option to the tests that should run. When a test with the `only`
235+
option is set, all subtests are also run.
236+
If a suite has the `only` option set, all tests within the suite are run,
237+
unless it has descendants with the `only` option set, in which case only those
238+
tests are run.
239+
240+
When using [subtests][] within a `test()`/`it()`, it is required to mark
241+
all ancestor tests with the `only` option to run only a
242+
selected subset of tests.
243+
244+
The test context's `runOnly()`
236245
method can be used to implement the same behavior at the subtest level. Tests
237246
that are not executed are omitted from the test runner output.
238247

239248
```js
240249
// Assume Node.js is run with the --test-only command-line option.
241-
// The 'only' option is set, so this test is run.
250+
// The suite's 'only' option is set, so these tests are run.
242251
test('this test is run', { only: true }, async (t) => {
243252
// Within this test, all subtests are run by default.
244253
await t.test('running subtest');
@@ -262,6 +271,29 @@ test('this test is not run', () => {
262271
// This code is not run.
263272
throw new Error('fail');
264273
});
274+
275+
describe('a suite', () => {
276+
// The 'only' option is set, so this test is run.
277+
it('this test is run', { only: true }, () => {
278+
// This code is run.
279+
});
280+
281+
it('this test is not run', () => {
282+
// This code is not run.
283+
throw new Error('fail');
284+
});
285+
});
286+
287+
describe.only('a suite', () => {
288+
// The 'only' option is set, so this test is run.
289+
it('this test is run', () => {
290+
// This code is run.
291+
});
292+
293+
it('this test is run', () => {
294+
// This code is run.
295+
});
296+
});
265297
```
266298

267299
## Filtering tests by name
@@ -3145,6 +3177,7 @@ Can be used to abort test subtasks when the test has been aborted.
31453177
[describe options]: #describename-options-fn
31463178
[it options]: #testname-options-fn
31473179
[stream.compose]: stream.md#streamcomposestreams
3180+
[subtests]: #subtests
31483181
[suite options]: #suitename-options-fn
31493182
[test reporters]: #test-reporters
31503183
[test runner execution model]: #test-runner-execution-model

lib/internal/test_runner/test.js

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -239,8 +239,8 @@ class Test extends AsyncResource {
239239
constructor(options) {
240240
super('Test');
241241

242-
let { fn, name, parent, skip } = options;
243-
const { concurrency, loc, only, timeout, todo, signal } = options;
242+
let { fn, name, parent } = options;
243+
const { concurrency, loc, only, timeout, todo, skip, signal } = options;
244244

245245
if (typeof fn !== 'function') {
246246
fn = noop;
@@ -301,10 +301,13 @@ class Test extends AsyncResource {
301301

302302
if ((testNamePatterns !== null && !this.matchesTestNamePatterns()) ||
303303
(testOnlyFlag && !this.only)) {
304-
skip = true;
305304
this.filtered = true;
306305
this.parent.filteredSubtestCount++;
307306
}
307+
308+
if (testOnlyFlag && only === false) {
309+
fn = noop;
310+
}
308311
}
309312

310313
switch (typeof concurrency) {
@@ -605,8 +608,12 @@ class Test extends AsyncResource {
605608
ArrayPrototypePush(this.diagnostics, message);
606609
}
607610

611+
get shouldFilter() {
612+
return this.filtered && this.parent?.filteredSubtestCount > 0;
613+
}
614+
608615
start() {
609-
if (this.filtered) {
616+
if (this.shouldFilter) {
610617
noopTestStream ??= new TestsStream();
611618
this.reporter = noopTestStream;
612619
this.run = this.filteredRun;
@@ -814,7 +821,7 @@ class Test extends AsyncResource {
814821
this.mock?.reset();
815822

816823
if (this.parent !== null) {
817-
if (!this.filtered) {
824+
if (!this.shouldFilter) {
818825
const report = this.getReportDetails();
819826
report.details.passed = this.passed;
820827
this.testNumber ||= ++this.parent.outputSubtestCount;
@@ -1027,23 +1034,27 @@ class Suite extends Test {
10271034
(err) => {
10281035
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
10291036
}),
1030-
() => {
1031-
this.buildPhaseFinished = true;
1032-
1033-
// A suite can transition from filtered to unfiltered based on the
1034-
// tests that it contains.
1035-
if (this.filtered && this.filteredSubtestCount !== this.subtests.length) {
1036-
this.filtered = false;
1037-
this.parent.filteredSubtestCount--;
1038-
}
1039-
},
1037+
() => this.postBuild(),
10401038
);
10411039
} catch (err) {
10421040
this.fail(new ERR_TEST_FAILURE(err, kTestCodeFailure));
10431041

10441042
this.buildPhaseFinished = true;
10451043
}
1046-
this.fn = () => {};
1044+
this.fn = noop;
1045+
}
1046+
1047+
postBuild() {
1048+
this.buildPhaseFinished = true;
1049+
if (this.filtered && this.filteredSubtestCount !== this.subtests.length) {
1050+
// A suite can transition from filtered to unfiltered based on the
1051+
// tests that it contains - in case of children matching patterns.
1052+
this.filtered = false;
1053+
this.parent.filteredSubtestCount--;
1054+
} else if (testOnlyFlag && testNamePatterns == null && this.filteredSubtestCount === this.subtests.length) {
1055+
// If no subtests are marked as "only", run them all
1056+
this.filteredSubtestCount = 0;
1057+
}
10471058
}
10481059

10491060
getRunArgs() {
Lines changed: 82 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,121 @@
11
// Flags: --test-only
22
'use strict';
3-
require('../../../common');
3+
const common = require('../../../common');
44
const { test, describe, it } = require('node:test');
55

66
// 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 });
7+
test('only = undefined', common.mustNotCall());
8+
test('only = undefined, skip = string', { skip: 'skip message' }, common.mustNotCall());
9+
test('only = undefined, skip = true', { skip: true }, common.mustNotCall());
10+
test('only = undefined, skip = false', { skip: false }, common.mustNotCall());
11+
test('only = false', { only: false }, common.mustNotCall());
12+
test('only = false, skip = string', { only: false, skip: 'skip message' }, common.mustNotCall());
13+
test('only = false, skip = true', { only: false, skip: true }, common.mustNotCall());
14+
test('only = false, skip = false', { only: false, skip: false }, common.mustNotCall());
1515

1616
// 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 });
17+
test('only = true, skip = string', { only: true, skip: 'skip message' }, common.mustNotCall());
18+
test('only = true, skip = true', { only: true, skip: true }, common.mustNotCall());
1919

2020
// An 'only' test with subtests.
21-
test('only = true, with subtests', { only: true }, async (t) => {
21+
test('only = true, with subtests', { only: true }, common.mustCall(async (t) => {
2222
// These subtests should run.
23-
await t.test('running subtest 1');
24-
await t.test('running subtest 2');
23+
await t.test('running subtest 1', common.mustCall());
24+
await t.test('running subtest 2', common.mustCall());
2525

2626
// Switch the context to only execute 'only' tests.
2727
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 });
28+
await t.test('skipped subtest 1', common.mustNotCall());
29+
await t.test('skipped subtest 2'), common.mustNotCall();
30+
await t.test('running subtest 3', { only: true }, common.mustCall());
3131

3232
// Switch the context back to execute all tests.
3333
t.runOnly(false);
34-
await t.test('running subtest 4', async (t) => {
34+
await t.test('running subtest 4', common.mustCall(async (t) => {
3535
// These subtests should run.
36-
await t.test('running sub-subtest 1');
37-
await t.test('running sub-subtest 2');
36+
await t.test('running sub-subtest 1', common.mustCall());
37+
await t.test('running sub-subtest 2', common.mustCall());
3838

3939
// Switch the context to only execute 'only' tests.
4040
t.runOnly(true);
41-
await t.test('skipped sub-subtest 1');
42-
await t.test('skipped sub-subtest 2');
43-
});
41+
await t.test('skipped sub-subtest 1', common.mustNotCall());
42+
await t.test('skipped sub-subtest 2', common.mustNotCall());
43+
}));
4444

4545
// 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-
});
46+
await t.test('skipped subtest 3', { only: false }, common.mustNotCall());
47+
await t.test('skipped subtest 4', { skip: true }, common.mustNotCall());
48+
}));
4949

50-
describe.only('describe only = true, with subtests', () => {
51-
it.only('`it` subtest 1 should run', () => {});
50+
describe.only('describe only = true, with subtests', common.mustCall(() => {
51+
it.only('`it` subtest 1 should run', common.mustCall());
5252

53-
it('`it` subtest 2 should not run', async () => {});
54-
});
53+
it('`it` subtest 2 should not run', common.mustNotCall());
54+
}));
5555

56-
describe.only('describe only = true, with a mixture of subtests', () => {
57-
it.only('`it` subtest 1', () => {});
56+
describe.only('describe only = true, with a mixture of subtests', common.mustCall(() => {
57+
it.only('`it` subtest 1', common.mustCall());
5858

59-
it.only('`it` async subtest 1', async () => {});
59+
it.only('`it` async subtest 1', common.mustCall(async () => {}));
6060

61-
it('`it` subtest 2 only=true', { only: true });
61+
it('`it` subtest 2 only=true', { only: true }, common.mustCall());
6262

63-
it('`it` subtest 2 only=false', { only: false }, () => {
64-
throw new Error('This should not run');
65-
});
63+
it('`it` subtest 2 only=false', { only: false }, common.mustNotCall());
6664

67-
it.skip('`it` subtest 3 skip', () => {
68-
throw new Error('This should not run');
69-
});
65+
it.skip('`it` subtest 3 skip', common.mustNotCall());
7066

71-
it.todo('`it` subtest 4 todo', { only: false }, () => {
72-
throw new Error('This should not run');
73-
});
67+
it.todo('`it` subtest 4 todo', { only: false }, common.mustNotCall());
7468

75-
test.only('`test` subtest 1', () => {});
69+
test.only('`test` subtest 1', common.mustCall());
7670

77-
test.only('`test` async subtest 1', async () => {});
71+
test.only('`test` async subtest 1', common.mustCall(async () => {}));
7872

79-
test('`test` subtest 2 only=true', { only: true });
73+
test('`test` subtest 2 only=true', { only: true }, common.mustCall());
8074

81-
test('`test` subtest 2 only=false', { only: false }, () => {
82-
throw new Error('This should not run');
83-
});
75+
test('`test` subtest 2 only=false', { only: false }, common.mustNotCall());
8476

85-
test.skip('`test` subtest 3 skip', () => {
86-
throw new Error('This should not run');
87-
});
77+
test.skip('`test` subtest 3 skip', common.mustNotCall());
8878

89-
test.todo('`test` subtest 4 todo', { only: false }, () => {
90-
throw new Error('This should not run');
91-
});
92-
});
79+
test.todo('`test` subtest 4 todo', { only: false }, common.mustNotCall());
80+
}));
9381

94-
describe.only('describe only = true, with subtests', () => {
95-
test.only('subtest should run', () => {});
82+
describe.only('describe only = true, with subtests', common.mustCall(() => {
83+
test.only('subtest should run', common.mustCall());
9684

97-
test('async subtest should not run', async () => {});
85+
test('async subtest should not run', common.mustNotCall());
9886

99-
test('subtest should be skipped', { only: false }, () => {});
100-
});
87+
test('subtest should be skipped', { only: false }, common.mustNotCall());
88+
}));
89+
90+
91+
describe('describe only = undefined, with nested only subtest', common.mustCall(() => {
92+
test('subtest should not run', common.mustNotCall());
93+
describe('nested describe', common.mustCall(() => {
94+
test('subtest should not run', common.mustNotCall());
95+
test.only('nested test should run', common.mustCall());
96+
}));
97+
}));
98+
99+
100+
describe('describe only = undefined, with subtests', common.mustCall(() => {
101+
test('async subtest should not run', common.mustNotCall());
102+
}));
103+
104+
describe('describe only = false, with subtests', { only: false }, common.mustNotCall(() => {
105+
test('async subtest should not run', common.mustNotCall());
106+
}));
107+
108+
109+
describe.only('describe only = true, with nested subtests', common.mustCall(() => {
110+
test('async subtest should run', common.mustCall());
111+
describe('nested describe', common.mustCall(() => {
112+
test('nested test should run', common.mustCall());
113+
}));
114+
}));
115+
116+
describe('describe only = false, with nested only subtests', { only: false }, common.mustNotCall(() => {
117+
test('async subtest should not run', common.mustNotCall());
118+
describe('nested describe', common.mustNotCall(() => {
119+
test.only('nested test should run', common.mustNotCall());
120+
}));
121+
}));

test/fixtures/test-runner/output/only_tests.snapshot

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,53 @@ ok 6 - describe only = true, with subtests
112112
duration_ms: *
113113
type: 'suite'
114114
...
115-
1..6
116-
# tests 18
117-
# suites 3
118-
# pass 15
115+
# Subtest: describe only = undefined, with nested only subtest
116+
# Subtest: nested describe
117+
# Subtest: nested test should run
118+
ok 1 - nested test should run
119+
---
120+
duration_ms: *
121+
...
122+
1..1
123+
ok 1 - nested describe
124+
---
125+
duration_ms: *
126+
type: 'suite'
127+
...
128+
1..1
129+
ok 7 - describe only = undefined, with nested only subtest
130+
---
131+
duration_ms: *
132+
type: 'suite'
133+
...
134+
# Subtest: describe only = true, with nested subtests
135+
# Subtest: async subtest should run
136+
ok 1 - async subtest should run
137+
---
138+
duration_ms: *
139+
...
140+
# Subtest: nested describe
141+
# Subtest: nested test should run
142+
ok 1 - nested test should run
143+
---
144+
duration_ms: *
145+
...
146+
1..1
147+
ok 2 - nested describe
148+
---
149+
duration_ms: *
150+
type: 'suite'
151+
...
152+
1..2
153+
ok 8 - describe only = true, with nested subtests
154+
---
155+
duration_ms: *
156+
type: 'suite'
157+
...
158+
1..8
159+
# tests 21
160+
# suites 7
161+
# pass 18
119162
# fail 0
120163
# cancelled 0
121164
# skipped 3

0 commit comments

Comments
 (0)