Skip to content

Commit

Permalink
test_runner: test runner bail
Browse files Browse the repository at this point in the history
  • Loading branch information
marco-ippolito committed Jul 26, 2023
1 parent 8f7c4e9 commit 41aa463
Show file tree
Hide file tree
Showing 12 changed files with 131 additions and 7 deletions.
11 changes: 11 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1441,6 +1441,16 @@ Starts the Node.js command line test runner. This flag cannot be combined with
See the documentation on [running tests from the command line][]
for more details.

### `--test-bail`

<!-- YAML
added:
- REPLACEME
-->

Specifies the bailout behavior of the test runner when running tests.
See the documentation on [test bailout][] for more details.

### `--test-name-pattern`

<!-- YAML
Expand Down Expand Up @@ -2643,6 +2653,7 @@ done
[security warning]: #warning-binding-inspector-to-a-public-ipport-combination-is-insecure
[semi-space]: https://www.memorymanagement.org/glossary/s.html#semi.space
[single executable application]: single-executable-applications.md
[test bailout]: test.md#test-bail
[test reporters]: test.md#test-reporters
[timezone IDs]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones
[tracking issue for user-land snapshots]: https://github.com/nodejs/node/issues/44014
Expand Down
7 changes: 7 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -2738,6 +2738,13 @@ the token causing the error is available via the `cause` property.

This error represents a failed TAP validation.

<a id="ERR_TEST_BAILOUT"></a>

### `ERR_TEST_BAILOUT`

This error represents a test that has bailed out after failure.
This error occurs only when the flag `--test-bail` is passed.

<a id="ERR_TEST_FAILURE"></a>

