UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

335 lines (292 loc) 9.63 kB
// 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 };