Skip to content

Commit f20819b

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

File tree

11 files changed

+121
-3
lines changed

11 files changed

+121
-3
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/test.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,23 @@ 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 --test-bail=1
614+
```
615+
616+
An integer value representing the number of test failures after
617+
which the test execution should stop.
618+
If set to `0`, the test runner will not bail out,
619+
and all tests will run regardless of failures.
620+
**Default:** `0`.
621+
605622
## Test reporters
606623

607624
<!-- YAML

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/harness.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ function setup(root) {
179179
topLevel: 0,
180180
suites: 0,
181181
},
182+
bailAmount: globalOptions.bail,
182183
shouldColorizeTestFiles: false,
183184
};
184185
root.startTime = hrtime();

lib/internal/test_runner/runner.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ class FileTest extends Test {
136136
#rawBufferSize = 0;
137137
#reportedChildren = 0;
138138
failedSubtests = false;
139+
140+
#bailAmount = this.root.harness.bailAmount;
141+
#failedTestsCount = 0;
142+
139143
#skipReporting() {
140144
return this.#reportedChildren > 0 && (!this.error || this.error.failureType === kSubtestsFailed);
141145
}
@@ -179,6 +183,11 @@ class FileTest extends Test {
179183
if (item.type !== 'test:pass' && item.type !== 'test:fail') {
180184
return;
181185
}
186+
187+
if (item.type === 'test:fail') {
188+
this.#failedTestsCount++;
189+
}
190+
182191
this.#reportedChildren++;
183192
if (item.data.nesting === 0 && item.type === 'test:fail') {
184193
this.failedSubtests = true;
@@ -191,6 +200,9 @@ class FileTest extends Test {
191200
}
192201
}
193202
addToReport(item) {
203+
if (this.#bailAmount && this.#failedTestsCount > this.#bailAmount) {
204+
return;
205+
}
194206
this.#accumulateReportItem(item);
195207
if (!this.isClearToSend()) {
196208
ArrayPrototypePush(this.#reportBuffer, item);
@@ -425,7 +437,7 @@ function run(options) {
425437
options = kEmptyObject;
426438
}
427439
let { testNamePatterns, shard } = options;
428-
const { concurrency, timeout, signal, files, inspectPort, watch, setup } = options;
440+
const { concurrency, timeout, signal, files, inspectPort, watch, setup, bail } = options;
429441

430442
if (files != null) {
431443
validateArray(files, 'options.files');
@@ -469,6 +481,10 @@ function run(options) {
469481
});
470482
}
471483

484+
if (bail != null) {
485+
validateInteger(bail, 'options.bail', 0);
486+
}
487+
472488
const root = createTestTree({ concurrency, timeout, signal });
473489
let testFiles = files ?? createTestFileList();
474490

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+
uint64_t test_bail = 0;
162163
bool test_runner_coverage = false;
163164
std::vector<std::string> test_name_pattern;
164165
std::vector<std::string> test_reporter;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const test = require('node:test');
2+
3+
test('nested', (t) => {
4+
t.test('ok', () => {});
5+
t.test('failing', () => {
6+
throw new Error('first');
7+
});
8+
});
9+
10+
test('top level', (t) => {
11+
t.test('ok', () => {});
12+
t.test('failing', () => {
13+
throw new Error('second');
14+
});
15+
});
16+
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
const test = require('node:test');
2+
3+
test('multiple', (t) => {
4+
t.test('ok', () => {});
5+
t.test('failing', () => {
6+
throw new Error('first');
7+
});
8+
});

test/parallel/test-runner-bail.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
require('../common');
4+
const fixtures = require('../common/fixtures');
5+
const tmpdir = require('../common/tmpdir');
6+
const { describe, it } = require('node:test');
7+
const { spawnSync } = require('node:child_process');
8+
const assert = require('node:assert');
9+
10+
const testFile = fixtures.path('test-runner/bail/bail.js');
11+
tmpdir.refresh();
12+
13+
describe('node:test bail', () => {
14+
it('should exit at first failure', async () => {
15+
const child = spawnSync(process.execPath, ['--test', '--test-bail=1', testFile]);
16+
console.log(child.stdout.toString());
17+
assert.strictEqual(child.stderr.toString(), '');
18+
assert.match(child.stdout.toString(), /TAP version 13/);
19+
assert.match(child.stdout.toString(), /ok 1 - ok/);
20+
assert.match(child.stdout.toString(), /not ok 2 - failing/);
21+
assert.match(child.stdout.toString(), /not ok 1 - nested/);
22+
assert.doesNotMatch(child.stdout.toString(), /not ok 2 - top level/);
23+
assert.doesNotMatch(child.stdout.toString(), /Subtest: top level/);
24+
});
25+
26+
it('should exit at second failure', async () => {
27+
const child = spawnSync(process.execPath, ['--test', '--test-bail=2', testFile]);
28+
console.log(child.stdout.toString());
29+
assert.strictEqual(child.stderr.toString(), '');
30+
assert.match(child.stdout.toString(), /TAP version 13/);
31+
assert.match(child.stdout.toString(), /ok 1 - ok/);
32+
assert.match(child.stdout.toString(), /not ok 2 - failing/);
33+
assert.match(child.stdout.toString(), /not ok 1 - nested/);
34+
assert.match(child.stdout.toString(), /not ok 2 - failing/);
35+
assert.match(child.stdout.toString(), /Subtest: top level/);
36+
});
37+
});

0 commit comments

Comments
 (0)