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 #1772

Closed
wants to merge 1 commit into from
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
61 changes: 48 additions & 13 deletions bin/_mocha
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ program
.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('-R, --reporter <name>,...', 'specify the reporters to use', 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 @@ -193,13 +193,44 @@ 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) {
throw new Error("invalid reporter option '" + opt + "'");

program.reporterOptions.split(",").forEach(

// Two possible formats ...
/\w+{(\w+:\w+)+}/.test(program.reporterOptions) ?

// per-reporter config:
// spec{a:1,b:2},dot{c:3,d:4}
function(opt) {
var m = opt.match(/(\w+){(\w+:\w+)+}/);
if (m.length !== 3) {
throw new("invalid reporter option '" + opt + "'");
}

var reporterName = m[1]
, reporterOptStr = m[2];

reporterOptStr.split(",").forEach(function (reporterOpt) {
var L = reporterOpt.split(':');
if (L.length !== 2) {
throw new Error("invalid reporter option '" + opt + "'");
}
reporterOptions[L[0]] = L[1];
});
reporterOptions[reporterName] = reporterOptions[reporterName] || {};
reporterOptions[reporterName][L[0]] = L[1];
});
} :

// single reporter config:
// a=1,b=2
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 @@ -212,15 +243,19 @@ mocha.ui(program.ui);

// load reporter

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];
Reporter = require('../lib/reporters/' + reporterName);
} catch (err) {
throw new Error('reporter "' + program.reporter + '" does not exist');
try {
Reporter = require(reporterName);
} catch (err) {
throw new Error('reporter "' + reporterName + '" does not exist');
}
}
}
});

// --no-colors

Expand Down
107 changes: 80 additions & 27 deletions lib/mocha.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,32 +131,63 @@ 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.
* @param {Object} reporterOptions optional options TODO FIXME
* @api public
*/
Mocha.prototype.reporter = function(reporter, reporterOptions){
if ('function' == typeof reporter) {
this._reporter = reporter;
} else {
reporter = reporter || 'spec';
var _reporter;
try { _reporter = require('./reporters/' + reporter); } catch (err) {}
if (!_reporter) try { _reporter = require(reporter); } catch (err) {
err.message.indexOf('Cannot find module') !== -1
? console.warn('"' + reporter + '" reporter not found')
: console.warn('"' + reporter + '" reporter blew up with error:\n' + err.stack);

Mocha.prototype.reporter = function(reporters, reporterOptions){
// if no reporter is given as input, default to spec reporter
reporters = reporters || ['spec'];
reporterOptions = reporterOptions || {};

// If reporters argument is not a list, turn it into a list of reporter names
// or constructors that we'll iterate on right after to initialize them
if (!Array.isArray(reporters)) {
if (('string' == typeof reporters) ||
('function' == typeof reporters)) {
reporters = [reporters];
}
if (!_reporter && reporter === 'teamcity')
console.warn('The Teamcity reporter was moved to a package named ' +
'mocha-teamcity-reporter ' +
'(https://npmjs.org/package/mocha-teamcity-reporter).');
if (!_reporter) throw new Error('invalid reporter "' + reporter + '"');
this._reporter = _reporter;
}
this.options.reporterOptions = reporterOptions;

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

reporterName = reporterConfig.split(':');
if (reporterName.length > 1) {
path = reporterName[1];
reporterName = reporterName[0];
}
try { _reporter = require('./reporters/' + reporterName); } catch (err) {};
if (!_reporter) try { _reporter = require(reporterName); } catch (err) {
err.message.indexOf('Cannot find module') !== -1
? console.warn('"' + reporterName + '" reporter not found')
: console.warn('"' + reporterName + '" reporter blew up with error:\n' + err.stack);
}
if (!_reporter && reporterName === 'teamcity')
console.warn('The Teamcity reporter was moved to a package named ' +
'mocha-teamcity-reporter ' +
'(https://npmjs.org/package/mocha-teamcity-reporter).');
if (!_reporter) throw new Error('invalid reporter "' + reporterName + '"');
return {
fn: _reporter,
path: path,
options: reporterOptions[reporterName] || reporterOptions._default || {}
};
}
}, this);

return this;
};

Expand Down Expand Up @@ -424,23 +455,45 @@ 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
, path = reporterConfig.path;

options.reporterOptions = reporterConfig.options;
return new _reporter(runner, options, path);
});
runner.ignoreLeaks = false !== options.ignoreLeaks;
runner.fullStackTrace = options.fullStackTrace;
runner.asyncOnly = options.asyncOnly;
if (options.grep) runner.grep(options.grep, options.invert);
if (options.globals) runner.globals(options.globals);
if (options.growl) this._growl(runner, reporter);

// 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, reporters[0]);

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

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

