worker.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. /**
  2. * A worker process. Consumes {@link module:reporters/parallel-buffered} reporter.
  3. * @module worker
  4. * @private
  5. */
  6. 'use strict';
  7. /**
  8. * @typedef {import('../types.d.ts').BufferedEvent} BufferedEvent
  9. * @typedef {import('../types.d.ts').MochaOptions} MochaOptions
  10. */
  11. const {
  12. createInvalidArgumentTypeError,
  13. createInvalidArgumentValueError
  14. } = require('../errors');
  15. const workerpool = require('workerpool');
  16. const Mocha = require('../mocha');
  17. const {handleRequires, validateLegacyPlugin} = require('../cli/run-helpers');
  18. const d = require('debug');
  19. const debug = d.debug(`mocha:parallel:worker:${process.pid}`);
  20. const isDebugEnabled = d.enabled(`mocha:parallel:worker:${process.pid}`);
  21. const {serialize} = require('./serializer');
  22. const {setInterval, clearInterval} = global;
  23. let rootHooks;
  24. if (workerpool.isMainThread) {
  25. throw new Error(
  26. 'This script is intended to be run as a worker (by the `workerpool` package).'
  27. );
  28. }
  29. /**
  30. * Initializes some stuff on the first call to {@link run}.
  31. *
  32. * Handles `--require` and `--ui`. Does _not_ handle `--reporter`,
  33. * as only the `Buffered` reporter is used.
  34. *
  35. * **This function only runs once per worker**; it overwrites itself with a no-op
  36. * before returning.
  37. *
  38. * @param {MochaOptions} argv - Command-line options
  39. */
  40. let bootstrap = async argv => {
  41. // globalSetup and globalTeardown do not run in workers
  42. const plugins = await handleRequires(argv.require, {
  43. ignoredPlugins: ['mochaGlobalSetup', 'mochaGlobalTeardown']
  44. });
  45. validateLegacyPlugin(argv, 'ui', Mocha.interfaces);
  46. rootHooks = plugins.rootHooks;
  47. bootstrap = () => {};
  48. debug('bootstrap(): finished with args: %O', argv);
  49. };
  50. /**
  51. * Runs a single test file in a worker thread.
  52. * @param {string} filepath - Filepath of test file
  53. * @param {string} [serializedOptions] - **Serialized** options. This string will be eval'd!
  54. * @see https://npm.im/serialize-javascript
  55. * @returns {Promise<{failures: number, events: BufferedEvent[]}>} - Test
  56. * failure count and list of events.
  57. */
  58. async function run(filepath, serializedOptions = '{}') {
  59. if (!filepath) {
  60. throw createInvalidArgumentTypeError(
  61. 'Expected a non-empty "filepath" argument',
  62. 'file',
  63. 'string'
  64. );
  65. }
  66. debug('run(): running test file %s', filepath);
  67. if (typeof serializedOptions !== 'string') {
  68. throw createInvalidArgumentTypeError(
  69. 'run() expects second parameter to be a string which was serialized by the `serialize-javascript` module',
  70. 'serializedOptions',
  71. 'string'
  72. );
  73. }
  74. let argv;
  75. try {
  76. // eslint-disable-next-line no-eval
  77. argv = eval('(' + serializedOptions + ')');
  78. } catch (err) {
  79. throw createInvalidArgumentValueError(
  80. 'run() was unable to deserialize the options',
  81. 'serializedOptions',
  82. serializedOptions
  83. );
  84. }
  85. const opts = Object.assign({ui: 'bdd'}, argv, {
  86. // if this was true, it would cause infinite recursion.
  87. parallel: false,
  88. // this doesn't work in parallel mode
  89. forbidOnly: true,
  90. // it's useful for a Mocha instance to know if it's running in a worker process.
  91. isWorker: true
  92. });
  93. await bootstrap(opts);
  94. opts.rootHooks = rootHooks;
  95. const mocha = new Mocha(opts).addFile(filepath);
  96. try {
  97. await mocha.loadFilesAsync();
  98. } catch (err) {
  99. debug('run(): could not load file %s: %s', filepath, err);
  100. throw err;
  101. }
  102. return new Promise((resolve, reject) => {
  103. let debugInterval;
  104. /* istanbul ignore next */
  105. if (isDebugEnabled) {
  106. debugInterval = setInterval(() => {
  107. debug('run(): still running %s...', filepath);
  108. }, 5000).unref();
  109. }
  110. mocha.run(result => {
  111. // Runner adds these; if we don't remove them, we'll get a leak.
  112. process.removeAllListeners('uncaughtException');
  113. process.removeAllListeners('unhandledRejection');
  114. try {
  115. const serialized = serialize(result);
  116. debug(
  117. 'run(): completed run with %d test failures; returning to main process',
  118. typeof result.failures === 'number' ? result.failures : 0
  119. );
  120. resolve(serialized);
  121. } catch (err) {
  122. // TODO: figure out exactly what the sad path looks like here.
  123. // rejection should only happen if an error is "unrecoverable"
  124. debug('run(): serialization failed; rejecting: %O', err);
  125. reject(err);
  126. } finally {
  127. clearInterval(debugInterval);
  128. }
  129. });
  130. });
  131. }
  132. // this registers the `run` function.
  133. workerpool.worker({run});
  134. debug('started worker process');
  135. // for testing
  136. exports.run = run;