Skip to content

Commit f1c269c

Browse files
committed
test_runner: add bail out
1 parent 062ae6f commit f1c269c

File tree

21 files changed

+487
-8
lines changed

21 files changed

+487
-8
lines changed

doc/api/cli.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2254,6 +2254,17 @@ Starts the Node.js command line test runner. This flag cannot be combined with
22542254
See the documentation on [running tests from the command line][]
22552255
for more details.
22562256

2257+
### `--test-bail`
2258+
2259+
<!-- YAML
2260+
added: REPLACEME
2261+
-->
2262+
2263+
> Stability: 1 - Experimental
2264+
2265+
Instructs the test runner to bail out if a test failure occurs.
2266+
See the documentation on [test bailout][] for more details.
2267+
22572268
### `--test-concurrency`
22582269

22592270
<!-- YAML
@@ -3714,6 +3725,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
37143725
[single executable application]: single-executable-applications.md
37153726
[snapshot testing]: test.md#snapshot-testing
37163727
[syntax detection]: packages.md#syntax-detection
3728+
[test bailout]: test.md#bailing-out
37173729
[test reporters]: test.md#test-reporters
37183730
[test runner execution model]: test.md#test-runner-execution-model
37193731
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones

doc/api/test.md

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,23 @@ exports[`suite of snapshot tests > snapshot test 2`] = `
990990
Once the snapshot file is created, run the tests again without the
991991
`--test-update-snapshots` flag. The tests should pass now.
992992

993+
## Bailing out
994+
995+
<!-- YAML
996+
added:
997+
- REPLACEME
998+
-->
999+
1000+
> Stability: 1 - Experimental
1001+
1002+
The `--test-bail` flag provides a way to stop test execution
1003+
as soon as a test fails.
1004+
By enabling this flag, the test runner will exit the test suite early
1005+
when it encounters the first failing test, preventing
1006+
the execution of subsequent tests.
1007+
Already running tests will be canceled, and no further tests will be started.
1008+
**Default:** `false`.
1009+
9931010
## Test reporters
9941011

9951012
<!-- YAML
@@ -1077,6 +1094,9 @@ const customReporter = new Transform({
10771094
case 'test:fail':
10781095
callback(null, `test ${event.data.name} failed`);
10791096
break;
1097+
case 'test:bail':
1098+
callback(null, `test ${event.data.name} bailed out`);
1099+
break;
10801100
case 'test:plan':
10811101
callback(null, 'test plan');
10821102
break;
@@ -1122,6 +1142,9 @@ const customReporter = new Transform({
11221142
case 'test:fail':
11231143
callback(null, `test ${event.data.name} failed`);
11241144
break;
1145+
case 'test:bail':
1146+
callback(null, `test ${event.data.name} bailed out`);
1147+
break;
11251148
case 'test:plan':
11261149
callback(null, 'test plan');
11271150
break;
@@ -1166,6 +1189,9 @@ export default async function * customReporter(source) {
11661189
case 'test:fail':
11671190
yield `test ${event.data.name} failed\n`;
11681191
break;
1192+
case 'test:bail':
1193+
yield `test ${event.data.name} bailed out\n`;
1194+
break;
11691195
case 'test:plan':
11701196
yield 'test plan\n';
11711197
break;
@@ -1206,6 +1232,9 @@ module.exports = async function * customReporter(source) {
12061232
case 'test:fail':
12071233
yield `test ${event.data.name} failed\n`;
12081234
break;
1235+
case 'test:bail':
1236+
yield `test ${event.data.name} bailed out\n`;
1237+
break;
12091238
case 'test:plan':
12101239
yield 'test plan\n';
12111240
break;
@@ -1483,6 +1512,11 @@ changes:
14831512
does not have a name.
14841513
* `options` {Object} Configuration options for the test. The following
14851514
properties are supported:
1515+
* `bail` {boolean}
1516+
If `true`, it will exit the test suite early
1517+
when it encounters the first failing test, preventing
1518+
the execution of subsequent tests and canceling already running tests.
1519+
**Default:** `false`.
14861520
* `concurrency` {number|boolean} If a number is provided,
14871521
then that many tests would run in parallel within the application thread.
14881522
If `true`, all scheduled asynchronous tests run concurrently within the
@@ -3124,6 +3158,22 @@ generated for each test file in addition to a final cumulative summary.
31243158

31253159
Emitted when no more tests are queued for execution in watch mode.
31263160

3161+
### Event: `'test:bail'`
3162+
3163+
* `data` {Object}
3164+
* `column` {number|undefined} The column number where the test is defined, or
3165+
`undefined` if the test was run through the REPL.
3166+
* `file` {string|undefined} The path of the test file,
3167+
`undefined` if test was run through the REPL.
3168+
* `line` {number|undefined} The line number where the test is defined, or
3169+
`undefined` if the test was run through the REPL.
3170+
* `name` {string} The test name.
3171+
* `nesting` {number} The nesting level of the test.
3172+
3173+
Emitted when the test runner stops executing tests due to the [`--test-bail`][] flag.
3174+
This event signals that the first failing test caused the suite to bail out,
3175+
canceling all pending and currently running tests.
3176+
31273177
## Class: `TestContext`
31283178

31293179
<!-- YAML
@@ -3618,6 +3668,7 @@ Can be used to abort test subtasks when the test has been aborted.
36183668
[`--experimental-test-module-mocks`]: cli.md#--experimental-test-module-mocks
36193669
[`--import`]: cli.md#--importmodule
36203670
[`--no-experimental-strip-types`]: cli.md#--no-experimental-strip-types
3671+
[`--test-bail`]: cli.md#--test-bail
36213672
[`--test-concurrency`]: cli.md#--test-concurrency
36223673
[`--test-coverage-exclude`]: cli.md#--test-coverage-exclude
36233674
[`--test-coverage-include`]: cli.md#--test-coverage-include

lib/internal/test_runner/harness.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ function createTestTree(rootTestOptions, globalOptions) {
7676

7777
buildPhaseDeferred.resolve();
7878
},
79+
testsProcesses: new SafeMap(),
7980
};
8081

8182
harness.resetCounters();

lib/internal/test_runner/reporter/spec.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const {
99
const assert = require('assert');
1010
const Transform = require('internal/streams/transform');
1111
const colors = require('internal/util/colors');
12-
const { kSubtestsFailed } = require('internal/test_runner/test');
12+
const { kSubtestsFailed, kTestBailedOut } = require('internal/test_runner/test');
1313
const { getCoverageReport } = require('internal/test_runner/utils');
1414
const { relative } = require('path');
1515
const {
@@ -57,6 +57,7 @@ class SpecReporter extends Transform {
5757
#handleEvent({ type, data }) {
5858
switch (type) {
5959
case 'test:fail':
60+
if (data.details?.error?.failureType === kTestBailedOut) break;
6061
if (data.details?.error?.failureType !== kSubtestsFailed) {
6162
ArrayPrototypePush(this.#failedTests, data);
6263
}
@@ -74,6 +75,8 @@ class SpecReporter extends Transform {
7475
case 'test:coverage':
7576
return getCoverageReport(indent(data.nesting), data.summary,
7677
reporterUnicodeSymbolMap['test:coverage'], colors.blue, true);
78+
case 'test:bail':
79+
return `${reporterColorMap[type]}${reporterUnicodeSymbolMap[type]}Bail out!${colors.white}\n`;
7780
}
7881
}
7982
_transform({ type, data }, encoding, callback) {

lib/internal/test_runner/reporter/tap.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ async function * tapReporter(source) {
3333
for await (const { type, data } of source) {
3434
switch (type) {
3535
case 'test:fail': {
36+
if (data.details?.error?.failureType === lazyLoadTest().kTestBailedOut) break;
3637
yield reportTest(data.nesting, data.testNumber, 'not ok', data.name, data.skip, data.todo);
3738
const location = data.file ? `${data.file}:${data.line}:${data.column}` : null;
3839
yield reportDetails(data.nesting, data.details, location);
@@ -61,6 +62,9 @@ async function * tapReporter(source) {
6162
case 'test:coverage':
6263
yield getCoverageReport(indent(data.nesting), data.summary, '# ', '', true);
6364
break;
65+
case 'test:bail':
66+
yield `${indent(data.nesting)}Bail out!\n`;
67+
break;
6468
}
6569
}
6670
}

lib/internal/test_runner/reporter/utils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ const reporterUnicodeSymbolMap = {
2424
'test:coverage': '\u2139 ',
2525
'arrow:right': '\u25B6 ',
2626
'hyphen:minus': '\uFE63 ',
27+
'test:bail': '\u2716 ',
2728
};
2829

2930
const reporterColorMap = {
@@ -37,6 +38,9 @@ const reporterColorMap = {
3738
get 'test:diagnostic'() {
3839
return colors.blue;
3940
},
41+
get 'test:bail'() {
42+
return colors.red;
43+
},
4044
};
4145

4246
function indent(nesting) {

lib/internal/test_runner/runner.js

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ const {
7676
kSubtestsFailed,
7777
kTestCodeFailure,
7878
kTestTimeoutFailure,
79+
kTestBailedOut,
7980
Test,
8081
} = require('internal/test_runner/test');
8182

@@ -101,7 +102,10 @@ const kFilterArgValues = ['--test-reporter', '--test-reporter-destination'];
101102
const kDiagnosticsFilterArgs = ['tests', 'suites', 'pass', 'fail', 'cancelled', 'skipped', 'todo', 'duration_ms'];
102103

103104
const kCanceledTests = new SafeSet()
104-
.add(kCancelledByParent).add(kAborted).add(kTestTimeoutFailure);
105+
.add(kCancelledByParent)
106+
.add(kAborted)
107+
.add(kTestTimeoutFailure)
108+
.add(kTestBailedOut);
105109

106110
let kResistStopPropagation;
107111

@@ -137,7 +141,8 @@ function getRunArgs(path, { forceExit,
137141
only,
138142
argv: suppliedArgs,
139143
execArgv,
140-
cwd }) {
144+
cwd,
145+
bail }) {
141146
const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
142147
if (forceExit === true) {
143148
ArrayPrototypePush(argv, '--test-force-exit');
@@ -154,6 +159,9 @@ function getRunArgs(path, { forceExit,
154159
if (only === true) {
155160
ArrayPrototypePush(argv, '--test-only');
156161
}
162+
if (bail === true) {
163+
ArrayPrototypePush(argv, '--test-bail');
164+
}
157165

158166
ArrayPrototypePushApply(argv, execArgv);
159167

@@ -216,6 +224,14 @@ class FileTest extends Test {
216224
if (item.data.details?.error) {
217225
item.data.details.error = deserializeError(item.data.details.error);
218226
}
227+
if (item.type === 'test:bail') {
228+
// <-- here we need to stop all the pending test files (aka subprocesses)
229+
// To be replaced, just for poc
230+
this.root.harness.testsProcesses.forEach((child) => {
231+
child.kill();
232+
});
233+
return;
234+
}
219235
if (item.type === 'test:pass' || item.type === 'test:fail') {
220236
item.data.testNumber = isTopLevel ? (this.root.harness.counters.topLevel + 1) : item.data.testNumber;
221237
countCompletedTest({
@@ -362,7 +378,12 @@ function runTestFile(path, filesWatcher, opts) {
362378
const watchMode = filesWatcher != null;
363379
const testPath = path === kIsolatedProcessName ? '' : path;
364380
const testOpts = { __proto__: null, signal: opts.signal };
381+
const subtestProcesses = opts.root.harness.testsProcesses;
365382
const subtest = opts.root.createSubtest(FileTest, testPath, testOpts, async (t) => {
383+
if (opts.root.bailed) {
384+
// TODO(pmarchini): this is a temporary solution to avoid running tests after bailing
385+
return; // No-op in order to avoid running tests after bailing
386+
}
366387
const args = getRunArgs(path, opts);
367388
const stdio = ['pipe', 'pipe', 'pipe'];
368389
const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8' };
@@ -389,6 +410,7 @@ function runTestFile(path, filesWatcher, opts) {
389410
filesWatcher.runningProcesses.set(path, child);
390411
filesWatcher.watcher.watchChildProcessModules(child, path);
391412
}
413+
subtestProcesses.set(path, child);
392414

393415
let err;
394416

@@ -422,6 +444,7 @@ function runTestFile(path, filesWatcher, opts) {
422444
finished(child.stdout, { __proto__: null, signal: t.signal }),
423445
]);
424446

447+
subtestProcesses.delete(path);
425448
if (watchMode) {
426449
filesWatcher.runningProcesses.delete(path);
427450
filesWatcher.runningSubtests.delete(path);
@@ -478,6 +501,8 @@ function watchFiles(testFiles, opts) {
478501
// Reset the topLevel counter
479502
opts.root.harness.counters.topLevel = 0;
480503
}
504+
// TODO(pmarchini): Reset the bailed flag to rerun the tests.
505+
// This must be added only when we add support for bail in watch mode.
481506
await runningSubtests.get(file);
482507
runningSubtests.set(file, runTestFile(file, filesWatcher, opts));
483508
}
@@ -564,6 +589,7 @@ function run(options = kEmptyObject) {
564589
execArgv = [],
565590
argv = [],
566591
cwd = process.cwd(),
592+
bail = false,
567593
} = options;
568594

569595
if (files != null) {
@@ -663,6 +689,15 @@ function run(options = kEmptyObject) {
663689

664690
validateStringArray(argv, 'options.argv');
665691
validateStringArray(execArgv, 'options.execArgv');
692+
validateBoolean(bail, 'options.bail');
693+
// TODO(pmarchini): watch mode with bail needs to be implemented
694+
if (bail && watch) {
695+
throw new ERR_INVALID_ARG_VALUE(
696+
'options.bail',
697+
watch,
698+
'bail not supported while watch mode is enabled',
699+
);
700+
}
666701

667702
const rootTestOptions = { __proto__: null, concurrency, timeout, signal };
668703
const globalOptions = {
@@ -678,6 +713,7 @@ function run(options = kEmptyObject) {
678713
branchCoverage: branchCoverage,
679714
functionCoverage: functionCoverage,
680715
cwd,
716+
bail,
681717
};
682718
const root = createTestTree(rootTestOptions, globalOptions);
683719
let testFiles = files ?? createTestFileList(globPatterns, cwd);
@@ -705,6 +741,7 @@ function run(options = kEmptyObject) {
705741
isolation,
706742
argv,
707743
execArgv,
744+
bail,
708745
};
709746

710747
if (isolation === 'process') {

0 commit comments

Comments
 (0)