const fs = require('fs'); const once = require('lodash.once'); const util = require('util'); const assign = require('object-assign'); const debug = require('debug')('mocha:multi'); const path = require('path'); const isString = require('is-string'); const mkdirp = require('mkdirp');
// Let mocha decide about tty early require('mocha/lib/reporters/base');
// Make sure we don't lose these! const { stdout } = process;
function defineGetter(obj, prop, get, set) {
Object.defineProperty(obj, prop, { get, set });
} const waitOn = fn => v => new Promise(resolve => fn(v, () => resolve())); const waitStream = waitOn((r, fn) => r.end(fn));
function awaitOnExit(waitFor) {
if (!waitFor) { return; } const { exit } = process; process.exit = function mochaMultiExitPatch(...args) { const quit = exit.bind(this, ...args); if (process._exiting) { return quit(); } waitFor().then(quit); return undefined; };
}
function identity(x) {
return x;
}
const msgs = {
no_definitions: 'reporter definitions should be set in ' + 'the `multi` shell variable\n' + "eg. `multi='dot=- xunit=file.xml' mocha`", invalid_definition: "'%s' is an invalid definition\n" + 'expected <reporter>=<destination>', invalid_reporter: "Unable to find '%s' reporter", invalid_setup: "Invalid setup for reporter '%s' (%s)", invalid_outfile: "Invalid stdout filename for reporter '%s' (%s)", bad_file: "Missing or malformed options file '%s' -- Error: %s",
}; function bombOut(id, …args) {
const newArgs = [`ERROR: ${msgs[id]}`, ...args]; process.stderr.write(`${util.format(...newArgs)}\n`); process.exit(1);
}
function parseReporter(definition) {
const pair = definition.split('='); if (pair.length !== 2) { bombOut('invalid_definition', definition); } return pair;
}
function convertSetup(reporters) {
let setup = []; Object.keys(reporters).forEach((reporter) => { if (reporter === 'mocha-multi') { debug('loading reporters from file %j', reporters[reporter]); try { setup = setup.concat(convertSetup(JSON.parse(fs.readFileSync(reporters[reporter])))); } catch (e) { bombOut('bad_file', reporters[reporter], e.message); } } else { const r = reporters[reporter]; debug('adding reporter %j %j', reporter, r); if (isString(r)) { setup.push([reporter, r, null]); } else if (typeof r !== 'object') { bombOut('invalid_setup', reporter, typeof r); } else { if (typeof r.stdout !== 'string') { bombOut('invalid_setup', reporter, typeof r); } setup.push([reporter, r.stdout, r.options]); } } }); return setup;
}
function parseSetup() {
const reporterDefinition = process.env.multi || ''; const reporterDefs = reporterDefinition.trim().split(/\s/).filter(identity); if (!reporterDefs.length) { bombOut('no_definitions'); } debug('Got reporter defs: %j', reporterDefs); const reporters = {}; // const but not readonly reporterDefs.forEach((def) => { const [reporter, r] = parseReporter(def); reporters[reporter] = r; }); return convertSetup(reporters);
}
function resolveStream(destination) {
if (destination === '-') { debug("Resolved stream '-' into stdout and stderr"); return null; } debug("Resolved stream '%s' into writeable file stream", destination); // Create directory if not existing const destinationDir = path.dirname(destination); if (!fs.existsSync(destinationDir)) { mkdirp.sync(destinationDir); } // Ensure we can write here fs.writeFileSync(destination, ''); return fs.createWriteStream(destination);
}
function safeRequire(module) {
try { return require(module); } catch (err) { if (!/Cannot find/.exec(err.message)) { throw err; } return null; }
}
function resolveReporter(name) {
// Cribbed from Mocha.prototype.reporter() const reporter = ( safeRequire(`mocha/lib/reporters/${name}`) || safeRequire(name) || bombOut('invalid_reporter', name) ); debug("Resolved reporter '%s' into '%s'", name, util.inspect(reporter)); return reporter;
}
function withReplacedStdout(stream, func) {
if (!stream) { return func(); } // The hackiest of hacks debug('Replacing stdout'); const stdoutGetter = Object.getOwnPropertyDescriptor(process, 'stdout').get; // eslint-disable-next-line no-console console._stdout = stream; defineGetter(process, 'stdout', () => stream); try { return func(); } finally { // eslint-disable-next-line no-console console._stdout = stdout; defineGetter(process, 'stdout', stdoutGetter); debug('stdout restored'); }
}
function createRunnerShim(runner, stream) {
const shim = new (require('events').EventEmitter)(); function addDelegate(prop) { defineGetter(shim, prop, () => { const property = runner[prop]; if (typeof property === 'function') { return property.bind(runner); } return property; }, () => runner[prop]); } addDelegate('grepTotal'); addDelegate('suite'); addDelegate('total'); addDelegate('stats'); const delegatedEvents = {}; shim.on('newListener', (event) => { if (event in delegatedEvents) return; delegatedEvents[event] = true; debug("Shim: Delegating '%s'", event); runner.on(event, (...eventArgs) => { eventArgs.unshift(event); withReplacedStdout(stream, () => { shim.emit(...eventArgs); }); }); }); return shim;
}
function initReportersAndStreams(runner, setup, multiOptions) {
return setup .map(([reporter, outstream, options]) => { debug("Initialising reporter '%s' to '%s' with options %j", reporter, outstream, options); const stream = resolveStream(outstream); const shim = createRunnerShim(runner, stream); debug("Shimming runner into reporter '%s' %j", reporter, options); return withReplacedStdout(stream, () => { const Reporter = resolveReporter(reporter); return { stream, reporter: new Reporter(shim, assign({}, multiOptions, { reporterOptions: options || {}, })), }; }); });
}
function promiseProgress(items, fn) {
let count = 0; fn(count); items.forEach(v => v.then(() => { count += 1; fn(count); })); return Promise.all(items);
}
/**
* Override done to allow done processing for any reporters that have a done method. */
function done(failures, fn, reportersWithDone, waitFor = identity) {
const count = reportersWithDone.length; const waitReporter = waitOn((r, f) => r.done(failures, f)); const progress = v => debug('Awaiting on %j reporters to invoke done callback.', count - v); promiseProgress(reportersWithDone.map(waitReporter), progress) .then(() => { debug('All reporters invoked done callback.'); }) .then(waitFor) .then(() => fn && fn(failures));
}
function mochaMulti(runner, options) {
// keep track of reporters that have a done method. const reporters = (options && options.reporterOptions); const setup = (() => { if (reporters && Object.keys(reporters).length > 0) { debug('options %j', options); return convertSetup(reporters); } return parseSetup(); })(); debug('setup %j', setup); // If the reporter possess a done() method register it so we can // wait for it to complete when done. const reportersAndStreams = initReportersAndStreams(runner, setup, options); const streams = reportersAndStreams .map(v => v.stream) .filter(identity); const reportersWithDone = reportersAndStreams .map(v => v.reporter) .filter(v => v.done); // we actually need to wait streams only if they are present const waitFor = streams.length > 0 ? once(() => Promise.all(streams.map(waitStream))) : undefined; awaitOnExit(waitFor); if (reportersWithDone.length > 0) { return { done: (failures, fn) => done(failures, fn, reportersWithDone, waitFor), }; } return {};
}
class MochaMulti {
constructor(runner, options) { Object.assign(this, mochaMulti(runner, options)); }
}
module.exports = MochaMulti;