mirror of
https://github.com/nodejs/node.git
synced 2026-10-14 10:26:26 -04:00
test_runner: support 'only' tests
This commit introduces a CLI flag and test runner functionality to support running a subset of tests that are indicated by an 'only' option passed to the test. PR-URL: https://github.com/nodejs/node/pull/42514 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
1 parent
0c9273d126
commit
54819f08e0
10 files changed
+283
-14
No files matched your search
@@ -1052,6 +1052,15 @@ minimum allocation from the secure heap. The minimum value is `2`.
|
||||
The maximum value is the lesser of `--secure-heap` or `2147483647`.
|
||||
The value given must be a power of two.
|
||||
|
||||
### `--test-only`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
Configures the test runner to only execute top level tests that have the `only`
|
||||
option set.
|
||||
|
||||
### `--throw-deprecation`
|
||||
|
||||
<!-- YAML
|
||||
@@ -1641,6 +1650,7 @@ Node.js options that are allowed are:
|
||||
* `--require`, `-r`
|
||||
* `--secure-heap-min`
|
||||
* `--secure-heap`
|
||||
* `--test-only`
|
||||
* `--throw-deprecation`
|
||||
* `--title`
|
||||
* `--tls-cipher-list`
|
||||
|
||||
@@ -148,6 +148,42 @@ test('skip() method with message', (t) => {
|
||||
});
|
||||
```
|
||||
|
||||
### `only` tests
|
||||
|
||||
If Node.js is started with the [`--test-only`][] command-line option, it is
|
||||
possible to skip all top level tests except for a selected subset by passing
|
||||
the `only` option to the tests that should be run. When a test with the `only`
|
||||
option set is run, all subtests are also run. The test context's `runOnly()`
|
||||
method can be used to implement the same behavior at the subtest level.
|
||||
|
||||
```js
|
||||
// Assume Node.js is run with the --test-only command-line option.
|
||||
// The 'only' option is set, so this test is run.
|
||||
test('this test is run', { only: true }, async (t) => {
|
||||
// Within this test, all subtests are run by default.
|
||||
await t.test('running subtest');
|
||||
|
||||
// The test context can be updated to run subtests with the 'only' option.
|
||||
t.runOnly(true);
|
||||
await t.test('this subtest is now skipped');
|
||||
await t.test('this subtest is run', { only: true });
|
||||
|
||||
// Switch the context back to execute all tests.
|
||||
t.runOnly(false);
|
||||
await t.test('this subtest is now run');
|
||||
|
||||
// Explicitly do not run these tests.
|
||||
await t.test('skipped subtest 3', { only: false });
|
||||
await t.test('skipped subtest 4', { skip: true });
|
||||
});
|
||||
|
||||
// The 'only' option is not set, so this test is skipped.
|
||||
test('this test is not run', () => {
|
||||
// This code is not run.
|
||||
throw new Error('fail');
|
||||
});
|
||||
```
|
||||
|
||||
## Extraneous asynchronous activity
|
||||
|
||||
Once a test function finishes executing, the TAP results are output as quickly
|
||||
@@ -197,6 +233,9 @@ added: REPLACEME
|
||||
* `concurrency` {number} The number of tests that can be run at the same time.
|
||||
If unspecified, subtests inherit this value from their parent.
|
||||
**Default:** `1`.
|
||||
* `only` {boolean} If truthy, and the test context is configured to run
|
||||
`only` tests, then this test will be run. Otherwise, the test is skipped.
|
||||
**Default:** `false`.
|
||||
* `skip` {boolean|string} If truthy, the test is skipped. If a string is
|
||||
provided, that string is displayed in the test results as the reason for
|
||||
skipping the test. **Default:** `false`.
|
||||
@@ -257,6 +296,19 @@ This function is used to write TAP diagnostics to the output. Any diagnostic
|
||||
information is included at the end of the test's results. This function does
|
||||
not return a value.
|
||||
|
||||
### `context.runOnly(shouldRunOnlyTests)`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `shouldRunOnlyTests` {boolean} Whether or not to run `only` tests.
|
||||
|
||||
If `shouldRunOnlyTests` is truthy, the test context will only run tests that
|
||||
have the `only` option set. Otherwise, all tests are run. If Node.js was not
|
||||
started with the [`--test-only`][] command-line option, this function is a
|
||||
no-op.
|
||||
|
||||
### `context.skip([message])`
|
||||
|
||||
<!-- YAML
|
||||
@@ -296,6 +348,9 @@ added: REPLACEME
|
||||
* `concurrency` {number} The number of tests that can be run at the same time.
|
||||
If unspecified, subtests inherit this value from their parent.
|
||||
**Default:** `1`.
|
||||
* `only` {boolean} If truthy, and the test context is configured to run
|
||||
`only` tests, then this test will be run. Otherwise, the test is skipped.
|
||||
**Default:** `false`.
|
||||
* `skip` {boolean|string} If truthy, the test is skipped. If a string is
|
||||
provided, that string is displayed in the test results as the reason for
|
||||
skipping the test. **Default:** `false`.
|
||||
@@ -312,5 +367,6 @@ This function is used to create subtests under the current test. This function
|
||||
behaves in the same fashion as the top level [`test()`][] function.
|
||||
|
||||
[TAP]: https://testanything.org/
|
||||
[`--test-only`]: cli.md#--test-only
|
||||
[`TestContext`]: #class-testcontext
|
||||
[`test()`]: #testname-options-fn
|
||||
@@ -381,6 +381,10 @@ the secure heap. The default is 0. The value must be a power of two.
|
||||
.It Fl -secure-heap-min Ns = Ns Ar n
|
||||
Specify the minimum allocation from the OpenSSL secure heap. The default is 2. The value must be a power of two.
|
||||
.
|
||||
.It Fl -test-only
|
||||
Configures the test runner to only execute top level tests that have the `only`
|
||||
option set.
|
||||
.
|
||||
.It Fl -throw-deprecation
|
||||
Throw errors for deprecations.
|
||||
.
|
||||
|
||||
@@ -13,6 +13,7 @@ const {
|
||||
ERR_TEST_FAILURE,
|
||||
},
|
||||
} = require('internal/errors');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
const { TapStream } = require('internal/test_runner/tap_stream');
|
||||
const { createDeferredPromise } = require('internal/util');
|
||||
const { isPromise } = require('internal/util/types');
|
||||
@@ -26,6 +27,7 @@ const kSubtestsFailed = 'subtestsFailed';
|
||||
const kTestCodeFailure = 'testCodeFailure';
|
||||
const kDefaultIndent = ' ';
|
||||
const noop = FunctionPrototype;
|
||||
const testOnlyFlag = getOptionValue('--test-only');
|
||||
|
||||
class TestContext {
|
||||
#test;
|
||||
@@ -38,6 +40,10 @@ class TestContext {
|
||||
this.#test.diagnostic(message);
|
||||
}
|
||||
|
||||
runOnly(value) {
|
||||
this.#test.runOnlySubtests = !!value;
|
||||
}
|
||||
|
||||
skip(message) {
|
||||
this.#test.skip(message);
|
||||
}
|
||||
@@ -57,8 +63,8 @@ class Test extends AsyncResource {
|
||||
constructor(options) {
|
||||
super('Test');
|
||||
|
||||
let { fn, name, parent } = options;
|
||||
const { concurrency, skip, todo } = options;
|
||||
let { fn, name, parent, skip } = options;
|
||||
const { concurrency, only, todo } = options;
|
||||
|
||||
if (typeof fn !== 'function') {
|
||||
fn = noop;
|
||||
@@ -72,19 +78,13 @@ class Test extends AsyncResource {
|
||||
parent = null;
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
fn = noop;
|
||||
}
|
||||
|
||||
this.fn = fn;
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
|
||||
if (parent === null) {
|
||||
this.concurrency = 1;
|
||||
this.indent = '';
|
||||
this.indentString = kDefaultIndent;
|
||||
this.only = testOnlyFlag;
|
||||
this.reporter = new TapStream();
|
||||
this.runOnlySubtests = this.only;
|
||||
this.testNumber = 0;
|
||||
} else {
|
||||
const indent = parent.parent === null ? parent.indent :
|
||||
@@ -93,7 +93,9 @@ class Test extends AsyncResource {
|
||||
this.concurrency = parent.concurrency;
|
||||
this.indent = indent;
|
||||
this.indentString = parent.indentString;
|
||||
this.only = only ?? !parent.runOnlySubtests;
|
||||
this.reporter = parent.reporter;
|
||||
this.runOnlySubtests = !this.only;
|
||||
this.testNumber = parent.subtests.length + 1;
|
||||
}
|
||||
|
||||
@@ -101,6 +103,17 @@ class Test extends AsyncResource {
|
||||
this.concurrency = concurrency;
|
||||
}
|
||||
|
||||
if (testOnlyFlag && !this.only) {
|
||||
skip = '\'only\' option not set';
|
||||
}
|
||||
|
||||
if (skip) {
|
||||
fn = noop;
|
||||
}
|
||||
|
||||
this.fn = fn;
|
||||
this.name = name;
|
||||
this.parent = parent;
|
||||
this.cancelled = false;
|
||||
this.skipped = !!skip;
|
||||
this.isTodo = !!todo;
|
||||
|
||||
@@ -498,6 +498,10 @@ EnvironmentOptionsParser::EnvironmentOptionsParser() {
|
||||
"write warnings to file instead of stderr",
|
||||
&EnvironmentOptions::redirect_warnings,
|
||||
kAllowedInEnvironment);
|
||||
AddOption("--test-only",
|
||||
"run tests with 'only' option set",
|
||||
&EnvironmentOptions::test_only,
|
||||
kAllowedInEnvironment);
|
||||
AddOption("--test-udp-no-try-send", "", // For testing only.
|
||||
&EnvironmentOptions::test_udp_no_try_send);
|
||||
AddOption("--throw-deprecation",
|
||||
|
||||
@@ -148,6 +148,7 @@ class EnvironmentOptions : public Options {
|
||||
#endif // HAVE_INSPECTOR
|
||||
std::string redirect_warnings;
|
||||
std::string diagnostic_dir;
|
||||
bool test_only = false;
|
||||
bool test_udp_no_try_send = false;
|
||||
bool throw_deprecation = false;
|
||||
bool trace_atomics_wait = false;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// Flags: --no-warnings --test-only
|
||||
'use strict';
|
||||
require('../common');
|
||||
const test = require('node:test');
|
||||
|
||||
// These tests should be skipped based on the 'only' option.
|
||||
test('only = undefined');
|
||||
test('only = undefined, skip = string', { skip: 'skip message' });
|
||||
test('only = undefined, skip = true', { skip: true });
|
||||
test('only = undefined, skip = false', { skip: false });
|
||||
test('only = false', { only: false });
|
||||
test('only = false, skip = string', { only: false, skip: 'skip message' });
|
||||
test('only = false, skip = true', { only: false, skip: true });
|
||||
test('only = false, skip = false', { only: false, skip: false });
|
||||
|
||||
// These tests should be skipped based on the 'skip' option.
|
||||
test('only = true, skip = string', { only: true, skip: 'skip message' });
|
||||
test('only = true, skip = true', { only: true, skip: true });
|
||||
|
||||
// An 'only' test with subtests.
|
||||
test('only = true, with subtests', { only: true }, async (t) => {
|
||||
// These subtests should run.
|
||||
await t.test('running subtest 1');
|
||||
await t.test('running subtest 2');
|
||||
|
||||
// Switch the context to only execute 'only' tests.
|
||||
t.runOnly(true);
|
||||
await t.test('skipped subtest 1');
|
||||
await t.test('skipped subtest 2');
|
||||
await t.test('running subtest 3', { only: true });
|
||||
|
||||
// Switch the context back to execute all tests.
|
||||
t.runOnly(false);
|
||||
await t.test('running subtest 4', async (t) => {
|
||||
// These subtests should run.
|
||||
await t.test('running sub-subtest 1');
|
||||
await t.test('running sub-subtest 2');
|
||||
|
||||
// Switch the context to only execute 'only' tests.
|
||||
t.runOnly(true);
|
||||
await t.test('skipped sub-subtest 1');
|
||||
await t.test('skipped sub-subtest 2');
|
||||
});
|
||||
|
||||
// Explicitly do not run these tests.
|
||||
await t.test('skipped subtest 3', { only: false });
|
||||
await t.test('skipped subtest 4', { skip: true });
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
TAP version 13
|
||||
ok 1 - only = undefined # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 2 - only = undefined, skip = string # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 3 - only = undefined, skip = true # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 4 - only = undefined, skip = false # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 5 - only = false # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 6 - only = false, skip = string # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 7 - only = false, skip = true # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 8 - only = false, skip = false # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 9 - only = true, skip = string # SKIP skip message
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 10 - only = true, skip = true # SKIP
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 1 - running subtest 1
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 2 - running subtest 2
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 3 - skipped subtest 1 # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 4 - skipped subtest 2 # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 5 - running subtest 3
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 1 - running sub-subtest 1
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 2 - running sub-subtest 2
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 3 - skipped sub-subtest 1 # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 4 - skipped sub-subtest 2 # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
1..4
|
||||
ok 6 - running subtest 4
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 7 - skipped subtest 3 # SKIP 'only' option not set
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 8 - skipped subtest 4 # SKIP
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
1..8
|
||||
ok 11 - only = true, with subtests
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
1..11
|
||||
# tests 11
|
||||
# pass 1
|
||||
# fail 0
|
||||
# skipped 10
|
||||
# todo 0
|
||||
# duration_ms *
|
||||
@@ -286,3 +286,13 @@ test('callback async throw after done', (t, done) => {
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
test('only is set but not in only mode', { only: true }, async (t) => {
|
||||
// All of these subtests should run.
|
||||
await t.test('running subtest 1');
|
||||
t.runOnly(true);
|
||||
await t.test('running subtest 2');
|
||||
await t.test('running subtest 3', { only: true });
|
||||
t.runOnly(false);
|
||||
await t.test('running subtest 4');
|
||||
});
|
||||
@@ -402,7 +402,28 @@ ok 52 - callback async throw after done
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
not ok 53 - invalid subtest fail
|
||||
ok 1 - running subtest 1
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 2 - running subtest 2
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 3 - running subtest 3
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
ok 4 - running subtest 4
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
1..4
|
||||
ok 53 - only is set but not in only mode
|
||||
---
|
||||
duration_ms: *
|
||||
...
|
||||
not ok 54 - invalid subtest fail
|
||||
---
|
||||
duration_ms: *
|
||||
failureType: 'parentAlreadyFinished'
|
||||
@@ -411,15 +432,15 @@ not ok 53 - invalid subtest fail
|
||||
stack: |-
|
||||
*
|
||||
...
|
||||
1..53
|
||||
1..54
|
||||
# Warning: Test "unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
|
||||
# Warning: Test "async unhandled rejection - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from async unhandled rejection fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
|
||||
# Warning: Test "immediate throw - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from immediate throw fail" and would have caused the test to fail, but instead triggered an uncaughtException event.
|
||||
# Warning: Test "immediate reject - passes but warns" generated asynchronous activity after the test ended. This activity created the error "Error: rejected from immediate reject fail" and would have caused the test to fail, but instead triggered an unhandledRejection event.
|
||||
# Warning: Test "callback called twice in different ticks" generated asynchronous activity after the test ended. This activity created the error "Error [ERR_TEST_FAILURE]: 'callback invoked multiple times'" and would have caused the test to fail, but instead triggered an uncaughtException event.
|
||||
# Warning: Test "callback async throw after done" generated asynchronous activity after the test ended. This activity created the error "Error: thrown from callback async throw after done" and would have caused the test to fail, but instead triggered an uncaughtException event.
|
||||
# tests 53
|
||||
# pass 23
|
||||
# tests 54
|
||||
# pass 24
|
||||
# fail 15
|
||||
# skipped 10
|
||||
# todo 5
|
||||
|
||||
Reference in new issue
Block a user