Skip to content

Commit 89f64db

Browse files
committed
test_runner: add bail out
1 parent 57b21b1 commit 89f64db

File tree

21 files changed

+505
-24
lines changed

21 files changed

+505
-24
lines changed

doc/api/cli.md

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

2217+
### `--test-bail`
2218+
2219+
<!-- YAML
2220+
added: REPLACEME
2221+
-->
2222+
2223+
> Stability: 1 - Experimental
2224+
2225+
Instructs the test runner to bail out if a test failure occurs.
2226+
See the documentation on [test bailout][] for more details.
2227+
22172228
### `--test-concurrency`
22182229

22192230
<!-- YAML
@@ -3671,6 +3682,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
36713682
[single executable application]: single-executable-applications.md
36723683
[snapshot testing]: test.md#snapshot-testing
36733684
[syntax detection]: packages.md#syntax-detection
3685+
[test bailout]: test.md#bailing-out
36743686
[test reporters]: test.md#test-reporters
36753687
[test runner execution model]: test.md#test-runner-execution-model
36763688
[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
@@ -984,6 +984,23 @@ exports[`suite of snapshot tests > snapshot test 2`] = `
984984
Once the snapshot file is created, run the tests again without the
985985
`--test-update-snapshots` flag. The tests should pass now.
986986

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

9891006
<!-- YAML
@@ -1071,6 +1088,9 @@ const customReporter = new Transform({
10711088
case 'test:fail':
10721089
callback(null, `test ${event.data.name} failed`);
10731090
break;
1091+
case 'test:bail':
1092+
callback(null, `test ${event.data.name} bailed out`);
1093+
break;
10741094
case 'test:plan':
10751095
callback(null, 'test plan');
10761096
break;
@@ -1116,6 +1136,9 @@ const customReporter = new Transform({
11161136
case 'test:fail':
11171137
callback(null, `test ${event.data.name} failed`);
11181138
break;
1139+
case 'test:bail':
1140+
callback(null, `test ${event.data.name} bailed out`);
1141+
break;
11191142
case 'test:plan':
11201143
callback(null, 'test plan');
11211144
break;
@@ -1160,6 +1183,9 @@ export default async function * customReporter(source) {
11601183
case 'test:fail':
11611184
yield `test ${event.data.name} failed\n`;
11621185
break;
1186+
case 'test:bail':
1187+
yield `test ${event.data.name} bailed out\n`;
1188+
break;
11631189
case 'test:plan':
11641190
yield 'test plan\n';
11651191
break;
@@ -1200,6 +1226,9 @@ module.exports = async function * customReporter(source) {
12001226
case 'test:fail':
12011227
yield `test ${event.data.name} failed\n`;
12021228
break;
1229+
case 'test:bail':
1230+
yield `test ${event.data.name} bailed out\n`;
1231+
break;
12031232
case 'test:plan':
12041233
yield 'test plan\n';
12051234
break;
@@ -1477,6 +1506,11 @@ changes:
14771506
does not have a name.
14781507
* `options` {Object} Configuration options for the test. The following
14791508
properties are supported:
1509+
* `bail` {boolean}
1510+
If `true`, it will exit the test suite early
1511+
when it encounters the first failing test, preventing
1512+
the execution of subsequent tests and canceling already running tests.
1513+
**Default:** `false`.
14801514
* `concurrency` {number|boolean} If a number is provided,
14811515
then that many tests would run in parallel within the application thread.
14821516
If `true`, all scheduled asynchronous tests run concurrently within the
@@ -3099,6 +3133,22 @@ generated for each test file in addition to a final cumulative summary.
30993133

31003134
Emitted when no more tests are queued for execution in watch mode.
31013135

3136+
### Event: `'test:bail'`
3137+
3138+
* `data` {Object}
3139+
* `column` {number|undefined} The column number where the test is defined, or
3140+
`undefined` if the test was run through the REPL.
3141+
* `file` {string|undefined} The path of the test file,
3142+
`undefined` if test was run through the REPL.
3143+
* `line` {number|undefined} The line number where the test is defined, or
3144+
`undefined` if the test was run through the REPL.
3145+
* `name` {string} The test name.
3146+
* `nesting` {number} The nesting level of the test.
3147+
3148+
Emitted when the test runner stops executing tests due to the [`--test-bail`][] flag.
3149+
This event signals that the first failing test caused the suite to bail out,
3150+
canceling all pending and currently running tests.
3151+
31023152
## Class: `TestContext`
31033153

31043154
<!-- YAML
@@ -3595,6 +3645,7 @@ Can be used to abort test subtasks when the test has been aborted.
35953645
[`--experimental-test-coverage`]: cli.md#--experimental-test-coverage
35963646
[`--experimental-test-module-mocks`]: cli.md#--experimental-test-module-mocks
35973647
[`--import`]: cli.md#--importmodule
3648+
[`--test-bail`]: cli.md#--test-bail
35983649
[`--test-concurrency`]: cli.md#--test-concurrency
35993650
[`--test-coverage-exclude`]: cli.md#--test-coverage-exclude
36003651
[`--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)