1'use strict'; 2const { 3 ArrayPrototypeForEach, 4 FunctionPrototypeBind, 5 PromiseResolve, 6 SafeMap, 7} = primordials; 8const { getCallerLocation } = internalBinding('util'); 9const { 10 createHook, 11 executionAsyncId, 12} = require('async_hooks'); 13const { 14 codes: { 15 ERR_TEST_FAILURE, 16 }, 17} = require('internal/errors'); 18const { kEmptyObject } = require('internal/util'); 19const { kCancelledByParent, Test, Suite } = require('internal/test_runner/test'); 20const { 21 parseCommandLine, 22 reporterScope, 23 setupTestReporters, 24} = require('internal/test_runner/utils'); 25const { bigint: hrtime } = process.hrtime; 26 27const testResources = new SafeMap(); 28 29testResources.set(reporterScope.asyncId(), reporterScope); 30 31function createTestTree(options = kEmptyObject) { 32 return setup(new Test({ __proto__: null, ...options, name: '<root>' })); 33} 34 35function createProcessEventHandler(eventName, rootTest) { 36 return (err) => { 37 if (!rootTest.harness.bootstrapComplete) { 38 // Something went wrong during the asynchronous portion of bootstrapping 39 // the test runner. Since the test runner is not setup properly, we can't 40 // do anything but throw the error. 41 throw err; 42 } 43 44 const test = testResources.get(executionAsyncId()); 45 46 // Check if this error is coming from a reporter. If it is, throw it. 47 if (test === reporterScope) { 48 throw err; 49 } 50 51 // Check if this error is coming from a test. If it is, fail the test. 52 if (!test || test.finished) { 53 // If the test is already finished or the resource that created the error 54 // is not mapped to a Test, report this as a top level diagnostic. 55 let msg; 56 57 if (test) { 58 msg = `Warning: Test "${test.name}" generated asynchronous ` + 59 'activity after the test ended. This activity created the error ' + 60 `"${err}" and would have caused the test to fail, but instead ` + 61 `triggered an ${eventName} event.`; 62 } else { 63 msg = 'Warning: A resource generated asynchronous activity after ' + 64 `the test ended. This activity created the error "${err}" which ` + 65 `triggered an ${eventName} event, caught by the test runner.`; 66 } 67 68 rootTest.diagnostic(msg); 69 process.exitCode = 1; 70 return; 71 } 72 73 test.fail(new ERR_TEST_FAILURE(err, eventName)); 74 test.postRun(); 75 }; 76} 77 78function configureCoverage(rootTest, globalOptions) { 79 if (!globalOptions.coverage) { 80 return null; 81 } 82 83 const { setupCoverage } = require('internal/test_runner/coverage'); 84 85 try { 86 return setupCoverage(); 87 } catch (err) { 88 const msg = `Warning: Code coverage could not be enabled. ${err}`; 89 90 rootTest.diagnostic(msg); 91 process.exitCode = 1; 92 } 93} 94 95function collectCoverage(rootTest, coverage) { 96 if (!coverage) { 97 return null; 98 } 99 100 let summary = null; 101 102 try { 103 summary = coverage.summary(); 104 coverage.cleanup(); 105 } catch (err) { 106 const op = summary ? 'clean up' : 'report'; 107 const msg = `Warning: Could not ${op} code coverage. ${err}`; 108 109 rootTest.diagnostic(msg); 110 process.exitCode = 1; 111 } 112 113 return summary; 114} 115 116function setup(root) { 117 if (root.startTime !== null) { 118 return root; 119 } 120 121 // Parse the command line options before the hook is enabled. We don't want 122 // global input validation errors to end up in the uncaughtException handler. 123 const globalOptions = parseCommandLine(); 124 125 const hook = createHook({ 126 __proto__: null, 127 init(asyncId, type, triggerAsyncId, resource) { 128 if (resource instanceof Test) { 129 testResources.set(asyncId, resource); 130 return; 131 } 132 133 const parent = testResources.get(triggerAsyncId); 134 135 if (parent !== undefined) { 136 testResources.set(asyncId, parent); 137 } 138 }, 139 destroy(asyncId) { 140 testResources.delete(asyncId); 141 }, 142 }); 143 144 hook.enable(); 145 146 const exceptionHandler = 147 createProcessEventHandler('uncaughtException', root); 148 const rejectionHandler = 149 createProcessEventHandler('unhandledRejection', root); 150 const coverage = configureCoverage(root, globalOptions); 151 const exitHandler = () => { 152 root.postRun(new ERR_TEST_FAILURE( 153 'Promise resolution is still pending but the event loop has already resolved', 154 kCancelledByParent)); 155 156 hook.disable(); 157 process.removeListener('unhandledRejection', rejectionHandler); 158 process.removeListener('uncaughtException', exceptionHandler); 159 }; 160 161 const terminationHandler = () => { 162 exitHandler(); 163 process.exit(); 164 }; 165 166 process.on('uncaughtException', exceptionHandler); 167 process.on('unhandledRejection', rejectionHandler); 168 process.on('beforeExit', exitHandler); 169 // TODO(MoLow): Make it configurable to hook when isTestRunner === false. 170 if (globalOptions.isTestRunner) { 171 process.on('SIGINT', terminationHandler); 172 process.on('SIGTERM', terminationHandler); 173 } 174 175 root.harness = { 176 __proto__: null, 177 bootstrapComplete: false, 178 coverage: FunctionPrototypeBind(collectCoverage, null, root, coverage), 179 counters: { 180 __proto__: null, 181 all: 0, 182 failed: 0, 183 passed: 0, 184 cancelled: 0, 185 skipped: 0, 186 todo: 0, 187 topLevel: 0, 188 suites: 0, 189 }, 190 shouldColorizeTestFiles: false, 191 }; 192 root.startTime = hrtime(); 193 return root; 194} 195 196let globalRoot; 197let reportersSetup; 198function getGlobalRoot() { 199 if (!globalRoot) { 200 globalRoot = createTestTree(); 201 globalRoot.reporter.on('test:fail', (data) => { 202 if (data.todo === undefined || data.todo === false) { 203 process.exitCode = 1; 204 } 205 }); 206 reportersSetup = setupTestReporters(globalRoot); 207 } 208 return globalRoot; 209} 210 211async function startSubtest(subtest) { 212 await reportersSetup; 213 getGlobalRoot().harness.bootstrapComplete = true; 214 await subtest.start(); 215} 216 217function runInParentContext(Factory) { 218 function run(name, options, fn, overrides) { 219 const parent = testResources.get(executionAsyncId()) || getGlobalRoot(); 220 const subtest = parent.createSubtest(Factory, name, options, fn, overrides); 221 if (!(parent instanceof Suite)) { 222 return startSubtest(subtest); 223 } 224 return PromiseResolve(); 225 } 226 227 const test = (name, options, fn) => { 228 const overrides = { 229 __proto__: null, 230 loc: getCallerLocation(), 231 }; 232 233 return run(name, options, fn, overrides); 234 }; 235 ArrayPrototypeForEach(['skip', 'todo', 'only'], (keyword) => { 236 test[keyword] = (name, options, fn) => { 237 const overrides = { 238 __proto__: null, 239 [keyword]: true, 240 loc: getCallerLocation(), 241 }; 242 243 return run(name, options, fn, overrides); 244 }; 245 }); 246 return test; 247} 248 249function hook(hook) { 250 return (fn, options) => { 251 const parent = testResources.get(executionAsyncId()) || getGlobalRoot(); 252 parent.createHook(hook, fn, { 253 __proto__: null, 254 ...options, 255 parent, 256 hookType: hook, 257 loc: getCallerLocation(), 258 }); 259 }; 260} 261 262module.exports = { 263 createTestTree, 264 test: runInParentContext(Test), 265 describe: runInParentContext(Suite), 266 it: runInParentContext(Test), 267 before: hook('before'), 268 after: hook('after'), 269 beforeEach: hook('beforeEach'), 270 afterEach: hook('afterEach'), 271}; 272