Skip to content

Commit 41aa463

Browse files
test_runner: test runner bail
1 parent 8f7c4e9 commit 41aa463

File tree

12 files changed

+131
-7
lines changed

12 files changed

+131
-7
lines changed

doc/api/cli.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1441,6 +1441,16 @@ Starts the Node.js command line test runner. This flag cannot be combined with
14411441
See the documentation on [running tests from the command line][]
14421442
for more details.
14431443

1444+
### `--test-bail`
1445+
1446+
<!-- YAML
1447+
added:
1448+
- REPLACEME
1449+
-->
1450+
1451+
Specifies the bailout behavior of the test runner when running tests.
1452+
See the documentation on [test bailout][] for more details.
1453+
14441454
### `--test-name-pattern`
14451455

14461456
<!-- YAML
@@ -2643,6 +2653,7 @@ done
26432653
[security warning]: #warning-binding-inspector-to-a-public-ipport-combination-is-insecure
26442654
[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space
26452655
[single executable application]: single-executable-applications.md
2656+
[test bailout]: test.md#test-bail
26462657
[test reporters]: test.md#test-reporters
26472658
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
26482659
[tracking issue for user-land snapshots]: https://github.com/nodejs/node/issues/44014

doc/api/errors.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2738,6 +2738,13 @@ the token causing the error is available via the `cause` property.
27382738

27392739
This error represents a failed TAP validation.
27402740

2741+
<a id="ERR_TEST_BAILOUT"></a>
2742+
2743+
### `ERR_TEST_BAILOUT`
2744+
2745+
This error represents a test that has bailed out after failure.
2746+
This error occurs only when the flag `--test-bail` is passed.
2747+
27412748
<a id="ERR_TEST_FAILURE"></a>
27422749

27432750
### `ERR_TEST_FAILURE`

doc/api/test.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,24 @@ test('mocks setTimeout to be executed synchronously without having to actually w
602602
});
603603
```
604604

605+
## Test bail
606+
607+
<!-- YAML
608+
added:
609+
- REPLACEME
610+
-->
611+
612+
```bash
613+
node --test-bail
614+
```
615+
616+
The `--test-bail` flag provides a way to stop the test execution
617+
as soon as a test fails.
618+
By enabling this flag, the test runner will exit the test suite early
619+
when it encounters the first failing test, preventing
620+
the execution of subsequent tests.
621+
**Default:** `false`.
622+
605623
## Test reporters
606624

607625
<!-- YAML

lib/internal/errors.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1613,6 +1613,10 @@ E('ERR_TAP_VALIDATION_ERROR', function(errorMsg) {
16131613
hideInternalStackFrames(this);
16141614
return errorMsg;
16151615
}, Error);
1616+
E('ERR_TEST_BAILOUT', function(errorMsg) {
1617+
hideInternalStackFrames(this);
1618+
return errorMsg;
1619+
}, Error);
16161620
E('ERR_TEST_FAILURE', function(error, failureType) {
16171621
hideInternalStackFrames(this);
16181622
assert(typeof failureType === 'string' || typeof failureType === 'symbol',

lib/internal/main/test_runner.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,13 @@ if (shardOption) {
5757
};
5858
}
5959

60-
run({ concurrency, inspectPort, watch: getOptionValue('--watch'), setup: setupTestReporters, shard })
61-
.once('test:fail', () => {
60+
run({
61+
concurrency,
62+
inspectPort,
63+
watch: getOptionValue('--watch'),
64+
setup: setupTestReporters,
65+
shard,
66+
bail: getOptionValue('--test-bail'),
67+
}).once('test:fail', () => {
6268
process.exitCode = kGenericUserError;
6369
});

lib/internal/test_runner/test.js

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ const {
2828
codes: {
2929
ERR_INVALID_ARG_TYPE,
3030
ERR_TEST_FAILURE,
31+
ERR_TEST_BAILOUT,
3132
},
3233
AbortError,
3334
} = require('internal/errors');
@@ -71,8 +72,9 @@ const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']);
7172
const kUnwrapErrors = new SafeSet()
7273
.add(kTestCodeFailure).add(kHookFailure)
7374
.add('uncaughtException').add('unhandledRejection');
74-
const { testNamePatterns, testOnlyFlag } = parseCommandLine();
75+
const { testNamePatterns, testOnlyFlag, bail } = parseCommandLine();
7576
let kResistStopPropagation;
77+
let bailedOut = false;
7678

7779
function stopTest(timeout, signal) {
7880
if (timeout === kDefaultTimeout) {
@@ -421,11 +423,13 @@ class Test extends AsyncResource {
421423
return;
422424
}
423425

426+
const unknownError = bailedOut ? new ERR_TEST_BAILOUT('test bailed out') : new ERR_TEST_FAILURE(
427+
'test did not finish before its parent and was cancelled',
428+
kCancelledByParent,
429+
);
430+
424431
this.fail(error ||
425-
new ERR_TEST_FAILURE(
426-
'test did not finish before its parent and was cancelled',
427-
kCancelledByParent,
428-
),
432+
unknownError,
429433
);
430434
this.startTime = this.startTime || this.endTime; // If a test was canceled before it was started, e.g inside a hook
431435
this.cancelled = true;
@@ -444,6 +448,7 @@ class Test extends AsyncResource {
444448
}
445449

446450
fail(err) {
451+
bailedOut = bail;
447452
if (this.error !== null) {
448453
return;
449454
}
@@ -526,6 +531,10 @@ class Test extends AsyncResource {
526531
}
527532

528533
async run(pendingSubtestsError) {
534+
if (bailedOut) {
535+
return;
536+
}
537+
529538
if (this.parent !== null) {
530539
this.parent.activeSubtests++;
531540
}

lib/internal/test_runner/utils.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ function parseCommandLine() {
179179
}
180180

181181
const isTestRunner = getOptionValue('--test');
182+
const bail = getOptionValue('--test-bail');
182183
const coverage = getOptionValue('--experimental-test-coverage');
183184
const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child';
184185
const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8';
@@ -230,6 +231,7 @@ function parseCommandLine() {
230231
globalTestOptions = {
231232
__proto__: null,
232233
isTestRunner,
234+
bail,
233235
coverage,
234236
testOnlyFlag,
235237
testNamePatterns,

src/node_options.cc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -567,6 +567,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
567567
"profile generated with --heap-prof. (default: 512 * 1024)",
568568
&EnvironmentOptions::heap_prof_interval);
569569
#endif // HAVE_INSPECTOR
570+
AddOption("--test-bail",
571+
"stop test execution when given number of tests have failed",
572+
&EnvironmentOptions::test_bail);
570573
AddOption("--max-http-header-size",
571574
"set the maximum size of HTTP headers (default: 16384 (16KB))",
572575
&EnvironmentOptions::max_http_header_size,

src/node_options.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ class EnvironmentOptions : public Options {
159159
std::string redirect_warnings;
160160
std::string diagnostic_dir;
161161
bool test_runner = false;
162+
bool test_bail = false;
162163
bool test_runner_coverage = false;
163164
std::vector<std::string> test_name_pattern;
164165
std::vector<std::string> test_reporter;
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const assert = require('assert');
2+
const test = require('node:test');
3+
4+
test('nested', (t) => {
5+
t.test('first', () => {});
6+
t.test('second', () => {
7+
throw new Error();
8+
});
9+
t.test('third', () => {});
10+
});
11+
12+
test('top level', (t) => {
13+
t.test('forth', () => {});
14+
t.test('fifth', () => {
15+
throw new Error();
16+
});
17+
});
18+

0 commit comments

Comments
 (0)