This repository has been archived on 2020-11-02. You can view files and clone it, but cannot push or open issues or pull requests.
2020-11-01 22:46:04 +00:00

279 lines
8.1 KiB
JavaScript

'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 {type} = require('../utils');
const {format} = require('util');
const {createInvalidPluginError, createUnsupportedError} = require('../errors');
const {requireOrImport} = require('../esm-utils');
/**
* 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<MochaRootHookObject|MochaRootHookFunction>} Any root hooks
* @private
*/
exports.handleRequires = async (requires = []) => {
const acc = [];
for (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' &&
requiredModule.mochaHooks
) {
const mochaHooksType = type(requiredModule.mochaHooks);
if (/function$/.test(mochaHooksType) || mochaHooksType === 'object') {
debug('found root hooks in required file %s', mod);
acc.push(requiredModule.mochaHooks);
} else {
throw createUnsupportedError(
'mochaHooks must be an object or a function returning (or fulfilling with) an object'
);
}
}
debug('loaded required module "%s"', mod);
}
return acc;
};
/**
* Loads root hooks as exported via `mochaHooks` from required files.
* These can be sync/async functions returning objects, or just objects.
* Flattens to a single object.
* @param {Array<MochaRootHookObject|MochaRootHookFunction>} rootHooks - Array of root hooks
* @private
* @returns {MochaRootHookObject}
*/
exports.loadRootHooks = async rootHooks => {
const rootHookObjects = await Promise.all(
rootHooks.map(async hook => (/function$/.test(type(hook)) ? hook() : hook))
);
return rootHookObjects.reduce(
(acc, hook) => {
acc.beforeAll = acc.beforeAll.concat(hook.beforeAll || []);
acc.beforeEach = acc.beforeEach.concat(hook.beforeEach || []);
acc.afterAll = acc.afterAll.concat(hook.afterAll || []);
acc.afterEach = acc.afterEach.concat(hook.afterEach || []);
return acc;
},
{beforeAll: [], beforeEach: [], afterAll: [], afterEach: []}
);
};
/**
* 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) across %d concurrent jobs',
files.length,
options.jobs
);
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"|"interface"} pluginType - Type of plugin.
* @param {Object} [map] - An object perhaps having key `key`. Used as a cache
* of sorts; `Mocha.reporters` is one, where each key corresponds to a reporter
* name
* @private
*/
exports.validatePlugin = (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 createInvalidPluginError(
`"--${pluginType}" can only be specified once`,
pluginType
);
}
const unknownError = err =>
createInvalidPluginError(
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 {
opts[pluginType] = require(pluginId);
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
// Try to load reporters from a path (absolute or relative)
try {
opts[pluginType] = require(path.resolve(pluginId));
} catch (err) {
throw unknownError(err);
}
} else {
throw unknownError(err);
}
}
}
};