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:
Moshe Atlow
2022-08-15 12:12:11 +03:00
parent cfdc713f0b
commit 59527de13d
10 changed files with 424 additions and 230 deletions

View File

@@ -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

View File

@@ -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;
});

View File

@@ -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'),

View 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 };

View File

@@ -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);

View File

@@ -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 {

View File

@@ -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,
});

View File

@@ -1 +1 @@
bound test
test

View 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'
}));
});
});

View File

@@ -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',