mirror of
https://github.com/zebrajr/node.git
synced 2026-01-15 12:15:26 +00:00
test_runner: support programmatically running --test
PR-URL: https://github.com/nodejs/node/pull/44241 Fixes: https://github.com/nodejs/node/issues/44023 Fixes: https://github.com/nodejs/node/issues/43675 Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com> Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
This commit is contained in:
@@ -316,6 +316,35 @@ Otherwise, the test is considered to be a failure. Test files must be
|
||||
executable by Node.js, but are not required to use the `node:test` module
|
||||
internally.
|
||||
|
||||
## `run([options])`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* `options` {Object} Configuration options for running tests. The following
|
||||
properties are supported:
|
||||
* `concurrency` {number|boolean} If a number is provided,
|
||||
then that many files would run in parallel.
|
||||
If truthy, it would run (number of cpu cores - 1)
|
||||
files in parallel.
|
||||
If falsy, it would only run one file at a time.
|
||||
If unspecified, subtests inherit this value from their parent.
|
||||
**Default:** `true`.
|
||||
* `files`: {Array} An array containing the list of files to run.
|
||||
**Default** matching files from [test runner execution model][].
|
||||
* `signal` {AbortSignal} Allows aborting an in-progress test execution.
|
||||
* `timeout` {number} A number of milliseconds the test execution will
|
||||
fail after.
|
||||
If unspecified, subtests inherit this value from their parent.
|
||||
**Default:** `Infinity`.
|
||||
* Returns: {TapStream}
|
||||
|
||||
```js
|
||||
run({ files: [path.resolve('./tests/test.js')] })
|
||||
.pipe(process.stdout);
|
||||
```
|
||||
|
||||
## `test([name][, options][, fn])`
|
||||
|
||||
<!-- YAML
|
||||
@@ -564,6 +593,47 @@ describe('tests', async () => {
|
||||
});
|
||||
```
|
||||
|
||||
## Class: `TapStream`
|
||||
|
||||
<!-- YAML
|
||||
added: REPLACEME
|
||||
-->
|
||||
|
||||
* Extends {ReadableStream}
|
||||
|
||||
A successful call to [`run()`][] method will return a new {TapStream}
|
||||
object, streaming a [TAP][] output
|
||||
`TapStream` will emit events, in the order of the tests definition
|
||||
|
||||
### Event: `'test:diagnostic'`
|
||||
|
||||
* `message` {string} The diagnostic message.
|
||||
|
||||
Emitted when [`context.diagnostic`][] is called.
|
||||
|
||||
### Event: `'test:fail'`
|
||||
|
||||
* `data` {Object}
|
||||
* `duration` {number} The test duration.
|
||||
* `error` {Error} The failure casing test to fail.
|
||||
* `name` {string} The test name.
|
||||
* `testNumber` {number} The ordinal number of the test.
|
||||
* `todo` {string|undefined} Present if [`context.todo`][] is called
|
||||
* `skip` {string|undefined} Present if [`context.skip`][] is called
|
||||
|
||||
Emitted when a test fails.
|
||||
|
||||
### Event: `'test:pass'`
|
||||
|
||||
* `data` {Object}
|
||||
* `duration` {number} The test duration.
|
||||
* `name` {string} The test name.
|
||||
* `testNumber` {number} The ordinal number of the test.
|
||||
* `todo` {string|undefined} Present if [`context.todo`][] is called
|
||||
* `skip` {string|undefined} Present if [`context.skip`][] is called
|
||||
|
||||
Emitted when a test passes.
|
||||
|
||||
## Class: `TestContext`
|
||||
|
||||
<!-- YAML
|
||||
@@ -849,6 +919,10 @@ added:
|
||||
[`--test`]: cli.md#--test
|
||||
[`SuiteContext`]: #class-suitecontext
|
||||
[`TestContext`]: #class-testcontext
|
||||
[`context.diagnostic`]: #contextdiagnosticmessage
|
||||
[`context.skip`]: #contextskipmessage
|
||||
[`context.todo`]: #contexttodomessage
|
||||
[`run()`]: #runoptions
|
||||
[`test()`]: #testname-options-fn
|
||||
[describe options]: #describename-options-fn
|
||||
[it options]: #testname-options-fn
|
||||
|
||||
@@ -1,147 +1,15 @@
|
||||
'use strict';
|
||||
const {
|
||||
ArrayFrom,
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypeIncludes,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeSlice,
|
||||
ArrayPrototypeSort,
|
||||
SafePromiseAll,
|
||||
SafeSet,
|
||||
} = primordials;
|
||||
const {
|
||||
prepareMainThreadExecution,
|
||||
markBootstrapComplete
|
||||
} = require('internal/process/pre_execution');
|
||||
const { spawn } = require('child_process');
|
||||
const { readdirSync, statSync } = require('fs');
|
||||
const console = require('internal/console/global');
|
||||
const {
|
||||
codes: {
|
||||
ERR_TEST_FAILURE,
|
||||
},
|
||||
} = require('internal/errors');
|
||||
const { test } = require('internal/test_runner/harness');
|
||||
const { kSubtestsFailed } = require('internal/test_runner/test');
|
||||
const {
|
||||
isSupportedFileType,
|
||||
doesPathMatchFilter,
|
||||
} = require('internal/test_runner/utils');
|
||||
const { basename, join, resolve } = require('path');
|
||||
const { once } = require('events');
|
||||
const kFilterArgs = ['--test'];
|
||||
const { run } = require('internal/test_runner/runner');
|
||||
|
||||
prepareMainThreadExecution(false);
|
||||
markBootstrapComplete();
|
||||
|
||||
// TODO(cjihrig): Replace this with recursive readdir once it lands.
|
||||
function processPath(path, testFiles, options) {
|
||||
const stats = statSync(path);
|
||||
|
||||
if (stats.isFile()) {
|
||||
if (options.userSupplied ||
|
||||
(options.underTestDir && isSupportedFileType(path)) ||
|
||||
doesPathMatchFilter(path)) {
|
||||
testFiles.add(path);
|
||||
}
|
||||
} else if (stats.isDirectory()) {
|
||||
const name = basename(path);
|
||||
|
||||
if (!options.userSupplied && name === 'node_modules') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'test' directories get special treatment. Recursively add all .js,
|
||||
// .cjs, and .mjs files in the 'test' directory.
|
||||
const isTestDir = name === 'test';
|
||||
const { underTestDir } = options;
|
||||
const entries = readdirSync(path);
|
||||
|
||||
if (isTestDir) {
|
||||
options.underTestDir = true;
|
||||
}
|
||||
|
||||
options.userSupplied = false;
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
processPath(join(path, entries[i]), testFiles, options);
|
||||
}
|
||||
|
||||
options.underTestDir = underTestDir;
|
||||
}
|
||||
}
|
||||
|
||||
function createTestFileList() {
|
||||
const cwd = process.cwd();
|
||||
const hasUserSuppliedPaths = process.argv.length > 1;
|
||||
const testPaths = hasUserSuppliedPaths ?
|
||||
ArrayPrototypeSlice(process.argv, 1) : [cwd];
|
||||
const testFiles = new SafeSet();
|
||||
|
||||
try {
|
||||
for (let i = 0; i < testPaths.length; i++) {
|
||||
const absolutePath = resolve(testPaths[i]);
|
||||
|
||||
processPath(absolutePath, testFiles, { userSupplied: true });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.code === 'ENOENT') {
|
||||
console.error(`Could not find '${err.path}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return ArrayPrototypeSort(ArrayFrom(testFiles));
|
||||
}
|
||||
|
||||
function filterExecArgv(arg) {
|
||||
return !ArrayPrototypeIncludes(kFilterArgs, arg);
|
||||
}
|
||||
|
||||
function runTestFile(path) {
|
||||
return test(path, async (t) => {
|
||||
const args = ArrayPrototypeFilter(process.execArgv, filterExecArgv);
|
||||
ArrayPrototypePush(args, path);
|
||||
|
||||
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' });
|
||||
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
|
||||
// instead of just displaying it all if the child fails.
|
||||
let err;
|
||||
|
||||
child.on('error', (error) => {
|
||||
err = error;
|
||||
});
|
||||
|
||||
const { 0: { 0: code, 1: signal }, 1: stdout, 2: stderr } = await SafePromiseAll([
|
||||
once(child, 'exit', { signal: t.signal }),
|
||||
child.stdout.toArray({ signal: t.signal }),
|
||||
child.stderr.toArray({ signal: t.signal }),
|
||||
]);
|
||||
|
||||
if (code !== 0 || signal !== null) {
|
||||
if (!err) {
|
||||
err = new ERR_TEST_FAILURE('test failed', kSubtestsFailed);
|
||||
err.exitCode = code;
|
||||
err.signal = signal;
|
||||
err.stdout = ArrayPrototypeJoin(stdout, '');
|
||||
err.stderr = ArrayPrototypeJoin(stderr, '');
|
||||
// The stack will not be useful since the failures came from tests
|
||||
// in a child process.
|
||||
err.stack = undefined;
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
(async function main() {
|
||||
const testFiles = createTestFileList();
|
||||
|
||||
for (let i = 0; i < testFiles.length; i++) {
|
||||
runTestFile(testFiles[i]);
|
||||
}
|
||||
})();
|
||||
const tapStream = run();
|
||||
tapStream.pipe(process.stdout);
|
||||
tapStream.once('test:fail', () => {
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
'use strict';
|
||||
const {
|
||||
ArrayPrototypeForEach,
|
||||
FunctionPrototypeBind,
|
||||
SafeMap,
|
||||
SafeWeakSet,
|
||||
} = primordials;
|
||||
const {
|
||||
createHook,
|
||||
@@ -13,13 +13,18 @@ const {
|
||||
ERR_TEST_FAILURE,
|
||||
},
|
||||
} = require('internal/errors');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
const { getOptionValue } = require('internal/options');
|
||||
const { kCancelledByParent, Test, ItTest, Suite } = require('internal/test_runner/test');
|
||||
const { bigint: hrtime } = process.hrtime;
|
||||
|
||||
const isTestRunner = getOptionValue('--test');
|
||||
const isTestRunnerCli = getOptionValue('--test');
|
||||
const testResources = new SafeMap();
|
||||
const root = new Test({ __proto__: null, name: '<root>' });
|
||||
let wasRootSetup = false;
|
||||
const wasRootSetup = new SafeWeakSet();
|
||||
|
||||
function createTestTree(options = kEmptyObject) {
|
||||
return setup(new Test({ __proto__: null, ...options, name: '<root>' }));
|
||||
}
|
||||
|
||||
function createProcessEventHandler(eventName, rootTest) {
|
||||
return (err) => {
|
||||
@@ -48,7 +53,7 @@ function createProcessEventHandler(eventName, rootTest) {
|
||||
}
|
||||
|
||||
function setup(root) {
|
||||
if (wasRootSetup) {
|
||||
if (wasRootSetup.has(root)) {
|
||||
return root;
|
||||
}
|
||||
const hook = createHook({
|
||||
@@ -81,52 +86,9 @@ function setup(root) {
|
||||
'Promise resolution is still pending but the event loop has already resolved',
|
||||
kCancelledByParent));
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
let skipCount = 0;
|
||||
let todoCount = 0;
|
||||
let cancelledCount = 0;
|
||||
|
||||
for (let i = 0; i < root.subtests.length; i++) {
|
||||
const test = root.subtests[i];
|
||||
|
||||
// Check SKIP and TODO tests first, as those should not be counted as
|
||||
// failures.
|
||||
if (test.skipped) {
|
||||
skipCount++;
|
||||
} else if (test.isTodo) {
|
||||
todoCount++;
|
||||
} else if (test.cancelled) {
|
||||
cancelledCount++;
|
||||
} else if (!test.passed) {
|
||||
failCount++;
|
||||
} else {
|
||||
passCount++;
|
||||
}
|
||||
}
|
||||
|
||||
root.reporter.plan(root.indent, root.subtests.length);
|
||||
|
||||
for (let i = 0; i < root.diagnostics.length; i++) {
|
||||
root.reporter.diagnostic(root.indent, root.diagnostics[i]);
|
||||
}
|
||||
|
||||
root.reporter.diagnostic(root.indent, `tests ${root.subtests.length}`);
|
||||
root.reporter.diagnostic(root.indent, `pass ${passCount}`);
|
||||
root.reporter.diagnostic(root.indent, `fail ${failCount}`);
|
||||
root.reporter.diagnostic(root.indent, `cancelled ${cancelledCount}`);
|
||||
root.reporter.diagnostic(root.indent, `skipped ${skipCount}`);
|
||||
root.reporter.diagnostic(root.indent, `todo ${todoCount}`);
|
||||
root.reporter.diagnostic(root.indent, `duration_ms ${process.uptime()}`);
|
||||
|
||||
root.reporter.push(null);
|
||||
hook.disable();
|
||||
process.removeListener('unhandledRejection', rejectionHandler);
|
||||
process.removeListener('uncaughtException', exceptionHandler);
|
||||
|
||||
if (failCount > 0 || cancelledCount > 0) {
|
||||
process.exitCode = 1;
|
||||
}
|
||||
};
|
||||
|
||||
const terminationHandler = () => {
|
||||
@@ -137,29 +99,41 @@ function setup(root) {
|
||||
process.on('uncaughtException', exceptionHandler);
|
||||
process.on('unhandledRejection', rejectionHandler);
|
||||
process.on('beforeExit', exitHandler);
|
||||
// TODO(MoLow): Make it configurable to hook when isTestRunner === false.
|
||||
if (isTestRunner) {
|
||||
// TODO(MoLow): Make it configurable to hook when isTestRunnerCli === false.
|
||||
if (isTestRunnerCli) {
|
||||
process.on('SIGINT', terminationHandler);
|
||||
process.on('SIGTERM', terminationHandler);
|
||||
}
|
||||
|
||||
root.reporter.pipe(process.stdout);
|
||||
root.startTime = hrtime();
|
||||
root.reporter.version();
|
||||
|
||||
wasRootSetup = true;
|
||||
wasRootSetup.add(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
let globalRoot;
|
||||
function getGlobalRoot() {
|
||||
if (!globalRoot) {
|
||||
globalRoot = createTestTree();
|
||||
globalRoot.reporter.pipe(process.stdout);
|
||||
globalRoot.reporter.once('test:fail', () => {
|
||||
process.exitCode = 1;
|
||||
});
|
||||
}
|
||||
return globalRoot;
|
||||
}
|
||||
|
||||
function test(name, options, fn) {
|
||||
const subtest = setup(root).createSubtest(Test, name, options, fn);
|
||||
const subtest = getGlobalRoot().createSubtest(Test, name, options, fn);
|
||||
return subtest.start();
|
||||
}
|
||||
|
||||
function runInParentContext(Factory) {
|
||||
function run(name, options, fn, overrides) {
|
||||
const parent = testResources.get(executionAsyncId()) || setup(root);
|
||||
const parent = testResources.get(executionAsyncId()) || getGlobalRoot();
|
||||
const subtest = parent.createSubtest(Factory, name, options, fn, overrides);
|
||||
if (parent === root) {
|
||||
if (parent === getGlobalRoot()) {
|
||||
subtest.start();
|
||||
}
|
||||
}
|
||||
@@ -178,13 +152,14 @@ function runInParentContext(Factory) {
|
||||
|
||||
function hook(hook) {
|
||||
return (fn, options) => {
|
||||
const parent = testResources.get(executionAsyncId()) || setup(root);
|
||||
const parent = testResources.get(executionAsyncId()) || getGlobalRoot();
|
||||
parent.createHook(hook, fn, options);
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
test: FunctionPrototypeBind(test, root),
|
||||
createTestTree,
|
||||
test,
|
||||
describe: runInParentContext(Suite),
|
||||
it: runInParentContext(ItTest),
|
||||
before: hook('before'),
|
||||
|
||||
163
lib/internal/test_runner/runner.js
Normal file
163
lib/internal/test_runner/runner.js
Normal file
@@ -0,0 +1,163 @@
|
||||
'use strict';
|
||||
const {
|
||||
ArrayFrom,
|
||||
ArrayPrototypeConcat,
|
||||
ArrayPrototypeFilter,
|
||||
ArrayPrototypeIncludes,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypeSlice,
|
||||
ArrayPrototypeSort,
|
||||
ObjectAssign,
|
||||
PromisePrototypeThen,
|
||||
SafePromiseAll,
|
||||
SafeSet,
|
||||
} = primordials;
|
||||
|
||||
const { spawn } = require('child_process');
|
||||
const { readdirSync, statSync } = require('fs');
|
||||
const console = require('internal/console/global');
|
||||
const {
|
||||
codes: {
|
||||
ERR_TEST_FAILURE,
|
||||
},
|
||||
} = require('internal/errors');
|
||||
const { validateArray } = require('internal/validators');
|
||||
const { kEmptyObject } = require('internal/util');
|
||||
const { createTestTree } = require('internal/test_runner/harness');
|
||||
const { kSubtestsFailed, Test } = require('internal/test_runner/test');
|
||||
const {
|
||||
isSupportedFileType,
|
||||
doesPathMatchFilter,
|
||||
} = require('internal/test_runner/utils');
|
||||
const { basename, join, resolve } = require('path');
|
||||
const { once } = require('events');
|
||||
|
||||
const kFilterArgs = ['--test'];
|
||||
|
||||
// TODO(cjihrig): Replace this with recursive readdir once it lands.
|
||||
function processPath(path, testFiles, options) {
|
||||
const stats = statSync(path);
|
||||
|
||||
if (stats.isFile()) {
|
||||
if (options.userSupplied ||
|
||||
(options.underTestDir && isSupportedFileType(path)) ||
|
||||
doesPathMatchFilter(path)) {
|
||||
testFiles.add(path);
|
||||
}
|
||||
} else if (stats.isDirectory()) {
|
||||
const name = basename(path);
|
||||
|
||||
if (!options.userSupplied && name === 'node_modules') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 'test' directories get special treatment. Recursively add all .js,
|
||||
// .cjs, and .mjs files in the 'test' directory.
|
||||
const isTestDir = name === 'test';
|
||||
const { underTestDir } = options;
|
||||
const entries = readdirSync(path);
|
||||
|
||||
if (isTestDir) {
|
||||
options.underTestDir = true;
|
||||
}
|
||||
|
||||
options.userSupplied = false;
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
processPath(join(path, entries[i]), testFiles, options);
|
||||
}
|
||||
|
||||
options.underTestDir = underTestDir;
|
||||
}
|
||||
}
|
||||
|
||||
function createTestFileList() {
|
||||
const cwd = process.cwd();
|
||||
const hasUserSuppliedPaths = process.argv.length > 1;
|
||||
const testPaths = hasUserSuppliedPaths ?
|
||||
ArrayPrototypeSlice(process.argv, 1) : [cwd];
|
||||
const testFiles = new SafeSet();
|
||||
|
||||
try {
|
||||
for (let i = 0; i < testPaths.length; i++) {
|
||||
const absolutePath = resolve(testPaths[i]);
|
||||
|
||||
processPath(absolutePath, testFiles, { userSupplied: true });
|
||||
}
|
||||
} catch (err) {
|
||||
if (err?.code === 'ENOENT') {
|
||||
console.error(`Could not find '${err.path}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return ArrayPrototypeSort(ArrayFrom(testFiles));
|
||||
}
|
||||
|
||||
function filterExecArgv(arg) {
|
||||
return !ArrayPrototypeIncludes(kFilterArgs, arg);
|
||||
}
|
||||
|
||||
function runTestFile(path, root) {
|
||||
const subtest = root.createSubtest(Test, path, async (t) => {
|
||||
const args = ArrayPrototypeConcat(
|
||||
ArrayPrototypeFilter(process.execArgv, filterExecArgv),
|
||||
path);
|
||||
|
||||
const child = spawn(process.execPath, args, { signal: t.signal, encoding: 'utf8' });
|
||||
// TODO(cjihrig): Implement a TAP parser to read the child's stdout
|
||||
// instead of just displaying it all if the child fails.
|
||||
let err;
|
||||
|
||||
child.on('error', (error) => {
|
||||
err = error;
|
||||
});
|
||||
|
||||
const { 0: { 0: code, 1: signal }, 1: stdout, 2: stderr } = await SafePromiseAll([
|
||||
once(child, 'exit', { signal: t.signal }),
|
||||
child.stdout.toArray({ signal: t.signal }),
|
||||
child.stderr.toArray({ signal: t.signal }),
|
||||
]);
|
||||
|
||||
if (code !== 0 || signal !== null) {
|
||||
if (!err) {
|
||||
err = ObjectAssign(new ERR_TEST_FAILURE('test failed', kSubtestsFailed), {
|
||||
__proto__: null,
|
||||
exitCode: code,
|
||||
signal: signal,
|
||||
stdout: ArrayPrototypeJoin(stdout, ''),
|
||||
stderr: ArrayPrototypeJoin(stderr, ''),
|
||||
// The stack will not be useful since the failures came from tests
|
||||
// in a child process.
|
||||
stack: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
throw err;
|
||||
}
|
||||
});
|
||||
return subtest.start();
|
||||
}
|
||||
|
||||
function run(options) {
|
||||
if (options === null || typeof options !== 'object') {
|
||||
options = kEmptyObject;
|
||||
}
|
||||
const { concurrency, timeout, signal, files } = options;
|
||||
|
||||
if (files != null) {
|
||||
validateArray(files, 'options.files');
|
||||
}
|
||||
|
||||
const root = createTestTree({ concurrency, timeout, signal });
|
||||
const testFiles = files ?? createTestFileList();
|
||||
|
||||
PromisePrototypeThen(SafePromiseAll(testFiles, (path) => runTestFile(path, root)),
|
||||
() => root.postRun());
|
||||
|
||||
return root.reporter;
|
||||
}
|
||||
|
||||
module.exports = { run };
|
||||
@@ -2,6 +2,7 @@
|
||||
const {
|
||||
ArrayPrototypeForEach,
|
||||
ArrayPrototypeJoin,
|
||||
ArrayPrototypeMap,
|
||||
ArrayPrototypePush,
|
||||
ArrayPrototypeShift,
|
||||
ObjectEntries,
|
||||
@@ -11,7 +12,7 @@ const {
|
||||
} = primordials;
|
||||
const { inspectWithNoCustomRetry } = require('internal/errors');
|
||||
const Readable = require('internal/streams/readable');
|
||||
const { isError } = require('internal/util');
|
||||
const { isError, kEmptyObject } = require('internal/util');
|
||||
const kFrameStartRegExp = /^ {4}at /;
|
||||
const kLineBreakRegExp = /\n|\r\n/;
|
||||
const inspectOptions = { colors: false, breakLength: Infinity };
|
||||
@@ -49,12 +50,16 @@ class TapStream extends Readable {
|
||||
this.#tryPush(`Bail out!${message ? ` ${tapEscape(message)}` : ''}\n`);
|
||||
}
|
||||
|
||||
fail(indent, testNumber, description, directive) {
|
||||
this.#test(indent, testNumber, 'not ok', description, directive);
|
||||
fail(indent, testNumber, name, duration, error, directive) {
|
||||
this.emit('test:fail', { __proto__: null, name, testNumber, duration, ...directive, error });
|
||||
this.#test(indent, testNumber, 'not ok', name, directive);
|
||||
this.#details(indent, duration, error);
|
||||
}
|
||||
|
||||
ok(indent, testNumber, description, directive) {
|
||||
this.#test(indent, testNumber, 'ok', description, directive);
|
||||
ok(indent, testNumber, name, duration, directive) {
|
||||
this.emit('test:pass', { __proto__: null, name, testNumber, duration, ...directive });
|
||||
this.#test(indent, testNumber, 'ok', name, directive);
|
||||
this.#details(indent, duration, null);
|
||||
}
|
||||
|
||||
plan(indent, count, explanation) {
|
||||
@@ -64,18 +69,18 @@ class TapStream extends Readable {
|
||||
}
|
||||
|
||||
getSkip(reason) {
|
||||
return `SKIP${reason ? ` ${tapEscape(reason)}` : ''}`;
|
||||
return { __proto__: null, skip: reason };
|
||||
}
|
||||
|
||||
getTodo(reason) {
|
||||
return `TODO${reason ? ` ${tapEscape(reason)}` : ''}`;
|
||||
return { __proto__: null, todo: reason };
|
||||
}
|
||||
|
||||
subtest(indent, name) {
|
||||
this.#tryPush(`${indent}# Subtest: ${tapEscape(name)}\n`);
|
||||
}
|
||||
|
||||
details(indent, duration, error) {
|
||||
#details(indent, duration, error) {
|
||||
let details = `${indent} ---\n`;
|
||||
|
||||
details += jsToYaml(indent, 'duration_ms', duration);
|
||||
@@ -85,6 +90,7 @@ class TapStream extends Readable {
|
||||
}
|
||||
|
||||
diagnostic(indent, message) {
|
||||
this.emit('test:diagnostic', message);
|
||||
this.#tryPush(`${indent}# ${tapEscape(message)}\n`);
|
||||
}
|
||||
|
||||
@@ -92,16 +98,16 @@ class TapStream extends Readable {
|
||||
this.#tryPush('TAP version 13\n');
|
||||
}
|
||||
|
||||
#test(indent, testNumber, status, description, directive) {
|
||||
#test(indent, testNumber, status, name, directive = kEmptyObject) {
|
||||
let line = `${indent}${status} ${testNumber}`;
|
||||
|
||||
if (description) {
|
||||
line += ` ${tapEscape(description)}`;
|
||||
if (name) {
|
||||
line += ` ${tapEscape(`- ${name}`)}`;
|
||||
}
|
||||
|
||||
if (directive) {
|
||||
line += ` # ${directive}`;
|
||||
}
|
||||
line += ArrayPrototypeJoin(ArrayPrototypeMap(ObjectEntries(directive), ({ 0: key, 1: value }) => (
|
||||
` # ${key.toUpperCase()}${value ? ` ${tapEscape(value)}` : ''}`
|
||||
)), '');
|
||||
|
||||
line += '\n';
|
||||
this.#tryPush(line);
|
||||
|
||||
@@ -176,7 +176,7 @@ class Test extends AsyncResource {
|
||||
|
||||
case 'boolean':
|
||||
if (concurrency) {
|
||||
this.concurrency = isTestRunner ? MathMax(cpus().length - 1, 1) : Infinity;
|
||||
this.concurrency = parent === null ? MathMax(cpus().length - 1, 1) : Infinity;
|
||||
} else {
|
||||
this.concurrency = 1;
|
||||
}
|
||||
@@ -512,7 +512,7 @@ class Test extends AsyncResource {
|
||||
}
|
||||
|
||||
postRun(pendingSubtestsError) {
|
||||
let failedSubtests = 0;
|
||||
const counters = { __proto__: null, failed: 0, passed: 0, cancelled: 0, skipped: 0, todo: 0, totalFailed: 0 };
|
||||
|
||||
// If the test was failed before it even started, then the end time will
|
||||
// be earlier than the start time. Correct that here.
|
||||
@@ -532,14 +532,28 @@ class Test extends AsyncResource {
|
||||
subtest.postRun(pendingSubtestsError);
|
||||
}
|
||||
|
||||
// Check SKIP and TODO tests first, as those should not be counted as
|
||||
// failures.
|
||||
if (subtest.skipped) {
|
||||
counters.skipped++;
|
||||
} else if (subtest.isTodo) {
|
||||
counters.todo++;
|
||||
} else if (subtest.cancelled) {
|
||||
counters.cancelled++;
|
||||
} else if (!subtest.passed) {
|
||||
counters.failed++;
|
||||
} else {
|
||||
counters.passed++;
|
||||
}
|
||||
|
||||
if (!subtest.passed) {
|
||||
failedSubtests++;
|
||||
counters.totalFailed++;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.passed && failedSubtests > 0) {
|
||||
const subtestString = `subtest${failedSubtests > 1 ? 's' : ''}`;
|
||||
const msg = `${failedSubtests} ${subtestString} failed`;
|
||||
if ((this.passed || this.parent === null) && counters.totalFailed > 0) {
|
||||
const subtestString = `subtest${counters.totalFailed > 1 ? 's' : ''}`;
|
||||
const msg = `${counters.totalFailed} ${subtestString} failed`;
|
||||
|
||||
this.fail(new ERR_TEST_FAILURE(msg, kSubtestsFailed));
|
||||
}
|
||||
@@ -551,6 +565,22 @@ class Test extends AsyncResource {
|
||||
this.parent.addReadySubtest(this);
|
||||
this.parent.processReadySubtestRange(false);
|
||||
this.parent.processPendingSubtests();
|
||||
} else if (!this.reported) {
|
||||
this.reported = true;
|
||||
this.reporter.plan(this.indent, this.subtests.length);
|
||||
|
||||
for (let i = 0; i < this.diagnostics.length; i++) {
|
||||
this.reporter.diagnostic(this.indent, this.diagnostics[i]);
|
||||
}
|
||||
|
||||
this.reporter.diagnostic(this.indent, `tests ${this.subtests.length}`);
|
||||
this.reporter.diagnostic(this.indent, `pass ${counters.passed}`);
|
||||
this.reporter.diagnostic(this.indent, `fail ${counters.failed}`);
|
||||
this.reporter.diagnostic(this.indent, `cancelled ${counters.cancelled}`);
|
||||
this.reporter.diagnostic(this.indent, `skipped ${counters.skipped}`);
|
||||
this.reporter.diagnostic(this.indent, `todo ${counters.todo}`);
|
||||
this.reporter.diagnostic(this.indent, `duration_ms ${this.#duration()}`);
|
||||
this.reporter.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,10 +614,12 @@ class Test extends AsyncResource {
|
||||
this.finished = true;
|
||||
}
|
||||
|
||||
report() {
|
||||
#duration() {
|
||||
// Duration is recorded in BigInt nanoseconds. Convert to seconds.
|
||||
const duration = Number(this.endTime - this.startTime) / 1_000_000_000;
|
||||
const message = `- ${this.name}`;
|
||||
return Number(this.endTime - this.startTime) / 1_000_000_000;
|
||||
}
|
||||
|
||||
report() {
|
||||
let directive;
|
||||
|
||||
if (this.skipped) {
|
||||
@@ -597,13 +629,11 @@ class Test extends AsyncResource {
|
||||
}
|
||||
|
||||
if (this.passed) {
|
||||
this.reporter.ok(this.indent, this.testNumber, message, directive);
|
||||
this.reporter.ok(this.indent, this.testNumber, this.name, this.#duration(), directive);
|
||||
} else {
|
||||
this.reporter.fail(this.indent, this.testNumber, message, directive);
|
||||
this.reporter.fail(this.indent, this.testNumber, this.name, this.#duration(), this.error, directive);
|
||||
}
|
||||
|
||||
this.reporter.details(this.indent, duration, this.error);
|
||||
|
||||
for (let i = 0; i < this.diagnostics.length; i++) {
|
||||
this.reporter.diagnostic(this.indent, this.diagnostics[i]);
|
||||
}
|
||||
@@ -626,6 +656,8 @@ class TestHook extends Test {
|
||||
getRunArgs() {
|
||||
return this.#args;
|
||||
}
|
||||
postRun() {
|
||||
}
|
||||
}
|
||||
|
||||
class ItTest extends Test {
|
||||
|
||||
19
lib/test.js
19
lib/test.js
@@ -1,14 +1,19 @@
|
||||
'use strict';
|
||||
const { ObjectAssign } = primordials;
|
||||
const { test, describe, it, before, after, beforeEach, afterEach } = require('internal/test_runner/harness');
|
||||
const { run } = require('internal/test_runner/runner');
|
||||
const { emitExperimentalWarning } = require('internal/util');
|
||||
|
||||
emitExperimentalWarning('The test runner');
|
||||
|
||||
module.exports = test;
|
||||
module.exports.test = test;
|
||||
module.exports.describe = describe;
|
||||
module.exports.it = it;
|
||||
module.exports.before = before;
|
||||
module.exports.after = after;
|
||||
module.exports.beforeEach = beforeEach;
|
||||
module.exports.afterEach = afterEach;
|
||||
ObjectAssign(module.exports, {
|
||||
after,
|
||||
afterEach,
|
||||
before,
|
||||
beforeEach,
|
||||
describe,
|
||||
it,
|
||||
run,
|
||||
test,
|
||||
});
|
||||
|
||||
@@ -1 +1 @@
|
||||
bound test
|
||||
test
|
||||
|
||||
69
test/parallel/test-runner-run.mjs
Normal file
69
test/parallel/test-runner-run.mjs
Normal file
@@ -0,0 +1,69 @@
|
||||
import * as common from '../common/index.mjs';
|
||||
import * as fixtures from '../common/fixtures.mjs';
|
||||
import { join } from 'node:path';
|
||||
import { describe, it, run } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
const testFixtures = fixtures.path('test-runner');
|
||||
|
||||
describe('require(\'node:test\').run', { concurrency: true }, () => {
|
||||
|
||||
it('should run with no tests', async () => {
|
||||
const stream = run({ files: [] });
|
||||
stream.setEncoding('utf8');
|
||||
stream.on('test:fail', common.mustNotCall());
|
||||
stream.on('test:pass', common.mustNotCall());
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const _ of stream); // TODO(MoLow): assert.snapshot
|
||||
});
|
||||
|
||||
it('should fail with non existing file', async () => {
|
||||
const stream = run({ files: ['a-random-file-that-does-not-exist.js'] });
|
||||
stream.on('test:fail', common.mustCall(1));
|
||||
stream.on('test:pass', common.mustNotCall());
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const _ of stream); // TODO(MoLow): assert.snapshot
|
||||
});
|
||||
|
||||
it('should succeed with a file', async () => {
|
||||
const stream = run({ files: [join(testFixtures, 'test/random.cjs')] });
|
||||
stream.on('test:fail', common.mustNotCall());
|
||||
stream.on('test:pass', common.mustCall(1));
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const _ of stream); // TODO(MoLow): assert.snapshot
|
||||
});
|
||||
|
||||
it('should run same file twice', async () => {
|
||||
const stream = run({ files: [join(testFixtures, 'test/random.cjs'), join(testFixtures, 'test/random.cjs')] });
|
||||
stream.on('test:fail', common.mustNotCall());
|
||||
stream.on('test:pass', common.mustCall(2));
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const _ of stream); // TODO(MoLow): assert.snapshot
|
||||
});
|
||||
|
||||
it('should run a failed test', async () => {
|
||||
const stream = run({ files: [testFixtures] });
|
||||
stream.on('test:fail', common.mustCall(1));
|
||||
stream.on('test:pass', common.mustNotCall());
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const _ of stream); // TODO(MoLow): assert.snapshot
|
||||
});
|
||||
|
||||
it('should support timeout', async () => {
|
||||
const stream = run({ timeout: 50, files: [
|
||||
fixtures.path('test-runner', 'never_ending_sync.js'),
|
||||
fixtures.path('test-runner', 'never_ending_async.js'),
|
||||
] });
|
||||
stream.on('test:fail', common.mustCall(2));
|
||||
stream.on('test:pass', common.mustNotCall());
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
for await (const _ of stream); // TODO(MoLow): assert.snapshot
|
||||
});
|
||||
|
||||
it('should validate files', async () => {
|
||||
[Symbol(), {}, () => {}, 0, 1, 0n, 1n, '', '1', Promise.resolve([]), true, false]
|
||||
.forEach((files) => assert.throws(() => run({ files }), {
|
||||
code: 'ERR_INVALID_ARG_TYPE'
|
||||
}));
|
||||
});
|
||||
});
|
||||
@@ -206,6 +206,8 @@ const customTypesMap = {
|
||||
'Timeout': 'timers.html#class-timeout',
|
||||
'Timer': 'timers.html#timers',
|
||||
|
||||
'TapStream': 'test.html#class-tapstream',
|
||||
|
||||
'tls.SecureContext': 'tls.html#tlscreatesecurecontextoptions',
|
||||
'tls.Server': 'tls.html#class-tlsserver',
|
||||
'tls.TLSSocket': 'tls.html#class-tlstlssocket',
|
||||
|
||||
Reference in New Issue
Block a user