From 8416292fdfea06204b708bbbac79cf8e3bf38d48 Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Sat, 25 Apr 2015 14:27:52 -0700 Subject: [PATCH 01/10] add webstorm metadata folder to gitignore --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index da23d0d..5fc2685 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,7 @@ build/Release # Deployed apps should consider commenting this line out: # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git node_modules + + +# Webstorm IDE metadata directory +.idea \ No newline at end of file From ca79d4eac0f94c3a897cae10b1f62404edd0c420 Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Sat, 25 Apr 2015 14:58:55 -0700 Subject: [PATCH 02/10] adding a mochaOptsStream export from lib/index to experiment with "parallel by options" --- gulpfile.js | 62 +++++++++++++++++++++++++------------------- lib/index.js | 32 +++++++++++++++++++++-- test/b-test-specs.js | 11 ++++++++ test/c-test-specs.js | 11 ++++++++ 4 files changed, 88 insertions(+), 28 deletions(-) create mode 100644 test/b-test-specs.js create mode 100644 test/c-test-specs.js diff --git a/gulpfile.js b/gulpfile.js index 4d7e181..48db891 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1,30 +1,32 @@ "use strict"; var gulp = require('gulp'), - mochaStream = require('./lib').mochaStream, - SpawnMocha = require('./lib').SpawnMocha, - _ = require('lodash'), - through = require('through'), - Q = require('q'), - runSequence = Q.denodeify(require('run-sequence')), - assert = require('assert'), - File = require('vinyl'), - from = require('from'); + mochaStream = require('./lib').mochaStream, + mochaOptsStream = require('./lib').mochaOptsStream, + SpawnMocha = require('./lib').SpawnMocha, + _ = require('lodash'), + through = require('through'), + Q = require('q'), + runSequence = Q.denodeify(require('run-sequence')), + assert = require('assert'), + File = require('vinyl'), + from = require('from'); function customMocha(opts) { opts = opts || {}; var spawnMocha = new SpawnMocha(opts); var stream = through(function write(file) { spawnMocha.add(file.path); - }, function() {}); + }, function () { + }); var errors = []; - spawnMocha.on('error', function(err) { + spawnMocha.on('error', function (err) { console.error(err.toString()); errors.push(err); - }).on('end', function() { - if(errors.length > 0) { + }).on('end', function () { + if (errors.length > 0) { console.error('ERROR SUMMARY: '); - _(errors).each(function(err) { + _(errors).each(function (err) { console.error(err); console.error(err.stack); }).value(); @@ -35,18 +37,19 @@ function customMocha(opts) { return stream; } -gulp.task('test-mocha', function() { +gulp.task('test-mocha', function () { var startMS = Date.now(); var StdOutFixture = require('fixture-stdout'); var fixture = new StdOutFixture(); var output = ''; - fixture.capture( function onWrite (string ) { + fixture.capture(function onWrite(string) { output += string; + return false; }); var mocha = mochaStream({concurrency: 10}); var srcFiles = []; - _(10).times(function() { + _(10).times(function () { srcFiles.push(new File({ cwd: "/", base: "test/", @@ -56,21 +59,22 @@ gulp.task('test-mocha', function() { return from(srcFiles) .pipe(mocha) .on('error', console.error) - .on('end', function() { + .on('end', function () { fixture.release(); + console.log(output); // we should have run 10 tests in parallel in less than 10 sec assert(output.match(/1 passing/g).length === 10); - assert( Date.now() - startMS < 5000); + assert(Date.now() - startMS < 5000); }); }); -gulp.task('test-custom-mocha', function() { +gulp.task('test-custom-mocha', function () { return gulp.src('test/*-specs.js', {read: false}) .pipe(customMocha()) .on('error', console.error); }); -gulp.task('test-live-output', function() { +gulp.task('test-live-output', function () { var mocha = mochaStream({liveOutput: true, concurrency: 1}); var srcFiles = []; srcFiles.push(new File({ @@ -82,7 +86,13 @@ gulp.task('test-live-output', function() { .pipe(mocha); }); -gulp.task('test-live-output-with-file', function() { +gulp.task('test-mocha-opts-parallel', function () { + var mocha = mochaOptsStream({liveOutput: true, concurrency: 1}); + gulp.src('test/*-specs.js') + .pipe(mocha); +}); + +gulp.task('test-live-output-with-file', function () { var mocha = mochaStream({ liveOutput: true, fileOutput: '/tmp/out.log', @@ -97,7 +107,7 @@ gulp.task('test-live-output-with-file', function() { return from(srcFiles).pipe(mocha); }); -gulp.task('test-with-file', function() { +gulp.task('test-with-file', function () { var mocha = mochaStream({ fileOutput: '/tmp/out.log', concurrency: 1 @@ -112,12 +122,12 @@ gulp.task('test-with-file', function() { }); -gulp.task('test-live-output-with-prepend', function() { +gulp.task('test-live-output-with-prepend', function () { var mocha = mochaStream({ liveOutput: true, liveOutputPrepend: 'client --> ', concurrency: 1, - flags: { R: "tap" } + flags: {R: "tap"} }); var srcFiles = []; srcFiles.push(new File({ @@ -130,7 +140,7 @@ gulp.task('test-live-output-with-prepend', function() { }); -gulp.task('test', function() { +gulp.task('test', function () { return runSequence( 'test-mocha', 'test-custom-mocha', diff --git a/lib/index.js b/lib/index.js index cfca208..7441878 100644 --- a/lib/index.js +++ b/lib/index.js @@ -54,7 +54,7 @@ var SpawnMocha = function (opts) { if(env.JUNIT_REPORT_PATH) { env.JUNIT_REPORT_PATH = env.JUNIT_REPORT_PATH + '.' + task.taskNum; } - + console.log('haoooooo', task.files); // Execute Mocha var child = proc.spawn(bin, args.concat(task.files), {env: env}); @@ -150,7 +150,35 @@ var mochaStream = function mocha(opts) { return stream; }; +var mochaOptsStream = function mocha(opts) { + var files = []; + opts = opts || {}; + var spawnMocha = new SpawnMocha(opts); + var stream = through(function write(file) { + //spawnMocha.add(file.path); + files.push(file.path); + }, function() { + spawnMocha.add(files); + }); + var errors = []; + spawnMocha.on('error', function(err) { + console.error(err.toString()); + errors.push(err); + }).on('end', function() { + if(errors.length > 0) { + console.error('ERROR SUMMARY: '); + _(errors).each(function(err) { + console.error(err); + console.error(err.stack); + }).value(); + stream.emit('error', "Some tests failed."); + } + stream.emit('end'); + }); + return stream; +}; module.exports = { SpawnMocha: SpawnMocha, - mochaStream: mochaStream + mochaStream: mochaStream, + mochaOptsStream: mochaOptsStream }; diff --git a/test/b-test-specs.js b/test/b-test-specs.js new file mode 100644 index 0000000..34067fa --- /dev/null +++ b/test/b-test-specs.js @@ -0,0 +1,11 @@ +"use strict"; +/* global describe, it */ + +describe('b test', function() { + this.timeout(3000); + it('should work', function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); diff --git a/test/c-test-specs.js b/test/c-test-specs.js new file mode 100644 index 0000000..8693bce --- /dev/null +++ b/test/c-test-specs.js @@ -0,0 +1,11 @@ +"use strict"; +/* global describe, it */ + +describe('c test', function() { + this.timeout(3000); + it('should work', function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); From 17a5c8db87a5e9459b52dbcd14379f6505589950 Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Sat, 25 Apr 2015 23:12:33 -0700 Subject: [PATCH 03/10] adding iterations array support, modifying JUNIT_REPORT_PATH feature to insert taskNum before .xml, adding docs --- .gitignore | 6 +- README.md | 47 ++++++++++++++ gulpfile.js | 37 ++++++++--- lib/index.js | 125 ++++++++++++++++-------------------- package.json | 1 + test/b-test-specs.js | 11 ---- test/c-test-specs.js | 11 ---- test/group/a-group-specs.js | 27 ++++++++ test/group/b-group-specs.js | 27 ++++++++ test/group/c-group-specs.js | 27 ++++++++ test/report/reports.md | 2 + 11 files changed, 220 insertions(+), 101 deletions(-) delete mode 100644 test/b-test-specs.js delete mode 100644 test/c-test-specs.js create mode 100644 test/group/a-group-specs.js create mode 100644 test/group/b-group-specs.js create mode 100644 test/group/c-group-specs.js create mode 100644 test/report/reports.md diff --git a/.gitignore b/.gitignore index 5fc2685..f96bd9c 100644 --- a/.gitignore +++ b/.gitignore @@ -26,4 +26,8 @@ node_modules # Webstorm IDE metadata directory -.idea \ No newline at end of file +.idea + +# test report files +test/report/*.xml* +test/report/*.log \ No newline at end of file diff --git a/README.md b/README.md index 090fc0b..ba777ce 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,53 @@ gulp.task('test-custom-mocha', function() { - flags: mocha flags (default: none) - liveOutput: print output direct to console - errorSummary: show error summary (default: true) +- iterations: Array, see below + +### Iterations + +If you want to run parallel processes based on criteria other than files, use an iterations array. + +The iterations array should contain whatever "Options" (see above) you wish to override for a given iteration. + +The easiest to envision example would be: +* your test suite names are annotated as: @groupA@, @groupB@, @groupC@, and so on +* these groupings are spread across files, where a file may contain one or more suite of a given group +* you want to spawn a mocha process for each grouping + +To achieve, you would specify an iterations array where each entry uses a different mocha grep. E.g. the task +example "test-mocha-opts-parallel" in this project's `gulpfile.js`. + +```javascript +gulp.task('test-mocha-opts-parallel', function () { + function setEnv(envs) { + var env = process.env; + env = _.clone(env); + env = _.merge(env, envs, {JUNIT_REPORT_PATH: path.resolve(__dirname, 'test/report/report.xml')}); + return env; + } + var opts = { + concurrency: 3, + flags: {R: 'mocha-jenkins-reporter'}, + iterations: [{ + env: setEnv({NODE_ENV: 'groupa'}), + flags: {grep: "@groupA@"} + }, { + env: setEnv({NODE_ENV: 'groupb'}), + flags: {grep: "@groupB@"} + }, { + env: setEnv({NODE_ENV: 'groupc'}), + flags: {grep: "@groupC@"} + }] + }; + var mocha = mochaStream(opts); + gulp.src('test/group/*-specs.js') + .pipe(mocha); +}); +``` + +Another use case might be if you are driving browser/device automation, and you want to run the same set of files +in parallel on several browser/device combinations. + ## Todo - concatenate mocha status at the end diff --git a/gulpfile.js b/gulpfile.js index 48db891..5fe4172 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,7 +2,6 @@ var gulp = require('gulp'), mochaStream = require('./lib').mochaStream, - mochaOptsStream = require('./lib').mochaOptsStream, SpawnMocha = require('./lib').SpawnMocha, _ = require('lodash'), through = require('through'), @@ -10,6 +9,7 @@ var gulp = require('gulp'), runSequence = Q.denodeify(require('run-sequence')), assert = require('assert'), File = require('vinyl'), + path = require('path'), from = require('from'); function customMocha(opts) { @@ -86,12 +86,6 @@ gulp.task('test-live-output', function () { .pipe(mocha); }); -gulp.task('test-mocha-opts-parallel', function () { - var mocha = mochaOptsStream({liveOutput: true, concurrency: 1}); - gulp.src('test/*-specs.js') - .pipe(mocha); -}); - gulp.task('test-live-output-with-file', function () { var mocha = mochaStream({ liveOutput: true, @@ -139,6 +133,32 @@ gulp.task('test-live-output-with-prepend', function () { .pipe(mocha); }); +gulp.task('test-mocha-opts-parallel', function () { + function setEnv(envs) { + var env = process.env; + env = _.clone(env); + env = _.merge(env, envs, {JUNIT_REPORT_PATH: path.resolve(__dirname, 'test/report/report.xml')}); + return env; + } + + var opts = { + concurrency: 3, + flags: {R: 'mocha-jenkins-reporter'}, + iterations: [{ + env: setEnv({NODE_ENV: 'groupa'}), + flags: {grep: "@groupA@"} + }, { + env: setEnv({NODE_ENV: 'groupb'}), + flags: {grep: "@groupB@"} + }, { + env: setEnv({NODE_ENV: 'groupc'}), + flags: {grep: "@groupC@"} + }] + }; + var mocha = mochaStream(opts); + gulp.src('test/group/*-specs.js') + .pipe(mocha); +}); gulp.task('test', function () { return runSequence( @@ -147,6 +167,7 @@ gulp.task('test', function () { 'test-live-output', 'test-live-output-with-prepend', 'test-live-output-with-file', - 'test-with-file' + 'test-with-file', + 'test-mocha-opts-parallel' ); }); diff --git a/lib/index.js b/lib/index.js index 7441878..ea16592 100644 --- a/lib/index.js +++ b/lib/index.js @@ -2,22 +2,22 @@ // Module Requirements var _ = require('lodash'), - proc = require('child_process'), - join = require('path').join, - async = require('async'), - util = require('util'), - EventEmitter = require('events').EventEmitter, - streamBuffers = require("stream-buffers"), - through = require('through'), - split = require('split'), - fs = require('fs'); + proc = require('child_process'), + join = require('path').join, + async = require('async'), + util = require('util'), + EventEmitter = require('events').EventEmitter, + streamBuffers = require("stream-buffers"), + through = require('through'), + split = require('split'), + fs = require('fs'); require('colors'); function newStreamBuffer() { var stream = new streamBuffers.WritableStreamBuffer({ - initialSize: (25 * 1024), - incrementAmount: (10 * 1024) + initialSize: (25 * 1024), + incrementAmount: (10 * 1024) }); return stream; } @@ -30,15 +30,20 @@ var SpawnMocha = function (opts) { }); var queue = async.queue(function (task, done) { // Setup + var _opts = opts; + //merge iterationOpts, if any + if (task.iterationOpts) { + _opts = _.merge(opts, task.iterationOpts); + } var bin = _.isFunction(opts.bin) ? opts.bin() : opts.bin || - join(__dirname, '..', 'node_modules', '.bin', 'mocha'); - var env = _.isFunction(opts.env) ? opts.env() : opts.env || process.env; + join(__dirname, '..', 'node_modules', '.bin', 'mocha'); + var env = _.isFunction(_opts.env) ? _opts.env() : _opts.env || process.env; env = _.clone(env); // Generate arguments var args = []; - _(opts.flags).each(function (val, key) { - if(_.isFunction(val)) val = val(); + _(_opts.flags).each(function (val, key) { + if (_.isFunction(val)) val = val(); args.push((key.length > 1 ? '--' : '-') + key); if (_.isString(val) || _.isNumber(val)) { args.push(val); @@ -47,27 +52,26 @@ var SpawnMocha = function (opts) { var stdout = newStreamBuffer(); var stderr = newStreamBuffer(); - var fsStream = opts.fileOutput ? - fs.createWriteStream(opts.fileOutput, { flags: 'a', encoding: 'utf8' }) : null; + var fsStream = _opts.fileOutput ? + fs.createWriteStream(_opts.fileOutput, {flags: 'a', encoding: 'utf8'}) : null; // Split xunit test report in several files if required - if(env.JUNIT_REPORT_PATH) { - env.JUNIT_REPORT_PATH = env.JUNIT_REPORT_PATH + '.' + task.taskNum; + if (env.JUNIT_REPORT_PATH) { + env.JUNIT_REPORT_PATH = env.JUNIT_REPORT_PATH.replace(/xml$/, task.taskNum + '.xml'); } - console.log('haoooooo', task.files); // Execute Mocha var child = proc.spawn(bin, args.concat(task.files), {env: env}); if (opts.liveOutput) { child.stdout.pipe(split()) .on('data', function (line) { - console.log((opts.liveOutputPrepend || '') + line); + console.log((_opts.liveOutputPrepend || '') + line); }); child.stderr.pipe(split()) .on('data', function (line) { - console.error((opts.liveOutputPrepend || '') + line); + console.error((_opts.liveOutputPrepend || '') + line); }); - if(fsStream) { + if (fsStream) { child.stdout.pipe(fsStream); child.stderr.pipe(fsStream); } @@ -77,25 +81,27 @@ var SpawnMocha = function (opts) { } // When done... - child.on('close', function(errCode) { - if(stdout.size()) { + child.on('close', function (errCode) { + if (stdout.size()) { var contentOut = stdout.getContentsAsString("utf8"); console.log(contentOut); - if(fsStream) { + if (fsStream) { fsStream.write(contentOut + '\n', 'utf8'); } } - if(stderr.size()) { + if (stderr.size()) { var contentErr = stdout.getContentsAsString("utf8"); console.error(contentErr); - if(fsStream) { + if (fsStream) { fsStream.write(contentErr + '\n', 'utf8'); } } - if(fsStream) { fsStream.close(); } + if (fsStream) { + fsStream.close(); + } var err = null; - if(errCode && opts.errorSummary) { + if (errCode && opts.errorSummary) { err = new Error('Error for files: ' + task.files.join(', ')); err.files = task.files; err.stderr = stderr.size() ? stderr.getContentsAsString("utf8") : ''; @@ -105,19 +111,19 @@ var SpawnMocha = function (opts) { }); }, opts.concurrency || 1); - queue.drain = function() { + queue.drain = function () { _this.emit('end'); }; var taskNum = 0; - this.add = function(files) { - taskNum ++; + this.add = function (files, iterationOpts) { + taskNum++; if (!_.isArray(files)) { files = [files]; } - var task = {taskNum: taskNum, files: files}; - queue.push(task, function(err) { - if(err){ + var task = {taskNum: taskNum, files: files, iterationOpts: iterationOpts || {}}; + queue.push(task, function (err) { + if (err) { _this.emit('error', err, files); } }); @@ -128,46 +134,25 @@ util.inherits(SpawnMocha, EventEmitter); var mochaStream = function mocha(opts) { opts = opts || {}; - var spawnMocha = new SpawnMocha(opts); - var stream = through(function write(file) { - spawnMocha.add(file.path); - }, function() {}); - var errors = []; - spawnMocha.on('error', function(err) { - console.error(err.toString()); - errors.push(err); - }).on('end', function() { - if(errors.length > 0) { - console.error('ERROR SUMMARY: '); - _(errors).each(function(err) { - console.error(err); - console.error(err.stack); - }).value(); - stream.emit('error', "Some tests failed."); - } - stream.emit('end'); - }); - return stream; -}; - -var mochaOptsStream = function mocha(opts) { var files = []; - opts = opts || {}; var spawnMocha = new SpawnMocha(opts); var stream = through(function write(file) { - //spawnMocha.add(file.path); - files.push(file.path); - }, function() { - spawnMocha.add(files); + (opts.iterations) ? files.push(file.path) : spawnMocha.add(file.path); + }, function () { + if (opts.iterations) { + opts.iterations.forEach(function (iteration) { + spawnMocha.add(files, iteration); + }); + } }); var errors = []; - spawnMocha.on('error', function(err) { + spawnMocha.on('error', function (err) { console.error(err.toString()); errors.push(err); - }).on('end', function() { - if(errors.length > 0) { + }).on('end', function () { + if (errors.length > 0) { console.error('ERROR SUMMARY: '); - _(errors).each(function(err) { + _(errors).each(function (err) { console.error(err); console.error(err.stack); }).value(); @@ -177,8 +162,8 @@ var mochaOptsStream = function mocha(opts) { }); return stream; }; + module.exports = { SpawnMocha: SpawnMocha, - mochaStream: mochaStream, - mochaOptsStream: mochaOptsStream + mochaStream: mochaStream }; diff --git a/package.json b/package.json index 7716d77..8855529 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "fixture-stdout": "^0.2.1", "from": "^0.1.3", "gulp": "^3.8.8", + "mocha-jenkins-reporter": "^0.1.8", "q": "^1.1.2", "run-sequence": "^1.0.1", "vinyl": "^0.4.3" diff --git a/test/b-test-specs.js b/test/b-test-specs.js deleted file mode 100644 index 34067fa..0000000 --- a/test/b-test-specs.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; -/* global describe, it */ - -describe('b test', function() { - this.timeout(3000); - it('should work', function(done) { - setTimeout(function() { - done(); - }, 2000); - }); -}); diff --git a/test/c-test-specs.js b/test/c-test-specs.js deleted file mode 100644 index 8693bce..0000000 --- a/test/c-test-specs.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; -/* global describe, it */ - -describe('c test', function() { - this.timeout(3000); - it('should work', function(done) { - setTimeout(function() { - done(); - }, 2000); - }); -}); diff --git a/test/group/a-group-specs.js b/test/group/a-group-specs.js new file mode 100644 index 0000000..d51495a --- /dev/null +++ b/test/group/a-group-specs.js @@ -0,0 +1,27 @@ +"use strict"; +/* global describe, it */ + +describe('@suiteA@groupA@', function() { + this.timeout(3000); + it('should work, NODE_ENV: ' + process.env.NODE_ENV, function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); +describe('@suiteA@groupB@', function() { + this.timeout(3000); + it('should work, NODE_ENV: ' + process.env.NODE_ENV, function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); +describe('@suiteA@groupC@', function() { + this.timeout(3000); + it('should work, NODE_ENV: ' + process.env.NODE_ENV, function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); \ No newline at end of file diff --git a/test/group/b-group-specs.js b/test/group/b-group-specs.js new file mode 100644 index 0000000..f36a6d6 --- /dev/null +++ b/test/group/b-group-specs.js @@ -0,0 +1,27 @@ +"use strict"; +/* global describe, it */ + +describe('@suiteB@groupA@', function() { + this.timeout(3000); + it('should work', function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); +describe('@suiteB@groupB@', function() { + this.timeout(3000); + it('should work', function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); +describe('@suiteB@groupC@', function() { + this.timeout(3000); + it('should work', function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); \ No newline at end of file diff --git a/test/group/c-group-specs.js b/test/group/c-group-specs.js new file mode 100644 index 0000000..1db2525 --- /dev/null +++ b/test/group/c-group-specs.js @@ -0,0 +1,27 @@ +"use strict"; +/* global describe, it */ + +describe('@suiteC@groupA@', function() { + this.timeout(3000); + it('should work', function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); +describe('@suiteC@groupB@', function() { + this.timeout(3000); + it('should work', function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); +describe('@suiteC@groupC@', function() { + this.timeout(3000); + it('should work', function(done) { + setTimeout(function() { + done(); + }, 2000); + }); +}); \ No newline at end of file diff --git a/test/report/reports.md b/test/report/reports.md new file mode 100644 index 0000000..ab465b4 --- /dev/null +++ b/test/report/reports.md @@ -0,0 +1,2 @@ +# reports +This directory for .log, .xml, etc reports \ No newline at end of file From f21ee63aff1a8c3be4d229cba50bf2d89ca20e30 Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Sun, 26 Apr 2015 14:35:05 -0700 Subject: [PATCH 04/10] add _.clone to base opts object --- lib/index.js | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/index.js b/lib/index.js index ea16592..89e130b 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,19 +30,15 @@ var SpawnMocha = function (opts) { }); var queue = async.queue(function (task, done) { // Setup - var _opts = opts; - //merge iterationOpts, if any - if (task.iterationOpts) { - _opts = _.merge(opts, task.iterationOpts); - } + var iterationOpts = _.merge(_.clone(opts), task.iterationOpts); var bin = _.isFunction(opts.bin) ? opts.bin() : opts.bin || join(__dirname, '..', 'node_modules', '.bin', 'mocha'); - var env = _.isFunction(_opts.env) ? _opts.env() : _opts.env || process.env; + var env = _.isFunction(iterationOpts.env) ? iterationOpts.env() : iterationOpts.env || process.env; env = _.clone(env); // Generate arguments var args = []; - _(_opts.flags).each(function (val, key) { + _(iterationOpts.flags).each(function (val, key) { if (_.isFunction(val)) val = val(); args.push((key.length > 1 ? '--' : '-') + key); if (_.isString(val) || _.isNumber(val)) { @@ -52,8 +48,8 @@ var SpawnMocha = function (opts) { var stdout = newStreamBuffer(); var stderr = newStreamBuffer(); - var fsStream = _opts.fileOutput ? - fs.createWriteStream(_opts.fileOutput, {flags: 'a', encoding: 'utf8'}) : null; + var fsStream = iterationOpts.fileOutput ? + fs.createWriteStream(iterationOpts.fileOutput, {flags: 'a', encoding: 'utf8'}) : null; // Split xunit test report in several files if required if (env.JUNIT_REPORT_PATH) { @@ -65,11 +61,11 @@ var SpawnMocha = function (opts) { if (opts.liveOutput) { child.stdout.pipe(split()) .on('data', function (line) { - console.log((_opts.liveOutputPrepend || '') + line); + console.log((iterationOpts.liveOutputPrepend || '') + line); }); child.stderr.pipe(split()) .on('data', function (line) { - console.error((_opts.liveOutputPrepend || '') + line); + console.error((iterationOpts.liveOutputPrepend || '') + line); }); if (fsStream) { child.stdout.pipe(fsStream); From 8a843211e8761ebd0197baaca0131c59fbc0c982 Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Sun, 26 Apr 2015 23:09:23 -0700 Subject: [PATCH 05/10] refactor to new exported method "mochaIteration" to separate streaming interface from async interface --- README.md | 78 +++++++++++++++++++++------------------------------- gulpfile.js | 18 ++++++++---- lib/index.js | 36 +++++++++++++++++------- package.json | 1 + 4 files changed, 72 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index ba777ce..2f3c962 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ then. ## Usage -Using built in mochaStream: +### Using built in mochaStream: ```js var mochaStream = require('spawn-mocha-parallel').mochaStream; @@ -31,7 +31,37 @@ gulp.task('test-mocha', function() { ``` -Using SpawnMocha +### Using built in mochaIteration: + +mochaIteration allows you to spawn parallel processes which all run over the same fileset, but differ by whatever +options are added (or overridden) in each iterations Array element. + +```js +var mochaIteration = require('spawn-mocha-parallel').mochaIteration; +var glob = require('glob'); + +gulp.task('test-mocha-iteration', function (cb) { + var opts = { + concurrency: 3, + flags: {R: 'mocha-jenkins-reporter'}, + iterations: [{ + flags: {grep: "@groupA@"} + }, { + flags: {grep: "@groupB@"} + }, { + flags: {grep: "@groupC@"} + }] + }; + glob('test/group/*-specs.js', function (err, files) { + if (err) { + return cb(err); + } + mochaIteration(opts, files, cb); + }); +}); +``` + +### Using SpawnMocha ```js var SpawnMocha = require('spawn-mocha-parallel').SpawnMocha, @@ -82,50 +112,6 @@ gulp.task('test-custom-mocha', function() { - errorSummary: show error summary (default: true) - iterations: Array, see below -### Iterations - -If you want to run parallel processes based on criteria other than files, use an iterations array. - -The iterations array should contain whatever "Options" (see above) you wish to override for a given iteration. - -The easiest to envision example would be: -* your test suite names are annotated as: @groupA@, @groupB@, @groupC@, and so on -* these groupings are spread across files, where a file may contain one or more suite of a given group -* you want to spawn a mocha process for each grouping - -To achieve, you would specify an iterations array where each entry uses a different mocha grep. E.g. the task -example "test-mocha-opts-parallel" in this project's `gulpfile.js`. - -```javascript -gulp.task('test-mocha-opts-parallel', function () { - function setEnv(envs) { - var env = process.env; - env = _.clone(env); - env = _.merge(env, envs, {JUNIT_REPORT_PATH: path.resolve(__dirname, 'test/report/report.xml')}); - return env; - } - var opts = { - concurrency: 3, - flags: {R: 'mocha-jenkins-reporter'}, - iterations: [{ - env: setEnv({NODE_ENV: 'groupa'}), - flags: {grep: "@groupA@"} - }, { - env: setEnv({NODE_ENV: 'groupb'}), - flags: {grep: "@groupB@"} - }, { - env: setEnv({NODE_ENV: 'groupc'}), - flags: {grep: "@groupC@"} - }] - }; - var mocha = mochaStream(opts); - gulp.src('test/group/*-specs.js') - .pipe(mocha); -}); -``` - -Another use case might be if you are driving browser/device automation, and you want to run the same set of files -in parallel on several browser/device combinations. ## Todo diff --git a/gulpfile.js b/gulpfile.js index 5fe4172..46e35be 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,6 +2,7 @@ var gulp = require('gulp'), mochaStream = require('./lib').mochaStream, + mochaIteration = require('./lib').mochaIteration, SpawnMocha = require('./lib').SpawnMocha, _ = require('lodash'), through = require('through'), @@ -10,7 +11,8 @@ var gulp = require('gulp'), assert = require('assert'), File = require('vinyl'), path = require('path'), - from = require('from'); + from = require('from'), + glob = require('glob'); function customMocha(opts) { opts = opts || {}; @@ -133,7 +135,9 @@ gulp.task('test-live-output-with-prepend', function () { .pipe(mocha); }); -gulp.task('test-mocha-opts-parallel', function () { +gulp.task('test-mocha-opts-parallel', function (cb) { + + function setEnv(envs) { var env = process.env; env = _.clone(env); @@ -155,9 +159,13 @@ gulp.task('test-mocha-opts-parallel', function () { flags: {grep: "@groupC@"} }] }; - var mocha = mochaStream(opts); - gulp.src('test/group/*-specs.js') - .pipe(mocha); + glob('test/group/*-specs.js', function (err, files) { + if (err) { + return cb(err); + } + console.log('files', files); + mochaIteration(opts, files, cb); + }); }); gulp.task('test', function () { diff --git a/lib/index.js b/lib/index.js index 89e130b..d922c74 100644 --- a/lib/index.js +++ b/lib/index.js @@ -130,17 +130,10 @@ util.inherits(SpawnMocha, EventEmitter); var mochaStream = function mocha(opts) { opts = opts || {}; - var files = []; var spawnMocha = new SpawnMocha(opts); var stream = through(function write(file) { - (opts.iterations) ? files.push(file.path) : spawnMocha.add(file.path); - }, function () { - if (opts.iterations) { - opts.iterations.forEach(function (iteration) { - spawnMocha.add(files, iteration); - }); - } - }); + spawnMocha.add(file.path); + }, function () {}); var errors = []; spawnMocha.on('error', function (err) { console.error(err.toString()); @@ -159,7 +152,30 @@ var mochaStream = function mocha(opts) { return stream; }; +var mochaIteration = function mochaIteration(opts, files, cb) { + opts = opts || {}; + var spawnMocha = new SpawnMocha(opts); + opts.iterations.forEach(function (iteration) { + spawnMocha.add(files, iteration); + }); + var errors = []; + spawnMocha.on('error', function (err) { + console.error(err.toString()); + errors.push(err); + }).on('end', function () { + if (errors.length > 0) { + console.error('ERROR SUMMARY: '); + _(errors).each(function (err) { + console.error(err); + console.error(err.stack); + }).value(); + return cb(new Error('some tests failed')); + } + cb(null); + }); +}; module.exports = { SpawnMocha: SpawnMocha, - mochaStream: mochaStream + mochaStream: mochaStream, + mochaIteration: mochaIteration }; diff --git a/package.json b/package.json index 8855529..f969368 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "devDependencies": { "fixture-stdout": "^0.2.1", "from": "^0.1.3", + "glob": "^5.0.5", "gulp": "^3.8.8", "mocha-jenkins-reporter": "^0.1.8", "q": "^1.1.2", From a8c55fdf3680c84960aabd7d5047b75b2d618fdc Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Sun, 26 Apr 2015 23:13:22 -0700 Subject: [PATCH 06/10] adding JUNIT_REPORT_PATH env variable to README example --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 2f3c962..de37dd4 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ gulp.task('test-mocha-iteration', function (cb) { flags: {grep: "@groupC@"} }] }; + process.env.JUNIT_REPORT_PATH = path.resolve(__dirname, 'test/report/report.xml'); glob('test/group/*-specs.js', function (err, files) { if (err) { return cb(err); From 5ba50b3d21d44ae7af478bd95a5b5fbd49fd97bc Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Mon, 27 Apr 2015 10:06:18 -0700 Subject: [PATCH 07/10] paring down PR to include an overrides ability to each spawned mocha process options --- README.md | 31 ------------------------------- gulpfile.js | 49 +++++++++++++++++++++++++++++++++++-------------- lib/index.js | 43 ++++++++++--------------------------------- 3 files changed, 45 insertions(+), 78 deletions(-) diff --git a/README.md b/README.md index de37dd4..267eb0e 100644 --- a/README.md +++ b/README.md @@ -31,36 +31,6 @@ gulp.task('test-mocha', function() { ``` -### Using built in mochaIteration: - -mochaIteration allows you to spawn parallel processes which all run over the same fileset, but differ by whatever -options are added (or overridden) in each iterations Array element. - -```js -var mochaIteration = require('spawn-mocha-parallel').mochaIteration; -var glob = require('glob'); - -gulp.task('test-mocha-iteration', function (cb) { - var opts = { - concurrency: 3, - flags: {R: 'mocha-jenkins-reporter'}, - iterations: [{ - flags: {grep: "@groupA@"} - }, { - flags: {grep: "@groupB@"} - }, { - flags: {grep: "@groupC@"} - }] - }; - process.env.JUNIT_REPORT_PATH = path.resolve(__dirname, 'test/report/report.xml'); - glob('test/group/*-specs.js', function (err, files) { - if (err) { - return cb(err); - } - mochaIteration(opts, files, cb); - }); -}); -``` ### Using SpawnMocha @@ -111,7 +81,6 @@ gulp.task('test-custom-mocha', function() { - flags: mocha flags (default: none) - liveOutput: print output direct to console - errorSummary: show error summary (default: true) -- iterations: Array, see below ## Todo diff --git a/gulpfile.js b/gulpfile.js index 46e35be..9c05d39 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -2,7 +2,6 @@ var gulp = require('gulp'), mochaStream = require('./lib').mochaStream, - mochaIteration = require('./lib').mochaIteration, SpawnMocha = require('./lib').SpawnMocha, _ = require('lodash'), through = require('through'), @@ -136,7 +135,28 @@ gulp.task('test-live-output-with-prepend', function () { }); gulp.task('test-mocha-opts-parallel', function (cb) { - + function mochaIteration(opts, overrides, files, cb) { + opts = opts || {}; + var spawnMocha = new SpawnMocha(opts); + overrides.forEach(function (override) { + spawnMocha.add(files, override); + }); + var errors = []; + spawnMocha.on('error', function (err) { + console.error(err.toString()); + errors.push(err); + }).on('end', function () { + if (errors.length > 0) { + console.error('ERROR SUMMARY: '); + _(errors).each(function (err) { + console.error(err); + console.error(err.stack); + }).value(); + return cb(new Error('some tests failed')); + } + cb(null); + }); + } function setEnv(envs) { var env = process.env; @@ -147,24 +167,25 @@ gulp.task('test-mocha-opts-parallel', function (cb) { var opts = { concurrency: 3, - flags: {R: 'mocha-jenkins-reporter'}, - iterations: [{ - env: setEnv({NODE_ENV: 'groupa'}), - flags: {grep: "@groupA@"} - }, { - env: setEnv({NODE_ENV: 'groupb'}), - flags: {grep: "@groupB@"} - }, { - env: setEnv({NODE_ENV: 'groupc'}), - flags: {grep: "@groupC@"} - }] + flags: {R: 'mocha-jenkins-reporter'} }; + var overrides = [{ + env: setEnv({NODE_ENV: 'groupa'}), + flags: {grep: "@groupA@"} + }, { + env: setEnv({NODE_ENV: 'groupb'}), + flags: {grep: "@groupB@"} + }, { + env: setEnv({NODE_ENV: 'groupc'}), + flags: {grep: "@groupC@"} + }]; + glob('test/group/*-specs.js', function (err, files) { if (err) { return cb(err); } console.log('files', files); - mochaIteration(opts, files, cb); + mochaIteration(opts, overrides, files, cb); }); }); diff --git a/lib/index.js b/lib/index.js index d922c74..3e3eb4e 100644 --- a/lib/index.js +++ b/lib/index.js @@ -30,15 +30,15 @@ var SpawnMocha = function (opts) { }); var queue = async.queue(function (task, done) { // Setup - var iterationOpts = _.merge(_.clone(opts), task.iterationOpts); + var overrideOpts = _.merge(_.clone(opts), task.overrideOpts); var bin = _.isFunction(opts.bin) ? opts.bin() : opts.bin || join(__dirname, '..', 'node_modules', '.bin', 'mocha'); - var env = _.isFunction(iterationOpts.env) ? iterationOpts.env() : iterationOpts.env || process.env; + var env = _.isFunction(overrideOpts.env) ? overrideOpts.env() : overrideOpts.env || process.env; env = _.clone(env); // Generate arguments var args = []; - _(iterationOpts.flags).each(function (val, key) { + _(overrideOpts.flags).each(function (val, key) { if (_.isFunction(val)) val = val(); args.push((key.length > 1 ? '--' : '-') + key); if (_.isString(val) || _.isNumber(val)) { @@ -48,8 +48,8 @@ var SpawnMocha = function (opts) { var stdout = newStreamBuffer(); var stderr = newStreamBuffer(); - var fsStream = iterationOpts.fileOutput ? - fs.createWriteStream(iterationOpts.fileOutput, {flags: 'a', encoding: 'utf8'}) : null; + var fsStream = overrideOpts.fileOutput ? + fs.createWriteStream(overrideOpts.fileOutput, {flags: 'a', encoding: 'utf8'}) : null; // Split xunit test report in several files if required if (env.JUNIT_REPORT_PATH) { @@ -61,11 +61,11 @@ var SpawnMocha = function (opts) { if (opts.liveOutput) { child.stdout.pipe(split()) .on('data', function (line) { - console.log((iterationOpts.liveOutputPrepend || '') + line); + console.log((overrideOpts.liveOutputPrepend || '') + line); }); child.stderr.pipe(split()) .on('data', function (line) { - console.error((iterationOpts.liveOutputPrepend || '') + line); + console.error((overrideOpts.liveOutputPrepend || '') + line); }); if (fsStream) { child.stdout.pipe(fsStream); @@ -112,12 +112,12 @@ var SpawnMocha = function (opts) { }; var taskNum = 0; - this.add = function (files, iterationOpts) { + this.add = function (files, overrideOpts) { taskNum++; if (!_.isArray(files)) { files = [files]; } - var task = {taskNum: taskNum, files: files, iterationOpts: iterationOpts || {}}; + var task = {taskNum: taskNum, files: files, overrideOpts: overrideOpts || {}}; queue.push(task, function (err) { if (err) { _this.emit('error', err, files); @@ -152,30 +152,7 @@ var mochaStream = function mocha(opts) { return stream; }; -var mochaIteration = function mochaIteration(opts, files, cb) { - opts = opts || {}; - var spawnMocha = new SpawnMocha(opts); - opts.iterations.forEach(function (iteration) { - spawnMocha.add(files, iteration); - }); - var errors = []; - spawnMocha.on('error', function (err) { - console.error(err.toString()); - errors.push(err); - }).on('end', function () { - if (errors.length > 0) { - console.error('ERROR SUMMARY: '); - _(errors).each(function (err) { - console.error(err); - console.error(err.stack); - }).value(); - return cb(new Error('some tests failed')); - } - cb(null); - }); -}; module.exports = { SpawnMocha: SpawnMocha, - mochaStream: mochaStream, - mochaIteration: mochaIteration + mochaStream: mochaStream }; From 63797d20cc6cc95f72510b32dafaa95757d78851 Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Mon, 27 Apr 2015 10:13:27 -0700 Subject: [PATCH 08/10] rollback README changes --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 267eb0e..39271eb 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ then. ## Usage -### Using built in mochaStream: +Using built in mochaStream: ```js var mochaStream = require('spawn-mocha-parallel').mochaStream; @@ -31,8 +31,7 @@ gulp.task('test-mocha', function() { ``` - -### Using SpawnMocha +Using SpawnMocha ```js var SpawnMocha = require('spawn-mocha-parallel').SpawnMocha, @@ -81,8 +80,6 @@ gulp.task('test-custom-mocha', function() { - flags: mocha flags (default: none) - liveOutput: print output direct to console - errorSummary: show error summary (default: true) - - ## Todo - concatenate mocha status at the end @@ -92,4 +89,4 @@ gulp.task('test-custom-mocha', function() { MIT [gulp]: http://gulpjs.com/ "gulp.js" - [mocha]: http://visionmedia.github.io/mocha/ "Mocha" + [mocha]: http://visionmedia.github.io/mocha/ "Mocha" \ No newline at end of file From 58dab9ded3d7177b805cc6cfbaffb8b90fa5ca7d Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Mon, 27 Apr 2015 10:15:36 -0700 Subject: [PATCH 09/10] rollback README changes --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 39271eb..090fc0b 100644 --- a/README.md +++ b/README.md @@ -89,4 +89,4 @@ gulp.task('test-custom-mocha', function() { MIT [gulp]: http://gulpjs.com/ "gulp.js" - [mocha]: http://visionmedia.github.io/mocha/ "Mocha" \ No newline at end of file + [mocha]: http://visionmedia.github.io/mocha/ "Mocha" From e136a9ee8146f4f2c2352b2cf3a917f754b0b7a7 Mon Sep 17 00:00:00 2001 From: Matt Edelman Date: Mon, 27 Apr 2015 10:21:35 -0700 Subject: [PATCH 10/10] rolling back unnecessary whitespace reformatting --- gulpfile.js | 43 ++++++++++++++++++++----------------------- lib/index.js | 11 ++++++----- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/gulpfile.js b/gulpfile.js index 9c05d39..55ea8fa 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -9,8 +9,8 @@ var gulp = require('gulp'), runSequence = Q.denodeify(require('run-sequence')), assert = require('assert'), File = require('vinyl'), - path = require('path'), from = require('from'), + path = require('path'), glob = require('glob'); function customMocha(opts) { @@ -18,16 +18,15 @@ function customMocha(opts) { var spawnMocha = new SpawnMocha(opts); var stream = through(function write(file) { spawnMocha.add(file.path); - }, function () { - }); + }, function() {}); var errors = []; - spawnMocha.on('error', function (err) { + spawnMocha.on('error', function(err) { console.error(err.toString()); errors.push(err); - }).on('end', function () { - if (errors.length > 0) { + }).on('end', function() { + if(errors.length > 0) { console.error('ERROR SUMMARY: '); - _(errors).each(function (err) { + _(errors).each(function(err) { console.error(err); console.error(err.stack); }).value(); @@ -38,19 +37,18 @@ function customMocha(opts) { return stream; } -gulp.task('test-mocha', function () { +gulp.task('test-mocha', function() { var startMS = Date.now(); var StdOutFixture = require('fixture-stdout'); var fixture = new StdOutFixture(); var output = ''; - fixture.capture(function onWrite(string) { + fixture.capture( function onWrite (string ) { output += string; - return false; }); var mocha = mochaStream({concurrency: 10}); var srcFiles = []; - _(10).times(function () { + _(10).times(function() { srcFiles.push(new File({ cwd: "/", base: "test/", @@ -60,22 +58,21 @@ gulp.task('test-mocha', function () { return from(srcFiles) .pipe(mocha) .on('error', console.error) - .on('end', function () { + .on('end', function() { fixture.release(); - console.log(output); // we should have run 10 tests in parallel in less than 10 sec assert(output.match(/1 passing/g).length === 10); - assert(Date.now() - startMS < 5000); + assert( Date.now() - startMS < 5000); }); }); -gulp.task('test-custom-mocha', function () { +gulp.task('test-custom-mocha', function() { return gulp.src('test/*-specs.js', {read: false}) .pipe(customMocha()) .on('error', console.error); }); -gulp.task('test-live-output', function () { +gulp.task('test-live-output', function() { var mocha = mochaStream({liveOutput: true, concurrency: 1}); var srcFiles = []; srcFiles.push(new File({ @@ -87,7 +84,7 @@ gulp.task('test-live-output', function () { .pipe(mocha); }); -gulp.task('test-live-output-with-file', function () { +gulp.task('test-live-output-with-file', function() { var mocha = mochaStream({ liveOutput: true, fileOutput: '/tmp/out.log', @@ -102,7 +99,7 @@ gulp.task('test-live-output-with-file', function () { return from(srcFiles).pipe(mocha); }); -gulp.task('test-with-file', function () { +gulp.task('test-with-file', function() { var mocha = mochaStream({ fileOutput: '/tmp/out.log', concurrency: 1 @@ -117,12 +114,12 @@ gulp.task('test-with-file', function () { }); -gulp.task('test-live-output-with-prepend', function () { +gulp.task('test-live-output-with-prepend', function() { var mocha = mochaStream({ liveOutput: true, liveOutputPrepend: 'client --> ', concurrency: 1, - flags: {R: "tap"} + flags: { R: "tap" } }); var srcFiles = []; srcFiles.push(new File({ @@ -134,7 +131,7 @@ gulp.task('test-live-output-with-prepend', function () { .pipe(mocha); }); -gulp.task('test-mocha-opts-parallel', function (cb) { +gulp.task('test-mocha-opts-override', function (cb) { function mochaIteration(opts, overrides, files, cb) { opts = opts || {}; var spawnMocha = new SpawnMocha(opts); @@ -189,7 +186,7 @@ gulp.task('test-mocha-opts-parallel', function (cb) { }); }); -gulp.task('test', function () { +gulp.task('test', function() { return runSequence( 'test-mocha', 'test-custom-mocha', @@ -197,6 +194,6 @@ gulp.task('test', function () { 'test-live-output-with-prepend', 'test-live-output-with-file', 'test-with-file', - 'test-mocha-opts-parallel' + 'test-mocha-opts-override' ); }); diff --git a/lib/index.js b/lib/index.js index 3e3eb4e..8bfbd2d 100644 --- a/lib/index.js +++ b/lib/index.js @@ -126,6 +126,7 @@ var SpawnMocha = function (opts) { }; }; + util.inherits(SpawnMocha, EventEmitter); var mochaStream = function mocha(opts) { @@ -133,15 +134,15 @@ var mochaStream = function mocha(opts) { var spawnMocha = new SpawnMocha(opts); var stream = through(function write(file) { spawnMocha.add(file.path); - }, function () {}); + }, function() {}); var errors = []; - spawnMocha.on('error', function (err) { + spawnMocha.on('error', function(err) { console.error(err.toString()); errors.push(err); - }).on('end', function () { - if (errors.length > 0) { + }).on('end', function() { + if(errors.length > 0) { console.error('ERROR SUMMARY: '); - _(errors).each(function (err) { + _(errors).each(function(err) { console.error(err); console.error(err.stack); }).value();