### `ERR_TEST_FAILURE`
Expand Down
18 changes: 18 additions & 0 deletions doc/api/test.md
Original file line number Diff line number Diff line change
Expand Up @@ -602,6 +602,24 @@ test('mocks setTimeout to be executed synchronously without having to actually w
});
```

## Test bail

<!-- YAML
added:
- REPLACEME
-->

```bash
node --test-bail
```

The `--test-bail` flag provides a way to stop the test execution
as soon as a test fails.
By enabling this flag, the test runner will exit the test suite early
when it encounters the first failing test, preventing
the execution of subsequent tests.
**Default:** `false`.

## Test reporters

<!-- YAML
Expand Down
4 changes: 4 additions & 0 deletions lib/internal/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -1613,6 +1613,10 @@ E('ERR_TAP_VALIDATION_ERROR', function(errorMsg) {
hideInternalStackFrames(this);
return errorMsg;
}, Error);
E('ERR_TEST_BAILOUT', function(errorMsg) {
hideInternalStackFrames(this);
return errorMsg;
}, Error);
E('ERR_TEST_FAILURE', function(error, failureType) {
hideInternalStackFrames(this);
assert(typeof failureType === 'string' || typeof failureType === 'symbol',
Expand Down
10 changes: 8 additions & 2 deletions lib/internal/main/test_runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ if (shardOption) {
};
}

run({ concurrency, inspectPort, watch: getOptionValue('--watch'), setup: setupTestReporters, shard })
.once('test:fail', () => {
run({
concurrency,
inspectPort,
watch: getOptionValue('--watch'),
setup: setupTestReporters,
shard,
bail: getOptionValue('--test-bail'),
}).once('test:fail', () => {
process.exitCode = kGenericUserError;
});
19 changes: 14 additions & 5 deletions lib/internal/test_runner/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_TEST_FAILURE,
ERR_TEST_BAILOUT,
},
AbortError,
} = require('internal/errors');
Expand Down Expand Up @@ -71,8 +72,9 @@ const kHookNames = ObjectSeal(['before', 'after', 'beforeEach', 'afterEach']);
const kUnwrapErrors = new SafeSet()
.add(kTestCodeFailure).add(kHookFailure)
.add('uncaughtException').add('unhandledRejection');
const { testNamePatterns, testOnlyFlag } = parseCommandLine();
const { testNamePatterns, testOnlyFlag, bail } = parseCommandLine();
let kResistStopPropagation;
let bailedOut = false;

function stopTest(timeout, signal) {
if (timeout === kDefaultTimeout) {
Expand Down Expand Up @@ -421,11 +423,13 @@ class Test extends AsyncResource {
return;
}

const unknownError = bailedOut ? new ERR_TEST_BAILOUT('test bailed out') : new ERR_TEST_FAILURE(
'test did not finish before its parent and was cancelled',
kCancelledByParent,
);

this.fail(error ||
new ERR_TEST_FAILURE(
'test did not finish before its parent and was cancelled',
kCancelledByParent,
),
unknownError,
);
this.startTime = this.startTime || this.endTime; // If a test was canceled before it was started, e.g inside a hook
this.cancelled = true;
Expand All @@ -444,6 +448,7 @@ class Test extends AsyncResource {
}

fail(err) {
bailedOut = bail;
if (this.error !== null) {
return;
}
Expand Down Expand Up @@ -526,6 +531,10 @@ class Test extends AsyncResource {
}

async run(pendingSubtestsError) {
if (bailedOut) {
return;
}

if (this.parent !== null) {
this.parent.activeSubtests++;
}
Expand Down
2 changes: 2 additions & 0 deletions lib/internal/test_runner/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ function parseCommandLine() {
}

const isTestRunner = getOptionValue('--test');
const bail = getOptionValue('--test-bail');
const coverage = getOptionValue('--experimental-test-coverage');
const isChildProcess = process.env.NODE_TEST_CONTEXT === 'child';
const isChildProcessV8 = process.env.NODE_TEST_CONTEXT === 'child-v8';
Expand Down Expand Up @@ -230,6 +231,7 @@ function parseCommandLine() {
globalTestOptions = {
__proto__: null,
isTestRunner,
bail,
coverage,
testOnlyFlag,
testNamePatterns,
Expand Down
3 changes: 3 additions & 0 deletions src/node_options.cc
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,9 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
"profile generated with --heap-prof. (default: 512 * 1024)",
&EnvironmentOptions::heap_prof_interval);
#endif // HAVE_INSPECTOR
AddOption("--test-bail",
"stop test execution when given number of tests have failed",
&EnvironmentOptions::test_bail);
AddOption("--max-http-header-size",
"set the maximum size of HTTP headers (default: 16384 (16KB))",
&EnvironmentOptions::max_http_header_size,
Expand Down
1 change: 1 addition & 0 deletions src/node_options.h
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ class EnvironmentOptions : public Options {
std::string redirect_warnings;
std::string diagnostic_dir;
bool test_runner = false;
bool test_bail = false;
bool test_runner_coverage = false;
std::vector<std::string> test_name_pattern;
std::vector<std::string> test_reporter;
Expand Down
18 changes: 18 additions & 0 deletions test/fixtures/test-runner/bail/bail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const assert = require('assert');
const test = require('node:test');

test('nested', (t) => {
t.test('first', () => {});
t.test('second', () => {
throw new Error();
});
t.test('third', () => {});
});

test('top level', (t) => {
t.test('forth', () => {});
t.test('fifth', () => {
throw new Error();
});
});

8 changes: 8 additions & 0 deletions test/fixtures/test-runner/bail/multiple.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const test = require('node:test');

test('multiple', (t) => {
t.test('ok', () => {});
t.test('failing', () => {
throw new Error('first');
});
});
37 changes: 37 additions & 0 deletions test/parallel/test-runner-bail.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
'use strict';

require('../common');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const { describe, it } = require('node:test');
const { spawnSync } = require('node:child_process');
const assert = require('node:assert');

const testFile = fixtures.path('test-runner/bail/bail.js');
tmpdir.refresh();

describe('node:test bail', () => {
it('should exit at first failure', async () => {
const child = spawnSync(process.execPath, ['--test', '--test-bail', testFile]);
assert.strictEqual(child.stderr.toString(), '');
assert.match(child.stdout.toString(), /ok 1 - first/);
assert.match(child.stdout.toString(), /not ok 2 - second/);
assert.match(child.stdout.toString(), /ok 3 - third/);
assert.match(child.stdout.toString(), /not ok 1 - nested/);
assert.doesNotMatch(child.stdout.toString(), /ok 1 - ok forth/);
assert.doesNotMatch(child.stdout.toString(), /not ok 2 - fifth/);
assert.doesNotMatch(child.stdout.toString(), /Subtest: top level/);
});

it('should exit not exit if bail isnt set', async () => {
const child = spawnSync(process.execPath, ['--test', testFile]);
assert.strictEqual(child.stderr.toString(), '');
assert.match(child.stdout.toString(), /ok 1 - first/);
assert.match(child.stdout.toString(), /not ok 2 - second/);
assert.match(child.stdout.toString(), /not ok 3 - third/);
assert.match(child.stdout.toString(), /not ok 1 - nested/);
assert.match(child.stdout.toString(), /ok 1 - forth/);
assert.match(child.stdout.toString(), /not ok 2 - fifth/);
assert.match(child.stdout.toString(), /not ok 2 - top level/);
});
});

0 comments on commit 41aa463

Please sign in to comment.