Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement support for multiple reporters #2091

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 45 additions & 17 deletions bin/_mocha
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@ program
.option('-c, --colors', 'force enabling of colors')
.option('-C, --no-colors', 'force disabling of colors')
.option('-G, --growl', 'enable growl notification support')
.option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options')
.option('-R, --reporter <name>', 'specify the reporter to use', 'spec')
.option('-O, --reporter-options <k=v,k2=v2,...>', 'reporter-specific options, use name:{k=v,k2=v2,...} for multiple reporters')
.option('-R, --reporter <name[:output]>,...', 'specify the reporters to use, provide optional output filename', list, ['spec'])
.option('-S, --sort', "sort test files")
.option('-b, --bail', "bail after first test failure")
.option('-d, --debug', "enable node's debugger, synonym for node --debug")
Expand Down Expand Up @@ -190,16 +190,41 @@ Error.stackTraceLimit = Infinity; // TODO: config

var reporterOptions = {};
if (program.reporterOptions !== undefined) {
program.reporterOptions.split(",").forEach(function(opt) {
var L = opt.split("=");
if (L.length > 2 || L.length === 0) {
throw new Error("invalid reporter option '" + opt + "'");
} else if (L.length === 2) {
reporterOptions[L[0]] = L[1];
} else {
reporterOptions[L[0]] = true;
var reporterOptionsParser;
var perReporterFormat = /([^,:]+):{([^}]+)}/g;
if (program.reporterOptions.match(perReporterFormat)) {
// per-reporter config:
// spec:{a:1,b:2},dot:{c:3,d:4}
var match;
while((match = perReporterFormat.exec(program.reporterOptions))) {
if (match.length !== 3) {
throw new Error("invalid reporter option '" + program.reporterOptions + "'");
}

var reporterName = match[1];
var reporterOptStr = match[2];

reporterOptStr.split(',').forEach(function(reporterOpt) {
var split = reporterOpt.indexOf(':');
if (split === -1) {
throw new Error("invalid reporter option '" + reporterOpt + "'" + L);
}
reporterOptions[reporterName] = reporterOptions[reporterName] || {};
reporterOptions[reporterName][reporterOpt.substr(0, split)] = reporterOpt.substr(split + 1);
});
};
} else {
// single reporter config:
// a=1,b=2
program.reporterOptions.split(',').forEach(function(opt) {
var L = opt.split('=');
if (L.length !== 2) {
throw new Error("invalid reporter option '" + opt + "'");
}
reporterOptions._default = reporterOptions._default || {};
reporterOptions._default[L[0]] = L[1];
});
}
}

// reporter
Expand All @@ -208,16 +233,19 @@ mocha.reporter(program.reporter, reporterOptions);

// load reporter

var Reporter = null;
try {
Reporter = require('../lib/reporters/' + program.reporter);
} catch (err) {
program.reporter.forEach(function(reporterConfig) {
var reporterName;
try {
Reporter = require(program.reporter);
reporterName = reporterConfig.split(':')[0];
require('../lib/reporters/' + reporterName);
} catch (err) {
throw new Error('reporter "' + program.reporter + '" does not exist');
try {
require(reporterName);
} catch (err2) {
throw new Error('reporter "' + reporterName + '" does not exist');
}
}
}
});

// --no-colors

Expand Down
102 changes: 77 additions & 25 deletions lib/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

var escapeRe = require('escape-string-regexp');
var path = require('path');
var reporters = require('./reporters');
var builtInReporters = require('./reporters');
var utils = require('./utils');

/**
Expand All @@ -34,7 +34,7 @@ if (!process.browser) {

exports.utils = utils;
exports.interfaces = require('./interfaces');
exports.reporters = reporters;
exports.reporters = builtInReporters;
exports.Runnable = require('./runnable');
exports.Context = require('./context');
exports.Runner = require('./runner');
Expand Down Expand Up @@ -143,23 +143,49 @@ Mocha.prototype.addFile = function(file) {
};

/**
* Set reporter to `reporter`, defaults to "spec".
* Set reporters to `reporters`, defaults to ['spec'].
*
* @param {String|Function} reporter name or constructor
* @param {Object} reporterOptions optional options
* @param {String|Function|Array of strings|Array of functions} reporter name as
* string, reporter constructor, or array of constructors or reporter names as
* strings. If using a string, can provide an optional output filename by
* adding a ":<filename>" suffix to the reporter name.
* @param {Object} reporterOptions optional options, keyed by reporter name, or
* '_default' for options to use when per-name options are not given.
* @api public
* @param {string|Function} reporter name or constructor
* @param {Object} reporterOptions optional options
*/
Mocha.prototype.reporter = function(reporter, reporterOptions) {
if (typeof reporter === 'function') {
this._reporter = reporter;
} else {
reporter = reporter || 'spec';
Mocha.prototype.reporter = function(reporters, reporterOptions) {
// if no reporter is given as input, default to spec reporter
reporters = reporters || ['spec'];
reporterOptions = reporterOptions || {};

if (!Array.isArray(reporters)) {
if ((typeof reporters === 'string')
|| (typeof reporters === 'function')) {
reporters = [reporters];
}
}

// Load each reporter
this._reporters = reporters.map(function(reporterConfig) {
if (typeof reporterConfig === 'function') {
return {
fn: reporterConfig,
options: reporterOptions
};
}
var reporter;
var outputPath;

reporter = reporterConfig.split(':');
if (reporter.length > 1) {
outputPath = reporter[1];
}
reporter = reporter[0];

var _reporter;
// Try to load a built-in reporter.
if (reporters[reporter]) {
_reporter = reporters[reporter];
if (builtInReporters[reporter]) {
_reporter = builtInReporters[reporter];
}
// Try to load reporters from process.cwd() and node_modules
if (!_reporter) {
Expand All @@ -179,9 +205,13 @@ Mocha.prototype.reporter = function(reporter, reporterOptions) {
if (!_reporter) {
throw new Error('invalid reporter "' + reporter + '"');
}
this._reporter = _reporter;
}
this.options.reporterOptions = reporterOptions;
return {
fn: _reporter,
outputPath: outputPath,
options: reporterOptions[reporter] || reporterOptions._default || {}
};
}, this);

return this;
};

Expand Down Expand Up @@ -471,7 +501,16 @@ Mocha.prototype.run = function(fn) {
var options = this.options;
options.files = this.files;
var runner = new exports.Runner(suite, options.delay);
var reporter = new this._reporter(runner, options);

// For each loaded reporter constructor, create
// an instance and initialize it with the runner
var reporters = this._reporters.map(function(reporterConfig) {
var _reporter = reporterConfig.fn;
var outputPath = reporterConfig.outputPath;

options.reporterOptions = reporterConfig.options;
return new _reporter(runner, options, outputPath);
});
runner.ignoreLeaks = options.ignoreLeaks !== false;
runner.fullStackTrace = options.fullStackTrace;
runner.asyncOnly = options.asyncOnly;
Expand All @@ -482,21 +521,34 @@ Mocha.prototype.run = function(fn) {
if (options.globals) {
runner.globals(options.globals);
}
// Use only the first reporter for growl, since we don't want to
// send several notifications for the same test suite
if (options.growl) {
this._growl(runner, reporter);
this._growl(runner, reporters[0]);
}

if (options.useColors !== undefined) {
exports.reporters.Base.useColors = options.useColors;
}

exports.reporters.Base.inlineDiffs = options.useInlineDiffs;

function done(failures) {
if (reporter.done) {
reporter.done(failures, fn);
} else {
fn && fn(failures);
function runnerDone(failures) {
function reporterDone(failures) {
if (--remain === 0) {
fn && fn(failures);
}
}

var remain = reporters.length;
reporters.forEach(function(reporter) {
if (reporter.done) {
reporter.done(failures, reporterDone);
} else {
reporterDone(failures);
}
});
}

return runner.run(done);
return runner.run(runnerDone);
};
41 changes: 40 additions & 1 deletion lib/reporters/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

var tty = require('tty');
var fs = require('fs');
var diff = require('diff');
var ms = require('../ms');
var utils = require('../utils');
Expand Down Expand Up @@ -231,16 +232,29 @@ exports.list = function(failures) {
* of tests passed / failed etc.
*
* @param {Runner} runner
* @param {Object} options runner optional options
* @param {string} outputPath optional filename to write the reporter output.
* @api public
*/

function Base(runner) {
function Base(runner, options, outputPath) {
var stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 };
var failures = this.failures = [];

this.options = options;
this.path = outputPath;

if (outputPath) {
if (!fs.createWriteStream) {
throw new Error('file output not supported in browser');
}
this.fileStream = fs.createWriteStream(outputPath);
}

if (!runner) {
return;
}

this.runner = runner;

runner.stats = stats;
Expand Down Expand Up @@ -332,6 +346,31 @@ Base.prototype.epilogue = function() {
console.log();
};

/**
* Write to reporter output stream
*/
Base.prototype.write = function(str) {
if (this.fileStream) {
this.fileStream.write(str);
} else {
process.stdout.write(str);
}
};

Base.prototype.writeLine = function(line) {
this.write(line + '\n');
};

Base.prototype.done = function(failures, fn) {
if (this.fileStream) {
this.fileStream.end(function() {
fn(failures);
});
} else {
fn(failures);
}
};

/**
* Pad the given `str` to `len`.
*
Expand Down
14 changes: 10 additions & 4 deletions lib/reporters/html-cov.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

var JSONCov = require('./json-cov');
var inherits = require('../utils').inherits;
var readFileSync = require('fs').readFileSync;
var join = require('path').join;

Expand All @@ -18,23 +19,28 @@ exports = module.exports = HTMLCov;
* @api public
* @param {Runner} runner
*/
function HTMLCov(runner) {
function HTMLCov(runner, options, outputPath) {
JSONCov.call(this, runner, options, outputPath, false);

var jade = require('jade');
var file = join(__dirname, '/templates/coverage.jade');
var str = readFileSync(file, 'utf8');
var fn = jade.compile(str, { filename: file });
var self = this;

JSONCov.call(this, runner, false);

runner.on('end', function() {
process.stdout.write(fn({
self.write(fn({
cov: self.cov,
coverageClass: coverageClass
}));
});
}

/**
* Inherit from `JSONCov.prototype`.
*/
inherits(HTMLCov, JSONCov);

/**
* Return coverage class for a given coverage percentage.
*
Expand Down
12 changes: 9 additions & 3 deletions lib/reporters/json-cov.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
*/

var Base = require('./base');
var inherits = require('../utils').inherits;

/**
* Expose `JSONCov`.
Expand All @@ -17,8 +18,8 @@ exports = module.exports = JSONCov;
* @param {Runner} runner
* @param {boolean} output
*/
function JSONCov(runner, output) {
Base.call(this, runner);
function JSONCov(runner, options, outputPath, output) {
Base.call(this, runner, options, outputPath);

output = arguments.length === 1 || output;
var self = this;
Expand Down Expand Up @@ -48,10 +49,15 @@ function JSONCov(runner, output) {
if (!output) {
return;
}
process.stdout.write(JSON.stringify(result, null, 2));
self.write(JSON.stringify(result, null, 2));
});
}

/**
* Inherit from `Base.prototype`.
*/
inherits(JSONCov, Base);

/**
* Map jscoverage data to a JSON structure
* suitable for reporting.
Expand Down
Loading