| 'use strict'; |
| |
| /** |
| * Helper scripts for the `run` command |
| * @see module:lib/cli/run |
| * @module |
| * @private |
| */ |
| |
| const fs = require('fs'); |
| const path = require('path'); |
| const debug = require('debug')('mocha:cli:run:helpers'); |
| const {watchRun, watchParallelRun} = require('./watch-run'); |
| const collectFiles = require('./collect-files'); |
| const {format} = require('util'); |
| const {createInvalidLegacyPluginError} = require('../errors'); |
| const {requireOrImport} = require('../esm-utils'); |
| const PluginLoader = require('../plugin-loader'); |
| |
| /** |
| * Exits Mocha when tests + code under test has finished execution (default) |
| * @param {number} code - Exit code; typically # of failures |
| * @ignore |
| * @private |
| */ |
| const exitMochaLater = code => { |
| process.on('exit', () => { |
| process.exitCode = Math.min(code, 255); |
| }); |
| }; |
| |
| /** |
| * Exits Mocha when Mocha itself has finished execution, regardless of |
| * what the tests or code under test is doing. |
| * @param {number} code - Exit code; typically # of failures |
| * @ignore |
| * @private |
| */ |
| const exitMocha = code => { |
| const clampedCode = Math.min(code, 255); |
| let draining = 0; |
| |
| // Eagerly set the process's exit code in case stream.write doesn't |
| // execute its callback before the process terminates. |
| process.exitCode = clampedCode; |
| |
| // flush output for Node.js Windows pipe bug |
| // https://github.com/joyent/node/issues/6247 is just one bug example |
| // https://github.com/visionmedia/mocha/issues/333 has a good discussion |
| const done = () => { |
| if (!draining--) { |
| process.exit(clampedCode); |
| } |
| }; |
| |
| const streams = [process.stdout, process.stderr]; |
| |
| streams.forEach(stream => { |
| // submit empty write request and wait for completion |
| draining += 1; |
| stream.write('', done); |
| }); |
| |
| done(); |
| }; |
| |
| /** |
| * Coerce a comma-delimited string (or array thereof) into a flattened array of |
| * strings |
| * @param {string|string[]} str - Value to coerce |
| * @returns {string[]} Array of strings |
| * @private |
| */ |
| exports.list = str => |
| Array.isArray(str) ? exports.list(str.join(',')) : str.split(/ *, */); |
| |
| /** |
| * `require()` the modules as required by `--require <require>`. |
| * |
| * Returns array of `mochaHooks` exports, if any. |
| * @param {string[]} requires - Modules to require |
| * @returns {Promise<object>} Plugin implementations |
| * @private |
| */ |
| exports.handleRequires = async (requires = [], {ignoredPlugins = []} = {}) => { |
| const pluginLoader = PluginLoader.create({ignore: ignoredPlugins}); |
| for await (const mod of requires) { |
| let modpath = mod; |
| // this is relative to cwd |
| if (fs.existsSync(mod) || fs.existsSync(`${mod}.js`)) { |
| modpath = path.resolve(mod); |
| debug('resolved required file %s to %s', mod, modpath); |
| } |
| const requiredModule = await requireOrImport(modpath); |
| if (requiredModule && typeof requiredModule === 'object') { |
| if (pluginLoader.load(requiredModule)) { |
| debug('found one or more plugin implementations in %s', modpath); |
| } |
| } |
| debug('loaded required module "%s"', mod); |
| } |
| const plugins = await pluginLoader.finalize(); |
| if (Object.keys(plugins).length) { |
| debug('finalized plugin implementations: %O', plugins); |
| } |
| return plugins; |
| }; |
| |
| /** |
| * Collect and load test files, then run mocha instance. |
| * @param {Mocha} mocha - Mocha instance |
| * @param {Options} [opts] - Command line options |
| * @param {boolean} [opts.exit] - Whether or not to force-exit after tests are complete |
| * @param {Object} fileCollectParams - Parameters that control test |
| * file collection. See `lib/cli/collect-files.js`. |
| * @returns {Promise<Runner>} |
| * @private |
| */ |
| const singleRun = async (mocha, {exit}, fileCollectParams) => { |
| const files = collectFiles(fileCollectParams); |
| debug('single run with %d file(s)', files.length); |
| mocha.files = files; |
| |
| // handles ESM modules |
| await mocha.loadFilesAsync(); |
| return mocha.run(exit ? exitMocha : exitMochaLater); |
| }; |
| |
| /** |
| * Collect files and run tests (using `BufferedRunner`). |
| * |
| * This is `async` for consistency. |
| * |
| * @param {Mocha} mocha - Mocha instance |
| * @param {Options} options - Command line options |
| * @param {Object} fileCollectParams - Parameters that control test |
| * file collection. See `lib/cli/collect-files.js`. |
| * @returns {Promise<BufferedRunner>} |
| * @ignore |
| * @private |
| */ |
| const parallelRun = async (mocha, options, fileCollectParams) => { |
| const files = collectFiles(fileCollectParams); |
| debug('executing %d test file(s) in parallel mode', files.length); |
| mocha.files = files; |
| |
| // note that we DO NOT load any files here; this is handled by the worker |
| return mocha.run(options.exit ? exitMocha : exitMochaLater); |
| }; |
| |
| /** |
| * Actually run tests. Delegates to one of four different functions: |
| * - `singleRun`: run tests in serial & exit |
| * - `watchRun`: run tests in serial, rerunning as files change |
| * - `parallelRun`: run tests in parallel & exit |
| * - `watchParallelRun`: run tests in parallel, rerunning as files change |
| * @param {Mocha} mocha - Mocha instance |
| * @param {Options} opts - Command line options |
| * @private |
| * @returns {Promise<Runner>} |
| */ |
| exports.runMocha = async (mocha, options) => { |
| const { |
| watch = false, |
| extension = [], |
| ignore = [], |
| file = [], |
| parallel = false, |
| recursive = false, |
| sort = false, |
| spec = [] |
| } = options; |
| |
| const fileCollectParams = { |
| ignore, |
| extension, |
| file, |
| recursive, |
| sort, |
| spec |
| }; |
| |
| let run; |
| if (watch) { |
| run = parallel ? watchParallelRun : watchRun; |
| } else { |
| run = parallel ? parallelRun : singleRun; |
| } |
| |
| return run(mocha, options, fileCollectParams); |
| }; |
| |
| /** |
| * Used for `--reporter` and `--ui`. Ensures there's only one, and asserts that |
| * it actually exists. This must be run _after_ requires are processed (see |
| * {@link handleRequires}), as it'll prevent interfaces from loading otherwise. |
| * @param {Object} opts - Options object |
| * @param {"reporter"|"ui"} pluginType - Type of plugin. |
| * @param {Object} [map] - Used as a cache of sorts; |
| * `Mocha.reporters` where each key corresponds to a reporter name, |
| * `Mocha.interfaces` where each key corresponds to an interface name. |
| * @private |
| */ |
| exports.validateLegacyPlugin = (opts, pluginType, map = {}) => { |
| /** |
| * This should be a unique identifier; either a string (present in `map`), |
| * or a resolvable (via `require.resolve`) module ID/path. |
| * @type {string} |
| */ |
| const pluginId = opts[pluginType]; |
| |
| if (Array.isArray(pluginId)) { |
| throw createInvalidLegacyPluginError( |
| `"--${pluginType}" can only be specified once`, |
| pluginType |
| ); |
| } |
| |
| const createUnknownError = err => |
| createInvalidLegacyPluginError( |
| format('Could not load %s "%s":\n\n %O', pluginType, pluginId, err), |
| pluginType, |
| pluginId |
| ); |
| |
| // if this exists, then it's already loaded, so nothing more to do. |
| if (!map[pluginId]) { |
| try { |
| map[pluginId] = require(pluginId); |
| } catch (err) { |
| if (err.code === 'MODULE_NOT_FOUND') { |
| // Try to load reporters from a path (absolute or relative) |
| try { |
| map[pluginId] = require(path.resolve(pluginId)); |
| } catch (err) { |
| throw createUnknownError(err); |
| } |
| } else { |
| throw createUnknownError(err); |
| } |
| } |
| } |
| }; |