function done(failures) {
function runnerDone(failures) {
var remain = reporters.length
, reporterDone = function(failures) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fyi: with the linting work that's being done, we're not going to be using leading commas. also, the below conditional will need curlies

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Roger.

if (--remain === 0) fn && fn(failures);
};

reporters.forEach(function (reporter) {
if (reporter.done) {
reporter.done(failures, fn);
} else fn && fn(failures);
reporter.done(failures, reporterDone);
} else {
reporterDone(failures);
}
});
}

return runner.run(done);
return runner.run(runnerDone);
};
39 changes: 38 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')
, fs = require('fs')
, diff = require('diff')
, ms = require('../ms')
, utils = require('../utils')
Expand Down Expand Up @@ -225,11 +226,21 @@ exports.list = function(failures){
* @api public
*/

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

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

if (path) {
if (!fs.createWriteStream) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking about this while working on my branch for plugin support.

It seems what'd be even more useful is a generic writable stream. at the risk of ballooning the inheritance tree, I went with keeping the base reporter completely generic. then, browser-based reporters would use it, and node-based ones would use a subclass StreamReporter or something. they'd just write to their internal stream--whatever that happened to be. so what if we passed in a writable stream instance instead of a path? that'd probably eliminate the conditionals below.

you don't have to do this, since my code will conflict with just about everything anyway, but something to think about.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that gives me an idea for a server-side reporter which launches the HTML reporter and uses websockets to stream the results. 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the idea of a generic writable stream hit me too ... but I'm hesitant to do much refactoring though, given how long this pull request has been floating around (almost a year). If there's a way to get it out as-is, and follow-up with improvements, I'm more keen to that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

understood

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

if (!runner) return;
this.runner = runner;

Expand Down Expand Up @@ -323,6 +334,32 @@ 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
4 changes: 2 additions & 2 deletions lib/reporters/json.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ exports = module.exports = JSONReporter;
* @api public
*/

function JSONReporter(runner) {
function JSONReporter(runner, options, path) {
var self = this;
Base.call(this, runner);
Base.call(this, runner, options, path);

var tests = []
, pending = []
Expand Down
46 changes: 8 additions & 38 deletions lib/reporters/xunit.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,12 @@ exports = module.exports = XUnit;
* @api public
*/

function XUnit(runner, options) {
Base.call(this, runner);
function XUnit(runner, options, path) {
Base.call(this, runner, options, path);
var stats = this.stats
, tests = []
, self = this;

if (options.reporterOptions && options.reporterOptions.output) {
if (! fs.createWriteStream) {
throw new Error('file output not supported in browser');
}
self.fileStream = fs.createWriteStream(options.reporterOptions.output);
}

runner.on('pending', function(test){
tests.push(test);
});
Expand All @@ -56,7 +49,7 @@ function XUnit(runner, options) {
});

runner.on('end', function(){
self.write(tag('testsuite', {
self.writeLine(tag('testsuite', {
name: 'Mocha Tests'
, tests: stats.tests
, failures: stats.failures
Expand All @@ -67,40 +60,16 @@ function XUnit(runner, options) {
}, false));

tests.forEach(function(t) { self.test(t); });
self.write('</testsuite>');
self.writeLine('</testsuite>');
});
}

/**
* Override done to close the stream (if it's a file).
*/
XUnit.prototype.done = function(failures, fn) {
if (this.fileStream) {
this.fileStream.end(function() {
fn(failures);
});
} else {
fn(failures);
}
};

/**
* Inherit from `Base.prototype`.
*/

XUnit.prototype.__proto__ = Base.prototype;

/**
* Write out the given line
*/
XUnit.prototype.write = function(line) {
if (this.fileStream) {
this.fileStream.write(line + '\n');
} else {
console.log(line);
}
};

/**
* Output tag for the given `test.`
*/
Expand All @@ -112,13 +81,14 @@ XUnit.prototype.test = function(test, ostream) {
, time: (test.duration / 1000) || 0
};


if ('failed' == test.state) {
var err = test.err;
this.write(tag('testcase', attrs, false, tag('failure', {}, false, cdata(escape(err.message) + "\n" + err.stack))));
this.writeLine(tag('testcase', attrs, false, tag('failure', {}, false, cdata(escape(err.message) + "\n" + err.stack))));
} else if (test.pending) {
this.write(tag('testcase', attrs, false, tag('skipped', {}, true)));
this.writeLine(tag('testcase', attrs, false, tag('skipped', {}, true)));
} else {
this.write(tag('testcase', attrs, true) );
this.writeLine(tag('testcase', attrs, true) );
}
};

Expand Down
2 changes: 1 addition & 1 deletion test/reporters/json.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ describe('json reporter', function(){
});
suite = new Suite('JSON suite', 'root');
runner = new Runner(suite);
var mochaReporter = new mocha._reporter(runner);
var mochaReporter = new mocha._reporters[0].fn(runner);
});

it('should have 1 test failure', function(done){
Expand Down
Loading