run-helpers.js 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  1. 'use strict';
  2. /**
  3. * Helper scripts for the `run` command
  4. * @see module:lib/cli/run
  5. * @module
  6. * @private
  7. */
  8. /**
  9. * @typedef {import('../mocha.js')} Mocha
  10. * @typedef {import('../types.d.ts').MochaOptions} MochaOptions
  11. * @typedef {import('../types.d.ts').UnmatchedFile} UnmatchedFile
  12. * @typedef {import('../runner.js')} Runner
  13. */
  14. const fs = require('node:fs');
  15. const path = require('node:path');
  16. const pc = require('picocolors');
  17. const debug = require('debug')('mocha:cli:run:helpers');
  18. const {watchRun, watchParallelRun} = require('./watch-run');
  19. const collectFiles = require('./collect-files');
  20. const {format} = require('node:util');
  21. const {createInvalidLegacyPluginError} = require('../errors');
  22. const {requireOrImport} = require('../nodejs/esm-utils');
  23. const PluginLoader = require('../plugin-loader');
  24. /**
  25. * Exits Mocha when tests + code under test has finished execution (default)
  26. * @param {number} clampedCode - Exit code; typically # of failures
  27. * @ignore
  28. * @private
  29. */
  30. const exitMochaLater = clampedCode => {
  31. process.on('exit', () => {
  32. process.exitCode = Math.min(clampedCode, process.argv.includes('--posix-exit-codes') ? 1 : 255);
  33. });
  34. };
  35. /**
  36. * Exits Mocha when Mocha itself has finished execution, regardless of
  37. * what the tests or code under test is doing.
  38. * @param {number} clampedCode - Exit code; typically # of failures
  39. * @ignore
  40. * @private
  41. */
  42. const exitMocha = clampedCode => {
  43. const usePosixExitCodes = process.argv.includes('--posix-exit-codes');
  44. clampedCode = Math.min(clampedCode, usePosixExitCodes ? 1 : 255);
  45. let draining = 0;
  46. // Eagerly set the process's exit code in case stream.write doesn't
  47. // execute its callback before the process terminates.
  48. process.exitCode = clampedCode;
  49. // flush output for Node.js Windows pipe bug
  50. // https://github.com/joyent/node/issues/6247 is just one bug example
  51. // https://github.com/visionmedia/mocha/issues/333 has a good discussion
  52. const done = () => {
  53. if (!draining--) {
  54. process.exit(clampedCode);
  55. }
  56. };
  57. const streams = [process.stdout, process.stderr];
  58. streams.forEach(stream => {
  59. // submit empty write request and wait for completion
  60. draining += 1;
  61. stream.write('', done);
  62. });
  63. done();
  64. };
  65. /**
  66. * Coerce a comma-delimited string (or array thereof) into a flattened array of
  67. * strings
  68. * @param {string|string[]} str - Value to coerce
  69. * @returns {string[]} Array of strings
  70. * @private
  71. */
  72. exports.list = str =>
  73. Array.isArray(str) ? exports.list(str.join(',')) : str.split(/ *, */);
  74. /**
  75. * `require()` the modules as required by `--require <require>`.
  76. *
  77. * Returns array of `mochaHooks` exports, if any.
  78. * @param {string[]} requires - Modules to require
  79. * @returns {Promise<object>} Plugin implementations
  80. * @private
  81. */
  82. exports.handleRequires = async (requires = [], {ignoredPlugins = []} = {}) => {
  83. const pluginLoader = PluginLoader.create({ignore: ignoredPlugins});
  84. for await (const mod of requires) {
  85. let modpath = mod;
  86. // this is relative to cwd
  87. if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) {
  88. modpath = path.resolve(mod);
  89. debug('resolved required file %s to %s', mod, modpath);
  90. }
  91. const requiredModule = await requireOrImport(modpath);
  92. if (requiredModule && typeof requiredModule === 'object') {
  93. if (pluginLoader.load(requiredModule)) {
  94. debug('found one or more plugin implementations in %s', modpath);
  95. }
  96. }
  97. debug('loaded required module "%s"', mod);
  98. }
  99. const plugins = await pluginLoader.finalize();
  100. if (Object.keys(plugins).length) {
  101. debug('finalized plugin implementations: %O', plugins);
  102. }
  103. return plugins;
  104. };
  105. /**
  106. * Logs errors and exits the app if unmatched files exist
  107. * @param {Mocha} mocha - Mocha instance
  108. * @param {UnmatchedFile} unmatchedFiles - object containing unmatched file paths
  109. * @returns {Promise<Runner>}
  110. * @private
  111. */
  112. const handleUnmatchedFiles = (mocha, unmatchedFiles) => {
  113. if (unmatchedFiles.length === 0) {
  114. return;
  115. }
  116. unmatchedFiles.forEach(({pattern, absolutePath}) => {
  117. console.error(
  118. pc.yellow(
  119. `Warning: Cannot find any files matching pattern "${pattern}" at the absolute path "${absolutePath}"`
  120. )
  121. );
  122. });
  123. console.log(
  124. 'No test file(s) found with the given pattern, exiting with code 1'
  125. );
  126. return mocha.run(exitMocha(1));
  127. };
  128. /**
  129. * Collect and load test files, then run mocha instance.
  130. * @param {Mocha} mocha - Mocha instance
  131. * @param {MochaOptions} [opts] - Command line options
  132. * @param {Object} fileCollectParams - Parameters that control test
  133. * file collection. See `lib/cli/collect-files.js`.
  134. * @returns {Promise<Runner>}
  135. * @private
  136. */
  137. const singleRun = async (
  138. mocha,
  139. {exit, passOnFailingTestSuite},
  140. fileCollectParams
  141. ) => {
  142. const fileCollectionObj = collectFiles(fileCollectParams);
  143. if (fileCollectionObj.unmatchedFiles.length > 0) {
  144. return handleUnmatchedFiles(mocha, fileCollectionObj.unmatchedFiles);
  145. }
  146. debug('single run with %d file(s)', fileCollectionObj.files.length);
  147. mocha.files = fileCollectionObj.files;
  148. // handles ESM modules
  149. await mocha.loadFilesAsync();
  150. return mocha.run(
  151. createExitHandler({exit, passOnFailingTestSuite})
  152. );
  153. };
  154. /**
  155. * Collect files and run tests (using `Runner`).
  156. *
  157. * This is `async` for consistency.
  158. *
  159. * @param {Mocha} mocha - Mocha instance
  160. * @param {MochaOptions} options - Command line options
  161. * @param {Object} fileCollectParams - Parameters that control test
  162. * file collection. See `lib/cli/collect-files.js`.
  163. * @returns {Promise<Runner>}
  164. * @ignore
  165. * @private
  166. */
  167. const parallelRun = async (mocha, options, fileCollectParams) => {
  168. const fileCollectionObj = collectFiles(fileCollectParams);
  169. if (fileCollectionObj.unmatchedFiles.length > 0) {
  170. return handleUnmatchedFiles(mocha, fileCollectionObj.unmatchedFiles);
  171. }
  172. debug(
  173. 'executing %d test file(s) in parallel mode',
  174. fileCollectionObj.files.length
  175. );
  176. mocha.files = fileCollectionObj.files;
  177. // note that we DO NOT load any files here; this is handled by the worker
  178. return mocha.run(
  179. createExitHandler(options)
  180. );
  181. };
  182. /**
  183. * Actually run tests. Delegates to one of four different functions:
  184. * - `singleRun`: run tests in serial & exit
  185. * - `watchRun`: run tests in serial, rerunning as files change
  186. * - `parallelRun`: run tests in parallel & exit
  187. * - `watchParallelRun`: run tests in parallel, rerunning as files change
  188. * @param {Mocha} mocha - Mocha instance
  189. * @param {MochaOptions} options - Command line options
  190. * @private
  191. * @returns {Promise<Runner>}
  192. */
  193. exports.runMocha = async (mocha, options) => {
  194. const {
  195. watch = false,
  196. extension = [],
  197. ignore = [],
  198. file = [],
  199. parallel = false,
  200. recursive = false,
  201. sort = false,
  202. spec = []
  203. } = options;
  204. const fileCollectParams = {
  205. ignore,
  206. extension,
  207. file,
  208. recursive,
  209. sort,
  210. spec
  211. };
  212. let run;
  213. if (watch) {
  214. run = parallel ? watchParallelRun : watchRun;
  215. } else {
  216. run = parallel ? parallelRun : singleRun;
  217. }
  218. return run(mocha, options, fileCollectParams);
  219. };
  220. /**
  221. * Used for `--reporter` and `--ui`. Ensures there's only one, and asserts that
  222. * it actually exists. This must be run _after_ requires are processed (see
  223. * {@link handleRequires}), as it'll prevent interfaces from loading otherwise.
  224. * @param {Object} opts - Options object
  225. * @param {"reporter"|"ui"} pluginType - Type of plugin.
  226. * @param {Object} [map] - Used as a cache of sorts;
  227. * `Mocha.reporters` where each key corresponds to a reporter name,
  228. * `Mocha.interfaces` where each key corresponds to an interface name.
  229. * @private
  230. */
  231. exports.validateLegacyPlugin = (opts, pluginType, map = {}) => {
  232. /**
  233. * This should be a unique identifier; either a string (present in `map`),
  234. * or a resolvable (via `require.resolve`) module ID/path.
  235. * @type {string}
  236. */
  237. const pluginId = opts[pluginType];
  238. if (Array.isArray(pluginId)) {
  239. throw createInvalidLegacyPluginError(
  240. `"--${pluginType}" can only be specified once`,
  241. pluginType
  242. );
  243. }
  244. const createUnknownError = err =>
  245. createInvalidLegacyPluginError(
  246. format('Could not load %s "%s":\n\n %O', pluginType, pluginId, err),
  247. pluginType,
  248. pluginId
  249. );
  250. // if this exists, then it's already loaded, so nothing more to do.
  251. if (!map[pluginId]) {
  252. let foundId;
  253. try {
  254. foundId = require.resolve(pluginId);
  255. map[pluginId] = require(foundId);
  256. } catch (err) {
  257. if (foundId) throw createUnknownError(err);
  258. // Try to load reporters from a cwd-relative path
  259. try {
  260. map[pluginId] = require(path.resolve(pluginId));
  261. } catch (err) {
  262. throw createUnknownError(err);
  263. }
  264. }
  265. }
  266. };
  267. const createExitHandler = ({ exit, passOnFailingTestSuite }) => {
  268. return code => {
  269. const clampedCode = passOnFailingTestSuite
  270. ? 0
  271. : Math.min(code, 255);
  272. return exit
  273. ? exitMocha(clampedCode)
  274. : exitMochaLater(clampedCode);
  275. };
  276. };