nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
335 lines (292 loc) • 9.63 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/test_runner/harness.js
import { getCallerLocation } from "nstdlib/stub/binding/util";
import { createHook, executionAsyncId } from "nstdlib/lib/async_hooks";
import { relative } from "nstdlib/lib/path";
import { codes as __codes__ } from "nstdlib/lib/internal/errors";
import { exitCodes as __exitCodes__ } from "nstdlib/stub/binding/errors";
import {
kCancelledByParent,
Test,
Suite,
} from "nstdlib/lib/internal/test_runner/test";
import {
parseCommandLine,
reporterScope,
shouldColorizeTestFiles,
} from "nstdlib/lib/internal/test_runner/utils";
import { queueMicrotask } from "nstdlib/lib/internal/process/task_queues";
import * as __hoisted_internal_test_runner_coverage__ from "nstdlib/lib/internal/test_runner/coverage";
const { ERR_TEST_FAILURE } = __codes__;
const { kGenericUserError } = __exitCodes__;
const { bigint: hrtime } = process.hrtime;
const resolvedPromise = Promise.resolve();
const testResources = new Map();
let globalRoot;
testResources.set(reporterScope.asyncId(), reporterScope);
function createTestTree(rootTestOptions, globalOptions) {
const harness = {
__proto__: null,
allowTestsToRun: false,
bootstrapPromise: resolvedPromise,
watching: false,
config: globalOptions,
coverage: null,
resetCounters() {
harness.counters = {
__proto__: null,
all: 0,
failed: 0,
passed: 0,
cancelled: 0,
skipped: 0,
todo: 0,
topLevel: 0,
suites: 0,
};
},
counters: null,
shouldColorizeTestFiles: shouldColorizeTestFiles(
globalOptions.destinations,
),
teardown: null,
snapshotManager: null,
};
harness.resetCounters();
globalRoot = new Test({
__proto__: null,
...rootTestOptions,
harness,
name: "<root>",
});
setupProcessState(globalRoot, globalOptions, harness);
globalRoot.startTime = hrtime();
return globalRoot;
}
function createProcessEventHandler(eventName, rootTest) {
return (err) => {
if (rootTest.harness.bootstrapPromise) {
// Something went wrong during the asynchronous portion of bootstrapping
// the test runner. Since the test runner is not setup properly, we can't
// do anything but throw the error.
throw err;
}
const test = testResources.get(executionAsyncId());
// Check if this error is coming from a reporter. If it is, throw it.
if (test === reporterScope) {
throw err;
}
// Check if this error is coming from a test or test hook. If it is, fail the test.
if (!test || test.finished || test.hookType) {
// If the test is already finished or the resource that created the error
// is not mapped to a Test, report this as a top level diagnostic.
let msg;
if (test) {
const name = test.hookType
? `Test hook "${test.hookType}"`
: `Test "${test.name}"`;
let locInfo = "";
if (test.loc) {
const relPath = relative(process.cwd(), test.loc.file);
locInfo = ` at ${relPath}:${test.loc.line}:${test.loc.column}`;
}
msg =
`Error: ${name}${locInfo} generated asynchronous ` +
"activity after the test ended. This activity created the error " +
`"${err}" and would have caused the test to fail, but instead ` +
`triggered an ${eventName} event.`;
} else {
msg =
"Error: A resource generated asynchronous activity after " +
`the test ended. This activity created the error "${err}" which ` +
`triggered an ${eventName} event, caught by the test runner.`;
}
rootTest.diagnostic(msg);
process.exitCode = kGenericUserError;
return;
}
test.fail(new ERR_TEST_FAILURE(err, eventName));
test.abortController.abort();
};
}
function configureCoverage(rootTest, globalOptions) {
if (!globalOptions.coverage) {
return null;
}
const { setupCoverage } = __hoisted_internal_test_runner_coverage__;
try {
return setupCoverage(globalOptions);
} catch (err) {
const msg = `Warning: Code coverage could not be enabled. ${err}`;
rootTest.diagnostic(msg);
process.exitCode = kGenericUserError;
}
}
function collectCoverage(rootTest, coverage) {
if (!coverage) {
return null;
}
let summary = null;
try {
summary = coverage.summary();
coverage.cleanup();
} catch (err) {
const op = summary ? "clean up" : "report";
const msg = `Warning: Could not ${op} code coverage. ${err}`;
rootTest.diagnostic(msg);
process.exitCode = kGenericUserError;
}
return summary;
}
function setupProcessState(root, globalOptions) {
const hook = createHook({
__proto__: null,
init(asyncId, type, triggerAsyncId, resource) {
if (resource instanceof Test) {
testResources.set(asyncId, resource);
return;
}
const parent = testResources.get(triggerAsyncId);
if (parent !== undefined) {
testResources.set(asyncId, parent);
}
},
destroy(asyncId) {
testResources.delete(asyncId);
},
});
hook.enable();
const exceptionHandler = createProcessEventHandler("uncaughtException", root);
const rejectionHandler = createProcessEventHandler(
"unhandledRejection",
root,
);
const coverage = configureCoverage(root, globalOptions);
const exitHandler = async () => {
if (
root.subtests.length === 0 &&
(root.hooks.before.length > 0 || root.hooks.after.length > 0)
) {
// Run global before/after hooks in case there are no tests
await root.run();
}
root.postRun(
new ERR_TEST_FAILURE(
"Promise resolution is still pending but the event loop has already resolved",
kCancelledByParent,
),
);
hook.disable();
process.removeListener("uncaughtException", exceptionHandler);
process.removeListener("unhandledRejection", rejectionHandler);
process.removeListener("beforeExit", exitHandler);
if (globalOptions.isTestRunner) {
process.removeListener("SIGINT", terminationHandler);
process.removeListener("SIGTERM", terminationHandler);
}
};
const terminationHandler = () => {
exitHandler();
process.exit();
};
process.on("uncaughtException", exceptionHandler);
process.on("unhandledRejection", rejectionHandler);
process.on("beforeExit", exitHandler);
// TODO(MoLow): Make it configurable to hook when isTestRunner === false.
if (globalOptions.isTestRunner) {
process.on("SIGINT", terminationHandler);
process.on("SIGTERM", terminationHandler);
}
root.harness.coverage = Function.prototype.bind.call(
collectCoverage,
null,
root,
coverage,
);
root.harness.teardown = exitHandler;
}
function lazyBootstrapRoot() {
if (!globalRoot) {
// This is where the test runner is bootstrapped when node:test is used
// without the --test flag or the run() API.
const rootTestOptions = {
__proto__: null,
entryFile: process.argv?.[1],
};
const globalOptions = parseCommandLine();
createTestTree(rootTestOptions, globalOptions);
globalRoot.reporter.on("test:fail", (data) => {
if (data.todo === undefined || data.todo === false) {
process.exitCode = kGenericUserError;
}
});
globalRoot.harness.bootstrapPromise = globalOptions.setup(
globalRoot.reporter,
);
}
return globalRoot;
}
async function startSubtestAfterBootstrap(subtest) {
if (subtest.root.harness.bootstrapPromise) {
// Only incur the overhead of awaiting the Promise once.
await subtest.root.harness.bootstrapPromise;
subtest.root.harness.bootstrapPromise = null;
queueMicrotask(() => {
subtest.root.harness.allowTestsToRun = true;
subtest.root.processPendingSubtests();
});
}
await subtest.start();
}
function runInParentContext(Factory) {
function run(name, options, fn, overrides) {
const parent = testResources.get(executionAsyncId()) || lazyBootstrapRoot();
const subtest = parent.createSubtest(Factory, name, options, fn, overrides);
if (parent instanceof Suite) {
return Promise.resolve();
}
return startSubtestAfterBootstrap(subtest);
}
const test = (name, options, fn) => {
const overrides = {
__proto__: null,
loc: getCallerLocation(),
};
return run(name, options, fn, overrides);
};
Array.prototype.forEach.call(["skip", "todo", "only"], (keyword) => {
test[keyword] = (name, options, fn) => {
const overrides = {
__proto__: null,
[keyword]: true,
loc: getCallerLocation(),
};
return run(name, options, fn, overrides);
};
});
return test;
}
function hook(hook) {
return (fn, options) => {
const parent = testResources.get(executionAsyncId()) || lazyBootstrapRoot();
parent.createHook(hook, fn, {
__proto__: null,
...options,
parent,
hookType: hook,
loc: getCallerLocation(),
});
};
}
export { createTestTree };
const _export_test_ = runInParentContext(Test);
export { _export_test_ as test };
const _export_suite_ = runInParentContext(Suite);
export { _export_suite_ as suite };
const _export_before_ = hook("before");
export { _export_before_ as before };
const _export_after_ = hook("after");
export { _export_after_ as after };
const _export_beforeEach_ = hook("beforeEach");
export { _export_beforeEach_ as beforeEach };
const _export_afterEach_ = hook("afterEach");
export { _export_afterEach_ as afterEach };