test
Version:
Node.js 18's node:test, as an npm package
174 lines (148 loc) • 4.74 kB
JavaScript
// https://github.com/nodejs/node/blob/a1b27b25bb01aadd3fd2714e4b136db11b7eb85a/lib/internal/test_runner/harness.js
const {
ArrayPrototypeForEach,
SafeMap,
SafeWeakSet
} = require('#internal/per_context/primordials')
const {
createHook,
executionAsyncId
} = require('async_hooks')
const {
codes: {
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 { setupTestReporters } = require('#internal/test_runner/utils')
const { bigint: hrtime } = process.hrtime
const isTestRunnerCli = getOptionValue('--test')
const testResources = new SafeMap()
const wasRootSetup = new SafeWeakSet()
function createTestTree (options = kEmptyObject) {
return setup(new Test({ __proto__: null, ...options, name: '<root>' }))
}
function createProcessEventHandler (eventName, rootTest) {
return (err) => {
// Check if this error is coming from a test. If it is, fail the test.
const test = testResources.get(executionAsyncId())
if (!test) {
// Node.js 14.x crashes if the error is throw here.
if (process.version.startsWith('v14.')) return
throw err
}
if (test.finished) {
// If the test is already finished, report this as a top level
// diagnostic since this is a malformed test.
const msg = `Warning: Test "${test.name}" 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.`
rootTest.diagnostic(msg)
process.exitCode = 1
return
}
test.fail(new ERR_TEST_FAILURE(err, eventName))
test.postRun()
}
}
function setup (root) {
if (wasRootSetup.has(root)) {
return root
}
const hook = createHook({
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 exitHandler = () => {
root.postRun(new ERR_TEST_FAILURE(
'Promise resolution is still pending but the event loop has already resolved',
kCancelledByParent))
hook.disable()
process.removeListener('unhandledRejection', rejectionHandler)
process.removeListener('uncaughtException', exceptionHandler)
}
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 isTestRunnerCli === false.
if (isTestRunnerCli) {
process.on('SIGINT', terminationHandler)
process.on('SIGTERM', terminationHandler)
}
root.startTime = hrtime()
wasRootSetup.add(root)
return root
}
let globalRoot
function getGlobalRoot () {
if (!globalRoot) {
globalRoot = createTestTree()
globalRoot.reporter.once('test:fail', () => {
process.exitCode = 1
})
setupTestReporters(globalRoot.reporter)
}
return globalRoot
}
function 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()) || getGlobalRoot()
const subtest = parent.createSubtest(Factory, name, options, fn, overrides)
if (parent === getGlobalRoot()) {
subtest.start()
}
}
const cb = (name, options, fn) => {
run(name, options, fn)
}
ArrayPrototypeForEach(['skip', 'todo'], (keyword) => {
cb[keyword] = (name, options, fn) => {
run(name, options, fn, { [keyword]: true })
}
})
return cb
}
function hook (hook) {
return (fn, options) => {
const parent = testResources.get(executionAsyncId()) || getGlobalRoot()
parent.createHook(hook, fn, options)
}
}
module.exports = {
createTestTree,
test,
describe: runInParentContext(Suite),
it: runInParentContext(ItTest),
before: hook('before'),
after: hook('after'),
beforeEach: hook('beforeEach'),
afterEach: hook('afterEach')
}