diff --git a/lib/shared/exit.js b/lib/shared/exit.js index 0591e1ed..9d619c32 100644 --- a/lib/shared/exit.js +++ b/lib/shared/exit.js @@ -2,6 +2,7 @@ // Fix stdout truncation on windows function exit(code) { + /* istanbul ignore next */ if (process.platform === 'win32' && process.stdout.bufferSize) { process.stdout.once('drain', function() { process.exit(code); diff --git a/lib/versioned/^3.7.0/log/events.js b/lib/versioned/^3.7.0/log/events.js index b7bd00a8..99df25c0 100644 --- a/lib/versioned/^3.7.0/log/events.js +++ b/lib/versioned/^3.7.0/log/events.js @@ -12,7 +12,21 @@ function logEvents(gulpInst) { // Exit with 0 or 1 var failed = false; + var tasks = []; + process.once('exit', function(code) { + if (tasks.length) { + failed = true; + + log.warn( + ansi.red('The following tasks did not complete:'), + ansi.cyan(tasks.join(', ')) + ); + log.warn( + ansi.red('Did you forget to signal async completion?') + ); + } + if (code === 0 && failed) { exit(1); } @@ -24,6 +38,8 @@ function logEvents(gulpInst) { }); gulpInst.on('task_start', function(e) { + tasks.push(e.task); + // TODO: batch these // so when 5 tasks start at once it only logs one time with all 5 log.info('Starting', '\'' + ansi.cyan(e.task) + '\'...'); @@ -35,6 +51,12 @@ function logEvents(gulpInst) { 'Finished', '\'' + ansi.cyan(e.task) + '\'', 'after', ansi.magenta(time) ); + + var foundIndex = tasks.indexOf(e.task); + /* istanbul ignore else */ + if (foundIndex >= 0) { + tasks.splice(foundIndex, 1); + } }); gulpInst.on('task_err', function(e) { diff --git a/package.json b/package.json index 246250f3..611868a9 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "lint": "eslint . && jscs index.js bin/ lib/ test/", "prepublish": "marked-man --name gulp docs/CLI.md > gulp.1", "pretest": "npm run lint", - "test": "mocha --async-only --timeout 5000 test/lib test", + "test": "mocha --async-only --timeout 5000 test/lib test/lib/v3.7.0 test", "cover": "nyc --reporter=lcov --reporter=text-summary npm test", "coveralls": "nyc --reporter=text-lcov npm test | coveralls" }, @@ -64,7 +64,9 @@ "marked-man": "^0.1.3", "mocha": "^3.2.0", "nyc": "^10.0.0", - "rimraf": "^2.6.1" + "plugin-error": "^1.0.0", + "rimraf": "^2.6.1", + "sver-compat": "^1.5.0" }, "keywords": [ "build", diff --git a/test/lib/v3.7.0/events.js b/test/lib/v3.7.0/events.js new file mode 100644 index 00000000..663682aa --- /dev/null +++ b/test/lib/v3.7.0/events.js @@ -0,0 +1,274 @@ +'use strict'; + +var expect = require('expect'); +var EventEmitter = require('events').EventEmitter; +var os = require('os'); +var log = require('gulplog'); +var Semver = require('sver-compat').Semver; +var ansi = require('../../../lib/shared/ansi'); + +var logEvents = require('../../../lib/versioned/^3.7.0/log/events'); + +var infoBuf = []; +log.info = function() { + infoBuf.push([].join.call(arguments, ' ')); +}; +var warnBuf = []; +log.warn = function() { + warnBuf.push([].join.call(arguments, ' ')); +}; +var errorBuf = []; +log.error = function() { + errorBuf.push([].join.call(arguments, ' ')); +}; + +function noColor(v) { + return v; +} +var ansiOrig = {}; + +var origExit = process.exit; +var origOnce = process.once; + +var tmpProcess = new EventEmitter(); + +// See https://www.appveyor.com/docs/lang/nodejs-iojs/#garbled-or-missing-output +function isGarbledOrMissingOutput() { + if (os.platform() === 'win32') { + var currentVersion = new Semver(process.versions.node); + var minimalVersion = new Semver('0.11.12'); + return currentVersion.lt(minimalVersion); + } + return false; +} + +describe('lib: ^3.7.0/log/events', function() { + before(function() { + Object.keys(ansi).forEach(function(color) { + ansiOrig[color] = ansi[color]; + ansi[color] = noColor; + }); + }); + after(function() { + Object.keys(ansi).forEach(function(color) { + ansi[color] = ansiOrig[color]; + }); + }); + beforeEach(function() { + process.exit = function() { + expect(true).toBe(false); + }; + process.once = tmpProcess.once.bind(tmpProcess); + infoBuf = []; + warnBuf = []; + errorBuf = []; + }); + afterEach(function() { + process.exit = origExit; + process.once = origOnce; + }); + + describe('"exit" event', function() { + it('Should not call process.exit when no error', function(done) { + var ee = new EventEmitter(); + logEvents(ee); + tmpProcess.emit('exit', 0); + expect(infoBuf).toEqual([]); + expect(warnBuf).toEqual([]); + expect(errorBuf).toEqual([]); + done(); + }); + }); + + describe('"err" event', function() { + it('Should call process.exit with exit code 1 when error', function(done) { + process.exit = function(code) { + if (!isGarbledOrMissingOutput()) { + expect(code).toBe(1); + done(); + } + }; + + var ee = new EventEmitter(); + logEvents(ee); + ee.emit('err'); + tmpProcess.emit('exit', 0); + expect(infoBuf).toEqual([]); + expect(warnBuf).toEqual([]); + expect(errorBuf).toEqual([]); + + if (isGarbledOrMissingOutput()) { + done(); + } + }); + + it('Should not call process.exit when already exit code is not zero', function(done) { + var ee = new EventEmitter(); + logEvents(ee); + ee.emit('err'); + tmpProcess.emit('exit', 1); + expect(infoBuf).toEqual([]); + expect(warnBuf).toEqual([]); + expect(errorBuf).toEqual([]); + done(); + }); + }); + + describe('"task_start" and "task_stop" events', function() { + it('Should output logs for starting and finishing a task', function(done) { + var ee = new EventEmitter(); + logEvents(ee); + ee.emit('task_start', { task: 'task-0' }); + ee.emit('task_stop', { task: 'task-0', hrDuration: [0, 12] }); + tmpProcess.emit('exit', 0); + expect(warnBuf).toEqual([]); + expect(errorBuf).toEqual([]); + expect(infoBuf).toEqual([ + 'Starting \'task-0\'...', + 'Finished \'task-0\' after 12 ns', + ]); + done(); + }); + + it('Should exit without error if there is no incomplete task', function(done) { + var ee = new EventEmitter(); + logEvents(ee); + ee.emit('task_start', { task: 'task-0' }); + ee.emit('task_start', { task: 'task-1' }); + ee.emit('task_start', { task: 'task-2' }); + ee.emit('task_stop', { task: 'task-2', hrDuration: [0, 12] }); + ee.emit('task_start', { task: 'task-3' }); + ee.emit('task_stop', { task: 'task-1', hrDuration: [0, 12] }); + ee.emit('task_stop', { task: 'task-3', hrDuration: [0, 12] }); + ee.emit('task_stop', { task: 'task-0', hrDuration: [0, 12] }); + tmpProcess.emit('exit', 0); + expect(warnBuf).toEqual([]); + expect(errorBuf).toEqual([]); + expect(infoBuf).toEqual([ + 'Starting \'task-0\'...', + 'Starting \'task-1\'...', + 'Starting \'task-2\'...', + 'Finished \'task-2\' after 12 ns', + 'Starting \'task-3\'...', + 'Finished \'task-1\' after 12 ns', + 'Finished \'task-3\' after 12 ns', + 'Finished \'task-0\' after 12 ns', + ]); + done(); + }); + + it('Should exit with warning log if there are incomplete tasks', function(done) { + process.exit = function(code) { + if (!isGarbledOrMissingOutput()) { + expect(code).toBe(1); + done(); + } + }; + + var ee = new EventEmitter(); + logEvents(ee); + ee.emit('task_start', { task: 'task-0' }); + ee.emit('task_start', { task: 'task-2' }); + ee.emit('task_start', { task: 'task-1' }); + ee.emit('task_start', { task: 'task-3' }); + tmpProcess.emit('exit', 0); + expect(errorBuf).toEqual([]); + expect(infoBuf).toEqual([ + 'Starting \'task-0\'...', + 'Starting \'task-2\'...', + 'Starting \'task-1\'...', + 'Starting \'task-3\'...', + ]); + expect(warnBuf).toEqual([ + 'The following tasks did not complete: task-0, task-2, task-1, task-3', + 'Did you forget to signal async completion?', + ]); + + if (isGarbledOrMissingOutput()) { + done(); + } + }); + + it('Should exit with warning log if there are incomplete tasks (2)', function(done) { + process.exit = function(code) { + if (!isGarbledOrMissingOutput()) { + expect(code).toBe(1); + done(); + } + }; + + var ee = new EventEmitter(); + logEvents(ee); + ee.emit('task_start', { task: 'task-0' }); + ee.emit('task_start', { task: 'task-2' }); + ee.emit('task_start', { task: 'task-1' }); + ee.emit('task_stop', { task: 'task-2', hrDuration: [0, 12] }); + ee.emit('task_start', { task: 'task-3' }); + ee.emit('task_stop', { task: 'task-3', hrDuration: [0, 12] }); + tmpProcess.emit('exit', 0); + expect(errorBuf).toEqual([]); + expect(infoBuf).toEqual([ + 'Starting \'task-0\'...', + 'Starting \'task-2\'...', + 'Starting \'task-1\'...', + 'Finished \'task-2\' after 12 ns', + 'Starting \'task-3\'...', + 'Finished \'task-3\' after 12 ns', + ]); + expect(warnBuf).toEqual([ + 'The following tasks did not complete: task-0, task-1', + 'Did you forget to signal async completion?', + ]); + + if (isGarbledOrMissingOutput()) { + done(); + } + }); + }); + + describe('"task_err" event', function() { + it('Should output log for unfound task', function(done) { + var ee = new EventEmitter(); + logEvents(ee); + ee.emit('task_err', { + task: 'task-0', + hrDuration: [0, 12], + message: 'Error Message', + }); + expect(infoBuf).toEqual([]); + expect(warnBuf).toEqual([]); + expect(errorBuf).toEqual([ + '\'task-0\' errored after 12 ns', + 'Error Message', + ]); + done(); + }); + }); + + describe('"task_not_found" event', function() { + it('Should output log for unfound task and call process.exit', function(done) { + process.exit = function(code) { + if (!isGarbledOrMissingOutput()) { + expect(code).toBe(1); + done(); + } + }; + + var ee = new EventEmitter(); + logEvents(ee); + ee.emit('task_not_found', { task: 'task-0' }); + expect(infoBuf).toEqual([]); + expect(warnBuf).toEqual([]); + expect(errorBuf).toEqual([ + 'Task \'task-0\' is not in your gulpfile', + 'Please check the documentation for proper gulpfile formatting', + ]); + + if (isGarbledOrMissingOutput()) { + done(); + } + }); + }); + +}); + diff --git a/test/lib/v3.7.0/format-error.js b/test/lib/v3.7.0/format-error.js new file mode 100644 index 00000000..8220c559 --- /dev/null +++ b/test/lib/v3.7.0/format-error.js @@ -0,0 +1,34 @@ +'use strict'; + +var expect = require('expect'); +var PluginError = require('plugin-error'); + +var formatError = require('../../../lib/versioned/^3.7.0/format-error'); + +describe('lib: ^3.7.0/format-error', function() { + + it('Should return error message when error is not orchestrator error', function(done) { + var err = new TypeError('ERROR!'); + expect(formatError(err)).toEqual('ERROR!'); + done(); + }); + + it('Should return error message when PluginError', function(done) { + var err = new PluginError('test', 'something broke'); + expect(formatError({ task: 'task-0', err: err })).toEqual(err.toString()); + done(); + }); + + it('Should return error message when normal error', function(done) { + var err = new TypeError('ERROR!'); + expect(formatError({ task: 'task-0', err: err })).toEqual(err.stack); + done(); + }); + + it('Should return error message when unknown error', function(done) { + var stack = formatError({ task: 'task-0', err: 'abc' }); + expect(stack.split(/\r\n|\r|\n/)[0]).toEqual('Error: abc'); + done(); + }); +}); +