diff --git a/Makefile b/Makefile index e8b7658e8b..e1f547e175 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,7 @@ TM_DEST = ~/Library/Application\ Support/TextMate/Bundles TM_BUNDLE = JavaScript\ mocha.tmbundle SRC = $(shell find lib -name "*.js" -type f | sort) SUPPORT = $(wildcard support/*.js) +REPORTERS := $(shell bin/mocha --reporters | sed -e 's/ - .*//') all: mocha.js @@ -33,7 +34,7 @@ lib-cov: test: test-unit -test-all: test-bdd test-tdd test-qunit test-exports test-unit test-grep test-jsapi test-compilers test-glob +test-all: test-bdd test-tdd test-qunit test-exports test-unit test-grep test-jsapi test-compilers test-glob test-reporter-output test-jsapi: @node test/jsapi @@ -104,6 +105,22 @@ test-async-only: test-glob: @./test/acceptance/glob/glob.sh + +# The html reporter isn't supported at the command line +TEST_REPORTERS := $(patsubst %,test-reporter-output-%,$(REPORTERS)) +TEST_REPORTERS := $(filter-out test-reporter-output-html,$(TEST_REPORTERS)) + +.PHONY: $(REPORTERS) +test-reporter-output: $(TEST_REPORTERS) +test-reporter-output-%: % + @echo "Testing file output for reporter $<" + @./bin/mocha --no-color --reporter $< test/acceptance/interfaces/bdd 2>&1 > /tmp/$<.stdout + @./bin/mocha --no-color --reporter $< test/acceptance/interfaces/bdd -O /tmp/$<.file 2>&1 > /tmp/dot.file.stdout + @test -s /tmp/$<.file || \ + (echo "ERROR: reporter $< does not support file output" && exit 1) + @test ! -s /tmp/$<.file.stdout || \ + (echo "ERROR: reporter $< file output wrote to stdout" && exit 1) + non-tty: @./bin/mocha \ --reporter dot \ diff --git a/bin/_mocha b/bin/_mocha index 7329505159..182c3db590 100755 --- a/bin/_mocha +++ b/bin/_mocha @@ -59,6 +59,7 @@ program .usage('[debug] [options] [files]') .option('-r, --require ', 'require the given module') .option('-R, --reporter ', 'specify the reporter to use', 'dot') + .option('-O, --output ', 'specify the output file', '-') .option('-u, --ui ', 'specify user-interface (bdd|tdd|exports)', 'bdd') .option('-g, --grep ', 'only run tests matching ') .option('-i, --invert', 'inverts --grep matches') @@ -177,6 +178,10 @@ Error.stackTraceLimit = Infinity; // TODO: config mocha.reporter(program.reporter); +// output + +mocha.output(program.output); + // interface mocha.ui(program.ui); diff --git a/lib/mocha.js b/lib/mocha.js index cd3d303268..e52fde65df 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -27,6 +27,7 @@ exports.reporters = require('./reporters'); exports.Runnable = require('./runnable'); exports.Context = require('./context'); exports.Runner = require('./runner'); +exports.output = require('./output'); exports.Suite = require('./suite'); exports.Hook = require('./hook'); exports.Test = require('./test'); @@ -70,6 +71,7 @@ function Mocha(options) { this.ui(options.ui); this.bail(options.bail); this.reporter(options.reporter); + this.output(options.output); if (options.timeout) this.timeout(options.timeout); if (options.slow) this.slow(options.slow); } @@ -121,6 +123,17 @@ Mocha.prototype.reporter = function(reporter){ return this; }; +/** + * Set output to `output`. + * + * @param {stream|String} target output stream, filename, or "-" for stdout + * @api public + */ + +Mocha.prototype.output = function(target) { + this._output = exports.output.initialize(target); +}; + /** * Set test UI `name`, defaults to "bdd". * @@ -306,11 +319,14 @@ Mocha.prototype.run = function(fn){ var suite = this.suite; var options = this.options; var runner = new exports.Runner(suite); - var reporter = new this._reporter(runner); + var reporter = new this._reporter(runner, this._output); runner.ignoreLeaks = false !== options.ignoreLeaks; 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); - return runner.run(fn); + var self = this; + return runner.run(function(errCount) { + exports.output.end(errCount, fn); + }); }; diff --git a/lib/output.js b/lib/output.js new file mode 100644 index 0000000000..a6eb370d46 --- /dev/null +++ b/lib/output.js @@ -0,0 +1,78 @@ +var debug = require('debug')('mocha:output'); +var util = require('util'); +var fs = require('fs'); +var stream = require('stream'); + +// Initialize in stdout mode +var ostream = exports.ostream = process.stdout; + +/** + * Write preformatted output directly to the stream. + */ +exports.write = function(data) { + ostream.write(data); +}; + + +/** + * Wrapper for log output. + */ +exports.log = function() { + if (ostream === process.stdout) { + console.log.apply(console, arguments); + } else { + ostream.write(util.format.apply(util, arguments) + "\n"); + } +}; + +/** + * Wrapper for error output. + */ +exports.error = function() { + if (ostream === process.stdout) { + console.error.apply(console, arguments); + } else { + ostream.write(util.format.apply(util, arguments) + "\n"); + } +}; + +/** + * Hook that's called when the test process is done to close the + * ostream ostream. + */ +exports.end = function(errCount, fn) { + if (ostream === process.stdout) { + // nothing to do + return fn(errCount); + } + + ostream.end(function() { + debug("ostream ended"); + fn(errCount); + + // Reset the ostream + ostream = process.stdout; + } ); +}; + +/** + * Initialize the output module. + * + * If target is undefined or "-" then use stdout, otherwise open the + * target file. + */ +var initialize = exports.initialize = function(target) { + if (target === undefined) { + return ostream; // don't change + } + + if (target === "-") { + ostream = exports.ostream = process.stdout; + } else if (target instanceof stream) { + ostream = exports.ostream = target; + } else { + ostream = exports.ostream = fs.createWriteStream(target); + } + + return ostream; +}; diff --git a/lib/reporters/base.js b/lib/reporters/base.js index 215be4562c..1265bff7fa 100644 --- a/lib/reporters/base.js +++ b/lib/reporters/base.js @@ -5,7 +5,8 @@ var tty = require('tty') , diff = require('diff') - , ms = require('../ms'); + , ms = require('../ms') + , output = require('../output'); /** * Save timer references to avoid Sinon interfering (see GH-237). @@ -29,6 +30,12 @@ var isatty = tty.isatty(1) && tty.isatty(2); exports = module.exports = Base; +/** + * Expose the output hooks + */ +exports.output = output; + + /** * Enable coloring by default. */ @@ -108,6 +115,7 @@ exports.window = { : 75 }; + /** * Expose some basic cursor interactions * that are common among reporters. @@ -144,7 +152,7 @@ exports.cursor = { */ exports.list = function(failures){ - console.error(); + output.error(); failures.forEach(function(test, i){ // format var fmt = color('error title', ' %s) %s:\n') @@ -206,7 +214,7 @@ exports.list = function(failures){ stack = stack.slice(index ? index + 1 : index) .replace(/^/gm, ' '); - console.error(fmt, (i + 1), test.fullTitle(), msg, stack); + output.error(fmt, (i + 1), test.fullTitle(), msg, stack); }); }; @@ -222,7 +230,7 @@ exports.list = function(failures){ * @api public */ -function Base(runner) { +function Base(runner, ostream) { var self = this , stats = this.stats = { suites: 0, tests: 0, passes: 0, pending: 0, failures: 0 } , failures = this.failures = []; @@ -230,6 +238,9 @@ function Base(runner) { if (!runner) return; this.runner = runner; + if (!ostream) return; + this.ostream = ostream; + runner.stats = stats; runner.on('start', function(){ @@ -288,14 +299,14 @@ Base.prototype.epilogue = function(){ var tests; var fmt; - console.log(); + output.log(); // passes fmt = color('bright pass', ' ') + color('green', ' %d passing') + color('light', ' (%s)'); - console.log(fmt, + output.log(fmt, stats.passes || 0, ms(stats.duration)); @@ -304,21 +315,21 @@ Base.prototype.epilogue = function(){ fmt = color('pending', ' ') + color('pending', ' %d pending'); - console.log(fmt, stats.pending); + output.log(fmt, stats.pending); } // failures if (stats.failures) { fmt = color('fail', ' %d failing'); - console.error(fmt, + output.error(fmt, stats.failures); Base.list(this.failures); - console.error(); + output.error(); } - console.log(); + output.log(); }; /** diff --git a/lib/reporters/doc.js b/lib/reporters/doc.js index 2e5bf57fc4..11e06a4311 100644 --- a/lib/reporters/doc.js +++ b/lib/reporters/doc.js @@ -4,6 +4,7 @@ */ var Base = require('./base') + , output = Base.output , utils = require('../utils'); /** @@ -19,8 +20,8 @@ exports = module.exports = Doc; * @api public */ -function Doc(runner) { - Base.call(this, runner); +function Doc(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats @@ -34,23 +35,23 @@ function Doc(runner) { runner.on('suite', function(suite){ if (suite.root) return; ++indents; - console.log('%s
', indent()); + output.log('%s
', indent()); ++indents; - console.log('%s

%s

', indent(), utils.escape(suite.title)); - console.log('%s
', indent()); + output.log('%s

%s

', indent(), utils.escape(suite.title)); + output.log('%s
', indent()); }); runner.on('suite end', function(suite){ if (suite.root) return; - console.log('%s
', indent()); + output.log('%s
', indent()); --indents; - console.log('%s
', indent()); + output.log('%s
', indent()); --indents; }); runner.on('pass', function(test){ - console.log('%s
%s
', indent(), utils.escape(test.title)); + output.log('%s
%s
', indent(), utils.escape(test.title)); var code = utils.escape(utils.clean(test.fn.toString())); - console.log('%s
%s
', indent(), code); + output.log('%s
%s
', indent(), code); }); } diff --git a/lib/reporters/dot.js b/lib/reporters/dot.js index 0c298ba71d..86d031ef5f 100644 --- a/lib/reporters/dot.js +++ b/lib/reporters/dot.js @@ -19,8 +19,8 @@ exports = module.exports = Dot; * @api public */ -function Dot(runner) { - Base.call(this, runner); +function Dot(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats @@ -28,29 +28,29 @@ function Dot(runner) { , n = 0; runner.on('start', function(){ - process.stdout.write('\n '); + ostream.write('\n '); }); runner.on('pending', function(test){ - process.stdout.write(color('pending', Base.symbols.dot)); + ostream.write(color('pending', Base.symbols.dot)); }); runner.on('pass', function(test){ - if (++n % width == 0) process.stdout.write('\n '); + if (++n % width == 0) ostream.write('\n '); if ('slow' == test.speed) { - process.stdout.write(color('bright yellow', Base.symbols.dot)); + ostream.write(color('bright yellow', Base.symbols.dot)); } else { - process.stdout.write(color(test.speed, Base.symbols.dot)); + ostream.write(color(test.speed, Base.symbols.dot)); } }); runner.on('fail', function(test, err){ - if (++n % width == 0) process.stdout.write('\n '); - process.stdout.write(color('fail', Base.symbols.dot)); + if (++n % width == 0) ostream.write('\n '); + ostream.write(color('fail', Base.symbols.dot)); }); runner.on('end', function(){ - console.log(); + ostream.write("\n"); self.epilogue(); }); } @@ -59,4 +59,4 @@ function Dot(runner) { * Inherit from `Base.prototype`. */ -Dot.prototype.__proto__ = Base.prototype; \ No newline at end of file +Dot.prototype.__proto__ = Base.prototype; diff --git a/lib/reporters/html-cov.js b/lib/reporters/html-cov.js index bfb27ffdf6..dc5a67d7e9 100644 --- a/lib/reporters/html-cov.js +++ b/lib/reporters/html-cov.js @@ -19,17 +19,17 @@ exports = module.exports = HTMLCov; * @api public */ -function HTMLCov(runner) { +function HTMLCov(runner, ostream) { var jade = require('jade') , file = __dirname + '/templates/coverage.jade' , str = fs.readFileSync(file, 'utf8') , fn = jade.compile(str, { filename: file }) , self = this; - JSONCov.call(this, runner, false); + JSONCov.call(this, runner, ostream, false); runner.on('end', function(){ - process.stdout.write(fn({ + ostream.write(fn({ cov: self.cov , coverageClass: coverageClass })); @@ -48,4 +48,4 @@ function coverageClass(n) { if (n >= 50) return 'medium'; if (n >= 25) return 'low'; return 'terrible'; -} \ No newline at end of file +} diff --git a/lib/reporters/html.js b/lib/reporters/html.js index f09279be20..ce679fc01c 100644 --- a/lib/reporters/html.js +++ b/lib/reporters/html.js @@ -42,8 +42,8 @@ var statsTemplate = '
    ' * @api public */ -function HTML(runner, root) { - Base.call(this, runner); +function HTML(runner, ostream, root) { + Base.call(this, ostream, runner); var self = this , stats = this.stats diff --git a/lib/reporters/json-cov.js b/lib/reporters/json-cov.js index 73d0009377..77ec2b273b 100644 --- a/lib/reporters/json-cov.js +++ b/lib/reporters/json-cov.js @@ -19,11 +19,11 @@ exports = module.exports = JSONCov; * @api public */ -function JSONCov(runner, output) { +function JSONCov(runner, ostream, output) { var self = this - , output = 1 == arguments.length ? true : output; + , output = 2 == arguments.length ? true : output; - Base.call(this, runner); + Base.call(this, runner, ostream); var tests = [] , failures = [] @@ -49,7 +49,7 @@ function JSONCov(runner, output) { result.failures = failures.map(clean); result.passes = passes.map(clean); if (!output) return; - process.stdout.write(JSON.stringify(result, null, 2 )); + ostream.write(JSON.stringify(result, null, 2 )); }); } diff --git a/lib/reporters/json-stream.js b/lib/reporters/json-stream.js index 7cb8fbede7..fe18eefe78 100644 --- a/lib/reporters/json-stream.js +++ b/lib/reporters/json-stream.js @@ -19,27 +19,27 @@ exports = module.exports = List; * @api public */ -function List(runner) { - Base.call(this, runner); +function List(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats , total = runner.total; runner.on('start', function(){ - console.log(JSON.stringify(['start', { total: total }])); + ostream.write(JSON.stringify(['start', { total: total }]) + "\n"); }); runner.on('pass', function(test){ - console.log(JSON.stringify(['pass', clean(test)])); + ostream.write(JSON.stringify(['pass', clean(test)]) + "\n"); }); runner.on('fail', function(test, err){ - console.log(JSON.stringify(['fail', clean(test)])); + ostream.write(JSON.stringify(['fail', clean(test)]) + "\n"); }); runner.on('end', function(){ - process.stdout.write(JSON.stringify(['end', self.stats])); + ostream.write(JSON.stringify(['end', self.stats]) + "\n"); }); } @@ -58,4 +58,4 @@ function clean(test) { , fullTitle: test.fullTitle() , duration: test.duration } -} \ No newline at end of file +} diff --git a/lib/reporters/json.js b/lib/reporters/json.js index a699f5014a..674440bf25 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -20,9 +20,9 @@ exports = module.exports = JSONReporter; * @api public */ -function JSONReporter(runner) { +function JSONReporter(runner, ostream) { var self = this; - Base.call(this, runner); + Base.call(this, runner, ostream); var tests = [] , failures = [] @@ -48,7 +48,7 @@ function JSONReporter(runner) { , passes: passes.map(clean) }; - process.stdout.write(JSON.stringify(obj, null, 2)); + ostream.write(JSON.stringify(obj, null, 2)); }); } @@ -67,4 +67,4 @@ function clean(test) { , fullTitle: test.fullTitle() , duration: test.duration } -} \ No newline at end of file +} diff --git a/lib/reporters/landing.js b/lib/reporters/landing.js index bf064f64b2..8ece1cd2a4 100644 --- a/lib/reporters/landing.js +++ b/lib/reporters/landing.js @@ -38,14 +38,13 @@ Base.colors.runway = 90; * @api public */ -function Landing(runner) { - Base.call(this, runner); +function Landing(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats , width = Base.window.width * .75 | 0 , total = runner.total - , stream = process.stdout , plane = color('plane', '✈') , crashed = -1 , n = 0; @@ -56,7 +55,7 @@ function Landing(runner) { } runner.on('start', function(){ - stream.write('\n '); + ostream.write('\n '); cursor.hide(); }); @@ -73,19 +72,19 @@ function Landing(runner) { } // render landing strip - stream.write('\u001b[4F\n\n'); - stream.write(runway()); - stream.write('\n '); - stream.write(color('runway', Array(col).join('⋅'))); - stream.write(plane) - stream.write(color('runway', Array(width - col).join('⋅') + '\n')); - stream.write(runway()); - stream.write('\u001b[0m'); + ostream.write('\u001b[4F\n\n'); + ostream.write(runway()); + ostream.write('\n '); + ostream.write(color('runway', Array(col).join('⋅'))); + ostream.write(plane); + ostream.write(color('runway', Array(width - col).join('⋅') + '\n')); + ostream.write(runway()); + ostream.write('\u001b[0m'); }); runner.on('end', function(){ cursor.show(); - console.log(); + ostream.write("\n"); self.epilogue(); }); } @@ -94,4 +93,4 @@ function Landing(runner) { * Inherit from `Base.prototype`. */ -Landing.prototype.__proto__ = Base.prototype; \ No newline at end of file +Landing.prototype.__proto__ = Base.prototype; diff --git a/lib/reporters/list.js b/lib/reporters/list.js index 3328e157a8..dcae654a33 100644 --- a/lib/reporters/list.js +++ b/lib/reporters/list.js @@ -4,6 +4,7 @@ */ var Base = require('./base') + , output = Base.output , cursor = Base.cursor , color = Base.color; @@ -20,25 +21,25 @@ exports = module.exports = List; * @api public */ -function List(runner) { - Base.call(this, runner); +function List(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats , n = 0; runner.on('start', function(){ - console.log(); + output.log(); }); runner.on('test', function(test){ - process.stdout.write(color('pass', ' ' + test.fullTitle() + ': ')); + output.write(color('pass', ' ' + test.fullTitle() + ': ')); }); runner.on('pending', function(test){ var fmt = color('checkmark', ' -') + color('pending', ' %s'); - console.log(fmt, test.fullTitle()); + output.log(fmt, test.fullTitle()); }); runner.on('pass', function(test){ @@ -46,12 +47,12 @@ function List(runner) { + color('pass', ' %s: ') + color(test.speed, '%dms'); cursor.CR(); - console.log(fmt, test.fullTitle(), test.duration); + output.log(fmt, test.fullTitle(), test.duration); }); runner.on('fail', function(test, err){ cursor.CR(); - console.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); + output.log(color('fail', ' %d) %s'), ++n, test.fullTitle()); }); runner.on('end', self.epilogue.bind(self)); diff --git a/lib/reporters/markdown.js b/lib/reporters/markdown.js index 6383a64248..70f20e26ac 100644 --- a/lib/reporters/markdown.js +++ b/lib/reporters/markdown.js @@ -18,8 +18,8 @@ exports = module.exports = Markdown; * @api public */ -function Markdown(runner) { - Base.call(this, runner); +function Markdown(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats @@ -84,8 +84,8 @@ function Markdown(runner) { }); runner.on('end', function(){ - process.stdout.write('# TOC\n'); - process.stdout.write(generateTOC(runner.suite)); - process.stdout.write(buf); + ostream.write('# TOC\n'); + ostream.write(generateTOC(runner.suite)); + ostream.write(buf); }); -} \ No newline at end of file +} diff --git a/lib/reporters/min.js b/lib/reporters/min.js index 1b6117d065..c495fc0fce 100644 --- a/lib/reporters/min.js +++ b/lib/reporters/min.js @@ -18,14 +18,14 @@ exports = module.exports = Min; * @api public */ -function Min(runner) { - Base.call(this, runner); +function Min(runner, ostream) { + Base.call(this, runner, ostream); runner.on('start', function(){ // clear screen - process.stdout.write('\u001b[2J'); + ostream.write('\u001b[2J'); // set cursor position - process.stdout.write('\u001b[1;3H'); + ostream.write('\u001b[1;3H'); }); runner.on('end', this.epilogue.bind(this)); diff --git a/lib/reporters/nyan.js b/lib/reporters/nyan.js index b43e2440d0..099329e409 100644 --- a/lib/reporters/nyan.js +++ b/lib/reporters/nyan.js @@ -3,6 +3,7 @@ */ var Base = require('./base') + , output = Base.output , color = Base.color; /** @@ -18,8 +19,8 @@ exports = module.exports = NyanCat; * @api public */ -function NyanCat(runner) { - Base.call(this, runner); +function NyanCat(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats @@ -239,11 +240,11 @@ NyanCat.prototype.rainbowify = function(str){ }; /** - * Stdout helper. + * Output helper. */ function write(string) { - process.stdout.write(string); + output.write(string); } /** diff --git a/lib/reporters/progress.js b/lib/reporters/progress.js index 59536386cb..19b40549f2 100644 --- a/lib/reporters/progress.js +++ b/lib/reporters/progress.js @@ -27,8 +27,8 @@ Base.colors.progress = 90; * @api public */ -function Progress(runner, options) { - Base.call(this, runner); +function Progress(runner, ostream, options) { + Base.call(this, runner, ostream); var self = this , options = options || {} @@ -47,7 +47,7 @@ function Progress(runner, options) { // tests started runner.on('start', function(){ - console.log(); + ostream.write("\n"); cursor.hide(); }); @@ -60,13 +60,13 @@ function Progress(runner, options) { , i = width - n; cursor.CR(); - process.stdout.write('\u001b[J'); - process.stdout.write(color('progress', ' ' + options.open)); - process.stdout.write(Array(n).join(options.complete)); - process.stdout.write(Array(i).join(options.incomplete)); - process.stdout.write(color('progress', options.close)); + ostream.write('\u001b[J'); + ostream.write(color('progress', ' ' + options.open)); + ostream.write(Array(n).join(options.complete)); + ostream.write(Array(i).join(options.incomplete)); + ostream.write(color('progress', options.close)); if (options.verbose) { - process.stdout.write(color('progress', ' ' + complete + ' of ' + total)); + ostream.write(color('progress', ' ' + complete + ' of ' + total)); } }); @@ -74,7 +74,7 @@ function Progress(runner, options) { // and the failures if any runner.on('end', function(){ cursor.show(); - console.log(); + ostream.write("\n"); self.epilogue(); }); } diff --git a/lib/reporters/spec.js b/lib/reporters/spec.js index cd97261c12..18d491fe26 100644 --- a/lib/reporters/spec.js +++ b/lib/reporters/spec.js @@ -4,6 +4,7 @@ */ var Base = require('./base') + , output = Base.output , cursor = Base.cursor , color = Base.color; @@ -20,8 +21,8 @@ exports = module.exports = Spec; * @api public */ -function Spec(runner) { - Base.call(this, runner); +function Spec(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats @@ -33,26 +34,26 @@ function Spec(runner) { } runner.on('start', function(){ - console.log(); + output.log(); }); runner.on('suite', function(suite){ ++indents; - console.log(color('suite', '%s%s'), indent(), suite.title); + output.log(color('suite', '%s%s'), indent(), suite.title); }); runner.on('suite end', function(suite){ --indents; - if (1 == indents) console.log(); + if (1 == indents) output.log(); }); runner.on('test', function(test){ - process.stdout.write(indent() + color('pass', ' ◦ ' + test.title + ': ')); + output.write(indent() + color('pass', ' ◦ ' + test.title + ': ')); }); runner.on('pending', function(test){ var fmt = indent() + color('pending', ' - %s'); - console.log(fmt, test.title); + output.log(fmt, test.title); }); runner.on('pass', function(test){ @@ -61,20 +62,20 @@ function Spec(runner) { + color('checkmark', ' ' + Base.symbols.ok) + color('pass', ' %s '); cursor.CR(); - console.log(fmt, test.title); + output.log(fmt, test.title); } else { var fmt = indent() + color('checkmark', ' ' + Base.symbols.ok) + color('pass', ' %s ') + color(test.speed, '(%dms)'); cursor.CR(); - console.log(fmt, test.title, test.duration); + output.log(fmt, test.title, test.duration); } }); runner.on('fail', function(test, err){ cursor.CR(); - console.log(indent() + color('fail', ' %d) %s'), ++n, test.title); + output.log(indent() + color('fail', ' %d) %s'), ++n, test.title); }); runner.on('end', self.epilogue.bind(self)); diff --git a/lib/reporters/tap.js b/lib/reporters/tap.js index 2bcd995baa..b2043a24ff 100644 --- a/lib/reporters/tap.js +++ b/lib/reporters/tap.js @@ -4,6 +4,7 @@ */ var Base = require('./base') + , output = Base.output , cursor = Base.cursor , color = Base.color; @@ -20,8 +21,8 @@ exports = module.exports = TAP; * @api public */ -function TAP(runner) { - Base.call(this, runner); +function TAP(runner, ostream) { + Base.call(this, runner, ostream); var self = this , stats = this.stats @@ -31,7 +32,7 @@ function TAP(runner) { runner.on('start', function(){ var total = runner.grepTotal(runner.suite); - console.log('%d..%d', 1, total); + output.log('%d..%d', 1, total); }); runner.on('test end', function(){ @@ -39,24 +40,24 @@ function TAP(runner) { }); runner.on('pending', function(test){ - console.log('ok %d %s # SKIP -', n, title(test)); + output.log('ok %d %s # SKIP -', n, title(test)); }); runner.on('pass', function(test){ passes++; - console.log('ok %d %s', n, title(test)); + output.log('ok %d %s', n, title(test)); }); runner.on('fail', function(test, err){ failures++; - console.log('not ok %d %s', n, title(test)); - if (err.stack) console.log(err.stack.replace(/^/gm, ' ')); + output.log('not ok %d %s', n, title(test)); + if (err.stack) output.log(err.stack.replace(/^/gm, ' ')); }); runner.on('end', function(){ - console.log('# tests ' + (passes + failures)); - console.log('# pass ' + passes); - console.log('# fail ' + failures); + output.log('# tests ' + (passes + failures)); + output.log('# pass ' + passes); + output.log('# fail ' + failures); }); } diff --git a/lib/reporters/teamcity.js b/lib/reporters/teamcity.js index 032eea7d2b..1572e3efa5 100644 --- a/lib/reporters/teamcity.js +++ b/lib/reporters/teamcity.js @@ -3,7 +3,8 @@ * Module dependencies. */ -var Base = require('./base'); +var Base = require('./base') + , output = Base.output; /** * Expose `Teamcity`. @@ -18,32 +19,32 @@ exports = module.exports = Teamcity; * @api public */ -function Teamcity(runner) { - Base.call(this, runner); +function Teamcity(runner, ostream) { + Base.call(this, runner, ostream); var stats = this.stats; runner.on('start', function() { - console.log("##teamcity[testSuiteStarted name='mocha.suite']"); + output.log("##teamcity[testSuiteStarted name='mocha.suite']"); }); runner.on('test', function(test) { - console.log("##teamcity[testStarted name='" + escape(test.fullTitle()) + "']"); + output.log("##teamcity[testStarted name='" + escape(test.fullTitle()) + "']"); }); runner.on('fail', function(test, err) { - console.log("##teamcity[testFailed name='" + escape(test.fullTitle()) + "' message='" + escape(err.message) + "']"); + output.log("##teamcity[testFailed name='" + escape(test.fullTitle()) + "' message='" + escape(err.message) + "']"); }); runner.on('pending', function(test) { - console.log("##teamcity[testIgnored name='" + escape(test.fullTitle()) + "' message='pending']"); + output.log("##teamcity[testIgnored name='" + escape(test.fullTitle()) + "' message='pending']"); }); runner.on('test end', function(test) { - console.log("##teamcity[testFinished name='" + escape(test.fullTitle()) + "' duration='" + test.duration + "']"); + output.log("##teamcity[testFinished name='" + escape(test.fullTitle()) + "' duration='" + test.duration + "']"); }); runner.on('end', function() { - console.log("##teamcity[testSuiteFinished name='mocha.suite' duration='" + stats.duration + "']"); + output.log("##teamcity[testSuiteFinished name='mocha.suite' duration='" + stats.duration + "']"); }); } diff --git a/lib/reporters/xunit.js b/lib/reporters/xunit.js index 702aa3cee2..6871e03e09 100644 --- a/lib/reporters/xunit.js +++ b/lib/reporters/xunit.js @@ -4,6 +4,7 @@ */ var Base = require('./base') + , output = Base.output , utils = require('../utils') , escape = utils.escape; @@ -30,8 +31,8 @@ exports = module.exports = XUnit; * @api public */ -function XUnit(runner) { - Base.call(this, runner); +function XUnit(runner, ostream) { + Base.call(this, runner, ostream); var stats = this.stats , tests = [] , self = this; @@ -45,7 +46,7 @@ function XUnit(runner) { }); runner.on('end', function(){ - console.log(tag('testsuite', { + output.log(tag('testsuite', { name: 'Mocha Tests' , tests: stats.tests , failures: stats.failures @@ -56,7 +57,7 @@ function XUnit(runner) { }, false)); tests.forEach(test); - console.log(''); + output.log(''); }); } @@ -80,11 +81,11 @@ function test(test) { if ('failed' == test.state) { var err = test.err; attrs.message = escape(err.message); - console.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack)))); + output.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack)))); } else if (test.pending) { - console.log(tag('testcase', attrs, false, tag('skipped', {}, true))); + output.log(tag('testcase', attrs, false, tag('skipped', {}, true))); } else { - console.log(tag('testcase', attrs, true) ); + output.log(tag('testcase', attrs, true) ); } } diff --git a/mocha.js b/mocha.js index 2b35a88302..ee972135d6 100644 --- a/mocha.js +++ b/mocha.js @@ -672,8 +672,14 @@ exports.isatty = function(){ }; exports.getWindowSize = function(){ - return [window.innerHeight, window.innerWidth]; + if ('innerHeight' in global) { + return [global.innerHeight, global.innerWidth]; + } else { + // In a Web Worker, the DOM Window is not available. + return [640, 480]; + } }; + }); // module: browser/tty.js require.register("context.js", function(module, exports, require){ @@ -1225,6 +1231,17 @@ module.exports = function(suite){ return suite; }; + /** + * Pending suite. + */ + context.suite.skip = function(title, fn) { + var suite = Suite.create(suites[0], title); + suite.pending = true; + suites.unshift(suite); + fn.call(suite); + suites.shift(); + }; + /** * Exclusive test-case. */ @@ -1241,8 +1258,10 @@ module.exports = function(suite){ */ context.test = function(title, fn){ + var suite = suites[0]; + if (suite.pending) var fn = null; var test = new Test(title, fn); - suites[0].addTest(test); + suite.addTest(test); return test; }; @@ -1297,6 +1316,7 @@ exports.reporters = require('./reporters'); exports.Runnable = require('./runnable'); exports.Context = require('./context'); exports.Runner = require('./runner'); +exports.output = require('./output'); exports.Suite = require('./suite'); exports.Hook = require('./hook'); exports.Test = require('./test'); @@ -1340,6 +1360,7 @@ function Mocha(options) { this.ui(options.ui); this.bail(options.bail); this.reporter(options.reporter); + this.output(options.output); if (options.timeout) this.timeout(options.timeout); if (options.slow) this.slow(options.slow); } @@ -1391,6 +1412,19 @@ Mocha.prototype.reporter = function(reporter){ return this; }; +/** + * Set output to `output`, defaults to "-". + * + * @param {String} target file name + * @api public + */ + +Mocha.prototype.output = function(target) { + if (target !== undefined) { + exports.output.initialize(target); + } +}; + /** * Set test UI `name`, defaults to "bdd". * @@ -1582,7 +1616,10 @@ Mocha.prototype.run = function(fn){ if (options.grep) runner.grep(options.grep, options.invert); if (options.globals) runner.globals(options.globals); if (options.growl) this._growl(runner, reporter); - return runner.run(fn); + var self = this; + return runner.run(function(errCount) { + exports.output.end(errCount, fn); + }); }; }); // module: mocha.js @@ -1671,6 +1708,74 @@ function format(ms) { } }); // module: ms.js +require.register("output.js", function(module, exports, require){ +var debug = require('browser/debug')('mocha:output'); +var fs = require('browser/fs'); + +// Initialize to be in stdout mode +var stream = process.stdout; +var _console = console; + +/** + * Write preformatted output directly to the stream. + */ +var write = exports.write = function(data) { + stream.write(data); +}; + + +/** + * Wrapper for console.log output. + */ +var log = exports.log = function() { + _console.log.apply(_console, arguments); +}; + +/** + * Wrapper for console.error output. + */ +var error = exports.error = function() { + _console.error.apply(_console, arguments); +}; + +/** + * Hook that's called when the test process is done to close the + * output stream. + */ +var end = exports.end = function(errCount, fn) { + if (stream === process.stdout) { + // nothing to do + return fn(errCount); + } + + stream.end(function() { + debug("stream ended"); + fn(errCount); + + // Reset the stream + stream = process.stdout; + } ); +}; + +/** + * Initialize the output layer. + * + * If target is undefined or "-" then use stdout, otherwise open the + * target file. + */ +var initialize = exports.initialize = function(target) { + if (target === undefined || target === "-") { + return; // nothing to do - stay in stdout mode + } + + stream = fs.createWriteStream(target); + _console = new console.Console(stream); +}; + + + +}); // module: output.js + require.register("reporters/base.js", function(module, exports, require){ /** @@ -1679,7 +1784,8 @@ require.register("reporters/base.js", function(module, exports, require){ var tty = require('browser/tty') , diff = require('browser/diff') - , ms = require('../ms'); + , ms = require('../ms') + , output = require('../output'); /** * Save timer references to avoid Sinon interfering (see GH-237). @@ -1703,6 +1809,12 @@ var isatty = tty.isatty(1) && tty.isatty(2); exports = module.exports = Base; +/** + * Expose the output hooks + */ +exports.output = output; + + /** * Enable coloring by default. */ @@ -1782,6 +1894,7 @@ exports.window = { : 75 }; + /** * Expose some basic cursor interactions * that are common among reporters. @@ -1818,7 +1931,7 @@ exports.cursor = { */ exports.list = function(failures){ - console.error(); + output.error(); failures.forEach(function(test, i){ // format var fmt = color('error title', ' %s) %s:\n') @@ -1835,6 +1948,11 @@ exports.list = function(failures){ , expected = err.expected , escape = true; + // uncaught + if (err.uncaught) { + msg = 'Uncaught ' + msg; + } + // explicitly show diff if (err.showDiff) { escape = false; @@ -1875,7 +1993,7 @@ exports.list = function(failures){ stack = stack.slice(index ? index + 1 : index) .replace(/^/gm, ' '); - console.error(fmt, (i + 1), test.fullTitle(), msg, stack); + output.error(fmt, (i + 1), test.fullTitle(), msg, stack); }); }; @@ -1953,51 +2071,41 @@ function Base(runner) { */ Base.prototype.epilogue = function(){ - var stats = this.stats - , fmt - , tests; - - console.log(); - - function pluralize(n) { - return 1 == n ? 'test' : 'tests'; - } - - // failure - if (stats.failures) { - fmt = color('bright fail', ' ' + exports.symbols.err) - + color('fail', ' %d of %d %s failed') - + color('light', ':') + var stats = this.stats; + var tests; + var fmt; - console.error(fmt, - stats.failures, - this.runner.total, - pluralize(this.runner.total)); + output.log(); - Base.list(this.failures); - console.error(); - return; - } - - // pass + // passes fmt = color('bright pass', ' ') - + color('green', ' %d %s complete') + + color('green', ' %d passing') + color('light', ' (%s)'); - console.log(fmt, - stats.tests || 0, - pluralize(stats.tests), + output.log(fmt, + stats.passes || 0, ms(stats.duration)); // pending if (stats.pending) { fmt = color('pending', ' ') - + color('pending', ' %d %s pending'); + + color('pending', ' %d pending'); + + output.log(fmt, stats.pending); + } - console.log(fmt, stats.pending, pluralize(stats.pending)); + // failures + if (stats.failures) { + fmt = color('fail', ' %d failing'); + + output.error(fmt, + stats.failures); + + Base.list(this.failures); + output.error(); } - console.log(); + output.log(); }; /** @@ -2120,6 +2228,7 @@ require.register("reporters/dot.js", function(module, exports, require){ */ var Base = require('./base') + , output = Base.output , color = Base.color; /** @@ -2144,29 +2253,29 @@ function Dot(runner) { , n = 0; runner.on('start', function(){ - process.stdout.write('\n '); + output.write('\n '); }); runner.on('pending', function(test){ - process.stdout.write(color('pending', Base.symbols.dot)); + output.write(color('pending', Base.symbols.dot)); }); runner.on('pass', function(test){ - if (++n % width == 0) process.stdout.write('\n '); + if (++n % width == 0) output.write('\n '); if ('slow' == test.speed) { - process.stdout.write(color('bright yellow', Base.symbols.dot)); + output.write(color('bright yellow', Base.symbols.dot)); } else { - process.stdout.write(color(test.speed, Base.symbols.dot)); + output.write(color(test.speed, Base.symbols.dot)); } }); runner.on('fail', function(test, err){ - if (++n % width == 0) process.stdout.write('\n '); - process.stdout.write(color('fail', Base.symbols.dot)); + if (++n % width == 0) output.write('\n '); + output.write(color('fail', Base.symbols.dot)); }); runner.on('end', function(){ - console.log(); + output.writeln(); self.epilogue(); }); } @@ -2180,6 +2289,7 @@ F.prototype = Base.prototype; Dot.prototype = new F; Dot.prototype.constructor = Dot; + }); // module: reporters/dot.js require.register("reporters/html-cov.js", function(module, exports, require){ @@ -3729,6 +3839,7 @@ require.register("reporters/xunit.js", function(module, exports, require){ */ var Base = require('./base') + , output = Base.output , utils = require('../utils') , escape = utils.escape; @@ -3770,7 +3881,7 @@ function XUnit(runner) { }); runner.on('end', function(){ - console.log(tag('testsuite', { + output.writeln(tag('testsuite', { name: 'Mocha Tests' , tests: stats.tests , failures: stats.failures @@ -3781,7 +3892,7 @@ function XUnit(runner) { }, false)); tests.forEach(test); - console.log(''); + output.writeln(''); }); } @@ -3809,11 +3920,11 @@ function test(test) { if ('failed' == test.state) { var err = test.err; attrs.message = escape(err.message); - console.log(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack)))); + output.writeln(tag('testcase', attrs, false, tag('failure', attrs, false, cdata(err.stack)))); } else if (test.pending) { - console.log(tag('testcase', attrs, false, tag('skipped', {}, true))); + output.writeln(tag('testcase', attrs, false, tag('skipped', {}, true))); } else { - console.log(tag('testcase', attrs, true) ); + output.writeln(tag('testcase', attrs, true) ); } } @@ -3983,16 +4094,14 @@ Runnable.prototype.inspect = function(){ */ Runnable.prototype.resetTimeout = function(){ - var self = this - , ms = this.timeout(); + var self = this; + var ms = this.timeout() || 1e9; this.clearTimeout(); - if (ms) { - this.timer = setTimeout(function(){ - self.callback(new Error('timeout of ' + ms + 'ms exceeded')); - self.timedOut = true; - }, ms); - } + this.timer = setTimeout(function(){ + self.callback(new Error('timeout of ' + ms + 'ms exceeded')); + self.timedOut = true; + }, ms); }; /** @@ -4073,7 +4182,6 @@ Runnable.prototype.run = function(fn){ }); // module: runnable.js require.register("runner.js", function(module, exports, require){ - /** * Module dependencies. */ @@ -4119,6 +4227,7 @@ module.exports = Runner; * - `hook end` (hook) hook complete * - `pass` (test) test passed * - `fail` (test, err) test failed + * - `pending` (test) test pending * * @api public */ @@ -4311,6 +4420,7 @@ Runner.prototype.hook = function(name, fn){ function next(i) { var hook = hooks[i]; if (!hook) return fn(); + if (self.failures && suite.bail()) return fn(); self.currentRunnable = hook; self.emit('hook', hook); @@ -5252,16 +5362,18 @@ exports.highlightTags = function(name) { }; }); // module: utils.js +// The global object is "self" in Web Workers. +global = (function() { return this; })(); /** * Save timer references to avoid Sinon interfering (see GH-237). */ -var Date = window.Date; -var setTimeout = window.setTimeout; -var setInterval = window.setInterval; -var clearTimeout = window.clearTimeout; -var clearInterval = window.clearInterval; +var Date = global.Date; +var setTimeout = global.setTimeout; +var setInterval = global.setInterval; +var clearTimeout = global.clearTimeout; +var clearInterval = global.clearInterval; /** * Node shims. @@ -5275,7 +5387,6 @@ var clearInterval = window.clearInterval; var process = {}; process.exit = function(status){}; process.stdout = {}; -global = window; /** * Remove uncaughtException listener. @@ -5283,7 +5394,7 @@ global = window; process.removeListener = function(e){ if ('uncaughtException' == e) { - window.onerror = null; + global.onerror = function() {}; } }; @@ -5293,7 +5404,7 @@ process.removeListener = function(e){ process.on = function(e, fn){ if ('uncaughtException' == e) { - window.onerror = function(err, url, line){ + global.onerror = function(err, url, line){ fn(new Error(err + ' (' + url + ':' + line + ')')); }; } @@ -5303,8 +5414,8 @@ process.on = function(e, fn){ * Expose mocha. */ -var Mocha = window.Mocha = require('mocha'), - mocha = window.mocha = new Mocha({ reporter: 'html' }); +var Mocha = global.Mocha = require('mocha'), + mocha = global.mocha = new Mocha({ reporter: 'html' }); var immediateQueue = [] , immediateTimeout; @@ -5339,7 +5450,7 @@ Mocha.Runner.immediately = function(callback) { mocha.ui = function(ui){ Mocha.prototype.ui.call(this, ui); - this.suite.emit('pre-require', window, null, this); + this.suite.emit('pre-require', global, null, this); return this; }; @@ -5361,12 +5472,15 @@ mocha.run = function(fn){ var options = mocha.options; mocha.globals('location'); - var query = Mocha.utils.parseQuery(window.location.search || ''); + var query = Mocha.utils.parseQuery(global.location.search || ''); if (query.grep) mocha.grep(query.grep); if (query.invert) mocha.invert(); return Mocha.prototype.run.call(mocha, function(){ - Mocha.utils.highlightTags('code'); + // The DOM Document is not available in Web Workers. + if (global.document) { + Mocha.utils.highlightTags('code'); + } if (fn) fn(); }); };