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