From 8736b3ceb42d9c2cfb4317a5f33edef3cea0c67a Mon Sep 17 00:00:00 2001 From: Santiago Aguiar Date: Wed, 3 Feb 2016 13:48:40 -0200 Subject: [PATCH 1/2] Added support for multiple reporters. --- bin/_mocha | 62 +++++++++++++++------- lib/mocha.js | 102 ++++++++++++++++++++++++++++--------- lib/reporters/base.js | 41 ++++++++++++++- lib/reporters/json.js | 4 +- lib/reporters/xunit.js | 6 +-- test/reporters/json.js | 2 +- test/reporters/multiple.js | 99 +++++++++++++++++++++++++++++++++++ 7 files changed, 267 insertions(+), 49 deletions(-) create mode 100644 test/reporters/multiple.js diff --git a/bin/_mocha b/bin/_mocha index 8a53888e1a..1919c193f7 100755 --- a/bin/_mocha +++ b/bin/_mocha @@ -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 ', 'reporter-specific options') - .option('-R, --reporter ', 'specify the reporter to use', 'spec') + .option('-O, --reporter-options ', 'reporter-specific options, use name:{k=v,k2=v2,...} for multiple reporters') + .option('-R, --reporter ,...', '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") @@ -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 @@ -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 diff --git a/lib/mocha.js b/lib/mocha.js index fc493d2d25..b5aca73f87 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -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'); /** @@ -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'); @@ -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 ":" 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) { @@ -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; }; @@ -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; @@ -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); }; diff --git a/lib/reporters/base.js b/lib/reporters/base.js index bb107ba229..a451d2437a 100644 --- a/lib/reporters/base.js +++ b/lib/reporters/base.js @@ -3,6 +3,7 @@ */ var tty = require('tty'); +var fs = require('fs'); var diff = require('diff'); var ms = require('../ms'); var utils = require('../utils'); @@ -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; @@ -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`. * diff --git a/lib/reporters/json.js b/lib/reporters/json.js index cd9ec286b7..a185d79551 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -16,8 +16,8 @@ exports = module.exports = JSONReporter; * @api public * @param {Runner} runner */ -function JSONReporter(runner) { - Base.call(this, runner); +function JSONReporter(runner, options, outputPath) { + Base.call(this, runner, options, outputPath); var self = this; var tests = []; diff --git a/lib/reporters/xunit.js b/lib/reporters/xunit.js index 01d9d87824..bc9bcbd51a 100644 --- a/lib/reporters/xunit.js +++ b/lib/reporters/xunit.js @@ -34,8 +34,8 @@ exports = module.exports = XUnit; * @api public * @param {Runner} runner */ -function XUnit(runner, options) { - Base.call(this, runner); +function XUnit(runner, options, outputPath) { + Base.call(this, runner, options, outputPath); var stats = this.stats; var tests = []; @@ -132,7 +132,7 @@ XUnit.prototype.test = function(test) { var err = test.err; this.write(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)); } diff --git a/test/reporters/json.js b/test/reporters/json.js index e7b8955757..bb4e8648e9 100644 --- a/test/reporters/json.js +++ b/test/reporters/json.js @@ -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){ diff --git a/test/reporters/multiple.js b/test/reporters/multiple.js new file mode 100644 index 0000000000..e343daf66e --- /dev/null +++ b/test/reporters/multiple.js @@ -0,0 +1,99 @@ + +var Mocha = require('../../'); +var Suite = Mocha.Suite; +var Runner = Mocha.Runner; +var Test = Mocha.Test; +var should = require('should'); + +describe('multiple reporters', function() { + var suite; + var runner; + var specReporter; + var jsonReporter; + + it('should have 1 test failure', function(done) { + var mocha = new Mocha({ + reporter: ['spec', 'json'] + }); + suite = new Suite('Multiple reporters suite', 'root'); + runner = new Runner(suite); + specReporter = new mocha._reporters[0].fn(runner); + jsonReporter = new mocha._reporters[1].fn(runner); + + var testTitle = 'json test 1'; + var error = { message: 'oh shit' }; + + suite.addTest(new Test(testTitle, function(done) { + done(new Error(error.message)); + })); + + runner.run(function() { + // Verify that each reporter ran + specReporter.should.have.property('failures'); + specReporter.failures.should.be.an.instanceOf(Array); + specReporter.failures.should.have.a.lengthOf(1); + + jsonReporter.should.have.property('failures'); + jsonReporter.failures.should.be.an.instanceOf(Array); + jsonReporter.failures.should.have.a.lengthOf(1); + done(); + }); + }); + + it('should pass correct reporter options and path to each reporter', function(done) { + var mocha = new Mocha({ + reporter: [ + 'spec', + 'dot:/var/log/dot.txt', + 'json:json.json' + ], + reporterOptions: { + spec: { foo: 'bar' }, + json: { bar: 'baz' } + } + }); + + // specReporter + mocha._reporters[0].fn = function(runner, options, path) { + options.reporterOptions.should.have.property('foo', 'bar'); + should.equal(path, undefined); + }; + + // dot (no options) + mocha._reporters[1].fn = function(runner, options, path) { + options.reporterOptions.should.eql({}); + path.should.equal('/var/log/dot.txt'); + }; + + // json + mocha._reporters[2].fn = function(runner, options, path) { + options.reporterOptions.should.have.property('bar', 'baz'); + path.should.equal('json.json'); + done(); + }; + + mocha.run(); + }); + + it('should pass _default reporter options to each reporter', function(done) { + var mocha = new Mocha({ + reporter: ['spec', 'json'], + reporterOptions: { + _default: { foo: 'bar' } + } + }); + + // specReporter + mocha._reporters[0].fn = function(runner, options) { + options.reporterOptions.should.have.property('foo', 'bar'); + }; + + // json + mocha._reporters[1].fn = function(runner, options) { + options.reporterOptions.should.have.property('foo', 'bar'); + done(); + }; + + mocha.run(); + }); +}); From ce35ee0320dd1c66b6089118a01191cc4242c4fa Mon Sep 17 00:00:00 2001 From: Santiago Aguiar Date: Wed, 3 Feb 2016 13:48:40 -0200 Subject: [PATCH 2/2] Use write() from Base reporter. --- lib/reporters/html-cov.js | 14 ++++++++++---- lib/reporters/json-cov.js | 12 +++++++++--- lib/reporters/json.js | 8 +++++++- lib/reporters/markdown.js | 17 ++++++++++++----- test/reporters/multiple.js | 29 ++++++++++++++++++++++++++++- 5 files changed, 66 insertions(+), 14 deletions(-) diff --git a/lib/reporters/html-cov.js b/lib/reporters/html-cov.js index e3f2dd91e3..bfdc095775 100644 --- a/lib/reporters/html-cov.js +++ b/lib/reporters/html-cov.js @@ -3,6 +3,7 @@ */ var JSONCov = require('./json-cov'); +var inherits = require('../utils').inherits; var readFileSync = require('fs').readFileSync; var join = require('path').join; @@ -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. * diff --git a/lib/reporters/json-cov.js b/lib/reporters/json-cov.js index 5a32569f03..66e7b0bc69 100644 --- a/lib/reporters/json-cov.js +++ b/lib/reporters/json-cov.js @@ -3,6 +3,7 @@ */ var Base = require('./base'); +var inherits = require('../utils').inherits; /** * Expose `JSONCov`. @@ -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; @@ -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. diff --git a/lib/reporters/json.js b/lib/reporters/json.js index a185d79551..e10817c90b 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -3,6 +3,7 @@ */ var Base = require('./base'); +var inherits = require('../utils').inherits; /** * Expose `JSON`. @@ -52,10 +53,15 @@ function JSONReporter(runner, options, outputPath) { runner.testResults = obj; - process.stdout.write(JSON.stringify(obj, null, 2)); + self.write(JSON.stringify(obj, null, 2)); }); } +/** + * Inherit from `Base.prototype`. + */ +inherits(JSONReporter, Base); + /** * Return a plain-object representation of `test` * free of cyclic properties etc. diff --git a/lib/reporters/markdown.js b/lib/reporters/markdown.js index 680c55d709..4bc2de7e05 100644 --- a/lib/reporters/markdown.js +++ b/lib/reporters/markdown.js @@ -4,6 +4,7 @@ var Base = require('./base'); var utils = require('../utils'); +var inherits = utils.inherits; /** * Constants @@ -23,9 +24,10 @@ exports = module.exports = Markdown; * @api public * @param {Runner} runner */ -function Markdown(runner) { - Base.call(this, runner); +function Markdown(runner, options, outputPath) { + Base.call(this, runner, options, outputPath); + var self = this; var level = 0; var buf = ''; @@ -90,8 +92,13 @@ function Markdown(runner) { }); runner.on('end', function() { - process.stdout.write('# TOC\n'); - process.stdout.write(generateTOC(runner.suite)); - process.stdout.write(buf); + self.write('# TOC\n'); + self.write(generateTOC(runner.suite)); + self.write(buf); }); } + +/** + * Inherit from `Base.prototype`. + */ +inherits(Markdown, Base); diff --git a/test/reporters/multiple.js b/test/reporters/multiple.js index e343daf66e..b84c9739cf 100644 --- a/test/reporters/multiple.js +++ b/test/reporters/multiple.js @@ -1,4 +1,6 @@ - +var fs = require('fs'); +var os = require('os'); +var path = require('path'); var Mocha = require('../../'); var Suite = Mocha.Suite; var Runner = Mocha.Runner; @@ -40,6 +42,31 @@ describe('multiple reporters', function() { }); }); + it('should write result to its output path', function(done) { + var outputFile = path.join(os.tmpDir(), 'mocha-multiple.json'); + var mocha = new Mocha({ + reporter: ['json:' + outputFile] + }); + var testTitle = 'json output test 1'; + var error = { message: 'oh shit' }; + + mocha.suite.addTest(new Test(testTitle, function(done) { + done(new Error(error.message)); + })); + + mocha.run(function() { + var content = fs.readFileSync(outputFile, 'UTF-8'); + try { + JSON.parse(content); + done(); + } catch (e) { + done(e); + } finally { + fs.unlinkSync(outputFile); + } + }); + }); + it('should pass correct reporter options and path to each reporter', function(done) { var mocha = new Mocha({ reporter: [