diff --git a/lib/winston.js b/lib/winston.js index b396a3af8..7e4e29ae8 100644 --- a/lib/winston.js +++ b/lib/winston.js @@ -51,6 +51,11 @@ winston.createLogger = require('./winston/create-logger'); * @type {Object} */ winston.ExceptionHandler = require('./winston/exception-handler'); +/** + * Expose core Logging-related prototypes. + * @type {Object} + */ +winston.RejectionHandler = require('./winston/rejection-handler'); /** * Expose core Logging-related prototypes. * @type {Container} @@ -85,21 +90,25 @@ winston.loggers = new winston.Container(); const defaultLogger = winston.createLogger(); // Pass through the target methods onto `winston. -Object.keys(winston.config.npm.levels).concat([ - 'log', - 'query', - 'stream', - 'add', - 'remove', - 'clear', - 'profile', - 'startTimer', - 'handleExceptions', - 'unhandleExceptions', - 'configure' -]).forEach(method => ( - winston[method] = (...args) => defaultLogger[method](...args) -)); +Object.keys(winston.config.npm.levels) + .concat([ + 'log', + 'query', + 'stream', + 'add', + 'remove', + 'clear', + 'profile', + 'startTimer', + 'handleExceptions', + 'unhandleExceptions', + 'handleRejections', + 'unhandleRejections', + 'configure' + ]) + .forEach( + method => (winston[method] = (...args) => defaultLogger[method](...args)) + ); /** * Define getter / setter for the default logger level which need to be exposed @@ -131,9 +140,7 @@ Object.defineProperty(winston, 'exceptions', { * which need to be exposed by winston. * @type {Logger} */ -[ - 'exitOnError' -].forEach(prop => { +['exitOnError'].forEach(prop => { Object.defineProperty(winston, prop, { get() { return defaultLogger[prop]; @@ -152,6 +159,7 @@ Object.defineProperty(winston, 'default', { get() { return { exceptionHandlers: defaultLogger.exceptionHandlers, + rejectionHandlers: defaultLogger.rejectionHandlers, transports: defaultLogger.transports }; } @@ -160,20 +168,14 @@ Object.defineProperty(winston, 'default', { // Have friendlier breakage notices for properties that were exposed by default // on winston < 3.0. warn.deprecated(winston, 'setLevels'); -warn.forFunctions(winston, 'useFormat', ['cli']); -warn.forProperties(winston, 'useFormat', [ - 'padLevels', - 'stripColors' -]); -warn.forFunctions(winston, 'deprecated', [ +warn.forFunctions(winston, 'useFormat', ['cli']); +warn.forProperties(winston, 'useFormat', ['padLevels', 'stripColors']); +warn.forFunctions(winston, 'deprecated', [ 'addRewriter', 'addFilter', 'clone', 'extend' ]); -warn.forProperties(winston, 'deprecated', [ - 'emitErrs', - 'levelLength' -]); +warn.forProperties(winston, 'deprecated', ['emitErrs', 'levelLength']); // Throw a useful error when users attempt to run `new winston.Logger`. warn.moved(winston, 'createLogger', 'Logger'); diff --git a/lib/winston/logger.js b/lib/winston/logger.js index f99b39272..dbc9efe28 100644 --- a/lib/winston/logger.js +++ b/lib/winston/logger.js @@ -12,6 +12,7 @@ const asyncForEach = require('async/forEach'); const { LEVEL, SPLAT } = require('triple-beam'); const isStream = require('is-stream'); const ExceptionHandler = require('./exception-handler'); +const RejectionHandler = require('./rejection-handler'); const LegacyTransportStream = require('winston-transport/legacy'); const Profiler = require('./profiler'); const { warn } = require('./common'); @@ -88,7 +89,8 @@ class Logger extends Transform { padLevels, rewriters, stripColors, - exceptionHandlers + exceptionHandlers, + rejectionHandlers } = {}) { // Reset transports if we already have them if (this.transports.length) { @@ -103,6 +105,7 @@ class Logger extends Transform { this.levels = levels || this.levels || config.npm.levels; this.level = level; this.exceptions = new ExceptionHandler(this); + this.rejections = new RejectionHandler(this); this.profilers = {}; this.exitOnError = exitOnError; @@ -113,19 +116,28 @@ class Logger extends Transform { } if ( - colors || emitErrs || formatters || - padLevels || rewriters || stripColors + colors || + emitErrs || + formatters || + padLevels || + rewriters || + stripColors ) { - throw new Error([ - '{ colors, emitErrs, formatters, padLevels, rewriters, stripColors } were removed in winston@3.0.0.', - 'Use a custom winston.format(function) instead.', - 'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md' - ].join('\n')); + throw new Error( + [ + '{ colors, emitErrs, formatters, padLevels, rewriters, stripColors } were removed in winston@3.0.0.', + 'Use a custom winston.format(function) instead.', + 'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md' + ].join('\n') + ); } if (exceptionHandlers) { this.exceptions.handle(exceptionHandlers); } + if (rejectionHandlers) { + this.rejections.handle(rejectionHandlers); + } } isLevelEnabled(level) { @@ -183,7 +195,8 @@ class Logger extends Transform { * */ /* eslint-enable valid-jsdoc */ - log(level, msg, ...splat) { // eslint-disable-line max-params + log(level, msg, ...splat) { + // eslint-disable-line max-params // Optimize for the hotpath of logging JSON literals if (arguments.length === 1) { // Yo dawg, I heard you like levels ... seriously ... @@ -276,7 +289,10 @@ class Logger extends Transform { // Remark: not sure if we should simply error here. if (!this._readableState.pipes) { // eslint-disable-next-line no-console - console.error('[winston] Attempt to write logs with no transports %j', info); + console.error( + '[winston] Attempt to write logs with no transports %j', + info + ); } // Here we write to the `format` pipe-chain, which on `readable` above will @@ -300,11 +316,15 @@ class Logger extends Transform { */ _final(callback) { const transports = this.transports.slice(); - asyncForEach(transports, (transport, next) => { - if (!transport || transport.finished) return setImmediate(next); - transport.once('finish', next); - transport.end(); - }, callback); + asyncForEach( + transports, + (transport, next) => { + if (!transport || transport.finished) return setImmediate(next); + transport.once('finish', next); + transport.end(); + }, + callback + ); } /** @@ -318,12 +338,15 @@ class Logger extends Transform { // 1. They inherit from winston.Transport in < 3.x.x which is NOT a stream. // 2. They expose a log method which has a length greater than 2 (i.e. more then // just `log(info, callback)`. - const target = !isStream(transport) || transport.log.length > 2 - ? new LegacyTransportStream({ transport }) - : transport; + const target = + !isStream(transport) || transport.log.length > 2 + ? new LegacyTransportStream({ transport }) + : transport; if (!target._writableState || !target._writableState.objectMode) { - throw new Error('Transports must WritableStreams in objectMode. Set { objectMode: true }.'); + throw new Error( + 'Transports must WritableStreams in objectMode. Set { objectMode: true }.' + ); } // Listen for the `error` event and the `warn` event on the new Transport. @@ -335,6 +358,10 @@ class Logger extends Transform { this.exceptions.handle(); } + if (transport.handleRejections) { + this.rejections.handle(); + } + return this; } @@ -346,11 +373,14 @@ class Logger extends Transform { remove(transport) { let target = transport; if (!isStream(transport) || transport.log.length > 2) { - target = this.transports - .filter(match => match.transport === transport)[0]; + target = this.transports.filter( + match => match.transport === transport + )[0]; } - if (target) { this.unpipe(target); } + if (target) { + this.unpipe(target); + } return this; } @@ -523,7 +553,9 @@ class Logger extends Transform { // Attempt to be kind to users if they are still using older APIs. if (typeof args[args.length - 2] === 'function') { // eslint-disable-next-line no-console - console.warn('Callback function no longer supported as of winston@3.0.0'); + console.warn( + 'Callback function no longer supported as of winston@3.0.0' + ); args.pop(); } @@ -546,7 +578,9 @@ class Logger extends Transform { */ handleExceptions(...args) { // eslint-disable-next-line no-console - console.warn('Deprecated: .handleExceptions() will be removed in winston@4. Use .exceptions.handle()'); + console.warn( + 'Deprecated: .handleExceptions() will be removed in winston@4. Use .exceptions.handle()' + ); this.exceptions.handle(...args); } @@ -557,7 +591,9 @@ class Logger extends Transform { */ unhandleExceptions(...args) { // eslint-disable-next-line no-console - console.warn('Deprecated: .unhandleExceptions() will be removed in winston@4. Use .exceptions.unhandle()'); + console.warn( + 'Deprecated: .unhandleExceptions() will be removed in winston@4. Use .exceptions.unhandle()' + ); this.exceptions.unhandle(...args); } @@ -566,11 +602,13 @@ class Logger extends Transform { * @throws {Error} - TODO: add throws description. */ cli() { - throw new Error([ - 'Logger.cli() was removed in winston@3.0.0', - 'Use a custom winston.formats.cli() instead.', - 'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md' - ].join('\n')); + throw new Error( + [ + 'Logger.cli() was removed in winston@3.0.0', + 'Use a custom winston.formats.cli() instead.', + 'See: https://github.com/winstonjs/winston/tree/master/UPGRADE-3.0.md' + ].join('\n') + ); } /** diff --git a/lib/winston/rejection-handler.js b/lib/winston/rejection-handler.js new file mode 100644 index 000000000..7e98dec0b --- /dev/null +++ b/lib/winston/rejection-handler.js @@ -0,0 +1,254 @@ +/** + * exception-handler.js: Object for handling uncaughtException events. + * + * (C) 2010 Charlie Robbins + * MIT LICENCE + */ + +'use strict'; + +const os = require('os'); +const asyncForEach = require('async/forEach'); +const debug = require('diagnostics')('winston:rejection'); +const once = require('one-time'); +const stackTrace = require('stack-trace'); +const ExceptionStream = require('./exception-stream'); + +/** + * Object for handling unhandledRejection events. + * @type {RejectionHandler} + */ +module.exports = class RejectionHandler { + /** + * TODO: add contructor description + * @param {!Logger} logger - TODO: add param description + */ + constructor(logger) { + if (!logger) { + throw new Error('Logger is required to handle rejections'); + } + + this.logger = logger; + this.handlers = new Map(); + } + + /** + * Handles `unhandledRejection` events for the current process by adding any + * handlers passed in. + * @returns {undefined} + */ + handle(...args) { + args.forEach(arg => { + if (Array.isArray(arg)) { + return arg.forEach(handler => this._addHandler(handler)); + } + + this._addHandler(arg); + }); + + if (!this.catcher) { + this.catcher = this._unhandledRejection.bind(this); + process.on('unhandledRejection', this.catcher); + } + } + + /** + * Removes any handlers to `unhandledRejection` events for the current + * process. This does not modify the state of the `this.handlers` set. + * @returns {undefined} + */ + unhandle() { + if (this.catcher) { + process.removeListener('unhandledRejection', this.catcher); + this.catcher = false; + + Array.from(this.handlers.values()).forEach(wrapper => + this.logger.unpipe(wrapper) + ); + } + } + + /** + * TODO: add method description + * @param {Error} err - Error to get information about. + * @returns {mixed} - TODO: add return description. + */ + getAllInfo(err) { + let { message } = err; + if (!message && typeof err === 'string') { + message = err; + } + + return { + error: err, + // TODO (indexzero): how do we configure this? + level: 'error', + message: [ + `unhandledRejection: ${message || '(no error message)'}`, + err.stack || ' No stack trace' + ].join('\n'), + stack: err.stack, + exception: true, + date: new Date().toString(), + process: this.getProcessInfo(), + os: this.getOsInfo(), + trace: this.getTrace(err) + }; + } + + /** + * Gets all relevant process information for the currently running process. + * @returns {mixed} - TODO: add return description. + */ + getProcessInfo() { + return { + pid: process.pid, + uid: process.getuid ? process.getuid() : null, + gid: process.getgid ? process.getgid() : null, + cwd: process.cwd(), + execPath: process.execPath, + version: process.version, + argv: process.argv, + memoryUsage: process.memoryUsage() + }; + } + + /** + * Gets all relevant OS information for the currently running process. + * @returns {mixed} - TODO: add return description. + */ + getOsInfo() { + return { + loadavg: os.loadavg(), + uptime: os.uptime() + }; + } + + /** + * Gets a stack trace for the specified error. + * @param {mixed} err - TODO: add param description. + * @returns {mixed} - TODO: add return description. + */ + getTrace(err) { + const trace = err ? stackTrace.parse(err) : stackTrace.get(); + return trace.map(site => { + return { + column: site.getColumnNumber(), + file: site.getFileName(), + function: site.getFunctionName(), + line: site.getLineNumber(), + method: site.getMethodName(), + native: site.isNative() + }; + }); + } + + /** + * Helper method to add a transport as an exception handler. + * @param {Transport} handler - The transport to add as an exception handler. + * @returns {void} + */ + _addHandler(handler) { + if (!this.handlers.has(handler)) { + handler.handleExceptions = true; + const wrapper = new ExceptionStream(handler); + this.handlers.set(handler, wrapper); + this.logger.pipe(wrapper); + } + } + + /** + * Logs all relevant information around the `err` and exits the current + * process. + * @param {Error} err - Error to handle + * @returns {mixed} - TODO: add return description. + * @private + */ + _unhandledRejection(err) { + const info = this.getAllInfo(err); + const handlers = this._getRejectionHandlers(); + // Calculate if we should exit on this error + let doExit = + typeof this.logger.exitOnError === 'function' + ? this.logger.exitOnError(err) + : this.logger.exitOnError; + let timeout; + + if (!handlers.length && doExit) { + // eslint-disable-next-line no-console + console.warn( + 'winston: exitOnError cannot be false with no rejection handlers.' + ); + // eslint-disable-next-line no-console + console.warn('winston: exiting process.'); + doExit = false; + } + + function gracefulExit() { + debug('doExit', doExit); + debug('process._exiting', process._exiting); + + if (doExit && !process._exiting) { + // Remark: Currently ignoring any rejections from transports when + // catching unhandled rejections. + if (timeout) { + clearTimeout(timeout); + } + // eslint-disable-next-line no-process-exit + process.exit(1); + } + } + + if (!handlers || handlers.length === 0) { + return process.nextTick(gracefulExit); + } + + // Log to all transports attempting to listen for when they are completed. + asyncForEach( + handlers, + (handler, next) => { + // TODO: Change these to the correct WritableStream events so that we + // wait until exit. + const done = once(next); + const transport = handler.transport || handler; + + // Debug wrapping so that we can inspect what's going on under the covers. + function onDone(event) { + return () => { + debug(event); + done(); + }; + } + + transport.once('logged', onDone('logged')); + transport.once('error', onDone('error')); + }, + gracefulExit + ); + + this.logger.log(info); + + // If exitOnError is true, then only allow the logging of exceptions to + // take up to `3000ms`. + if (doExit) { + timeout = setTimeout(gracefulExit, 3000); + } + } + + /** + * Returns the list of transports and exceptionHandlers for this instance. + * @returns {Array} - List of transports and exceptionHandlers for this + * instance. + * @private + */ + _getRejectionHandlers() { + // Remark (indexzero): since `logger.transports` returns all of the pipes + // from the _readableState of the stream we actually get the join of the + // explicit handlers and the implicit transports with + // `handleRejections: true` + return this.logger.transports.filter(wrap => { + const transport = wrap.transport || wrap; + return transport.handleRejections; + }); + } +}; diff --git a/test/exception-handler.test.js b/test/exception-handler.test.js index 06b95b578..9a2286c7b 100644 --- a/test/exception-handler.test.js +++ b/test/exception-handler.test.js @@ -50,30 +50,22 @@ describe('ExceptionHandler', function () { it('.getProcessInfo()', function () { var handler = helpers.exceptionHandler(); - helpers.assertProcessInfo( - handler.getProcessInfo() - ); + helpers.assertProcessInfo(handler.getProcessInfo()); }); it('.getOsInfo()', function () { var handler = helpers.exceptionHandler(); - helpers.assertOsInfo( - handler.getOsInfo() - ); + helpers.assertOsInfo(handler.getOsInfo()); }); it('.getTrace(new Error)', function () { var handler = helpers.exceptionHandler(); - helpers.assertTrace( - handler.getTrace(new Error()) - ); + helpers.assertTrace(handler.getTrace(new Error())); }); it('.getTrace()', function () { var handler = helpers.exceptionHandler(); - helpers.assertTrace( - handler.getTrace() - ); + helpers.assertTrace(handler.getTrace()); }); it('.handle()', function (done) { @@ -107,8 +99,9 @@ describe('ExceptionHandler', function () { handler.handle(); assume(handler.catcher).is.a('function'); - assume(process.listeners('uncaughtException')) - .deep.equals([handler.catcher]); + assume(process.listeners('uncaughtException')).deep.equals([ + handler.catcher + ]); helpers.throw('wtf this error'); }); diff --git a/test/helpers/index.js b/test/helpers/index.js index c86cf8528..fe51be9e3 100644 --- a/test/helpers/index.js +++ b/test/helpers/index.js @@ -59,6 +59,19 @@ helpers.exceptionHandler = function (opts) { return new winston.ExceptionHandler(logger); }; +/** + * Creates a new RejectionHandler instance with a new + * winston.Logger instance with the specified options + * + * @param {Object} opts Options for the logger associated + * with the RejectionHandler + * @returns {RejectionHandler} A new ExceptionHandler instance + */ +helpers.rejectionHandler = function (opts) { + var logger = winston.createLogger(opts); + return new winston.RejectionHandler(logger); +}; + /** * Removes all listeners to `process.on('uncaughtException')` * and returns an object that allows you to restore them later. @@ -79,6 +92,26 @@ helpers.clearExceptions = function () { }; }; +/** + * Removes all listeners to `process.on('unhandledRejection')` + * and returns an object that allows you to restore them later. + * + * @returns {Object} Facade to restore unhandledRejection handlers. + */ +helpers.clearRejections = function () { + var listeners = process.listeners('unhandledRejection'); + process.removeAllListeners('unhandledRejections'); + + return { + restore: function () { + process.removeAllListeners('unhandledRejection'); + listeners.forEach(function (fn) { + process.on('unhandledRejection', fn); + }); + } + }; +}; + /** * Throws an exception with the specified `msg` * @param {String} msg Error mesage to use @@ -87,25 +120,37 @@ helpers.throw = function (msg) { throw new Error(msg); }; +/** + * Causes a Promise rejection with the specified `msg` + * @param {String} msg Error mesage to use + */ +helpers.reject = function (msg) { + return new Promise((resolve, reject) => { + reject(msg); + }); +}; + /** * Attempts to unlink the specifyed `filename` ignoring errors * @param {String} File Full path to attempt to unlink. */ helpers.tryUnlink = function (filename) { - try { fs.unlinkSync(filename); } - catch (ex) { } + try { + fs.unlinkSync(filename); + } catch (ex) {} }; /** * Returns a stream that will emit data for the `filename` if it exists * and is capable of being opened. * @param {filename} Full path to attempt to read from. - * @return {Stream} Stream instance to the contents of the file + * @returns {Stream} Stream instance to the contents of the file */ helpers.tryRead = function tryRead(filename) { var proxy = through(); (function inner() { - var stream = fs.createReadStream(filename) + var stream = fs + .createReadStream(filename) .once('open', function () { stream.pipe(proxy); }) @@ -115,7 +160,7 @@ helpers.tryRead = function tryRead(filename) { } proxy.emit('error', err); }); - })(); + }()); return proxy; }; @@ -188,7 +233,7 @@ helpers.assertLogger = function (logger, level) { * Asserts that the script located at `options.script` logs a single exception * (conforming to the ExceptionHandler structure) at the specified `options.logfile`. * @param {Object} options Configuration for this test. - * @return {function} Test macro asserting that `options.script` performs the + * @returns {function} Test macro asserting that `options.script` performs the * expected behavior. */ helpers.assertHandleExceptions = function (options) { @@ -219,3 +264,41 @@ helpers.assertHandleExceptions = function (options) { }); }; }; + +/** + * Asserts that the script located at `options.script` logs a single rejection + * (conforming to the RejectionHandler structure) at the specified `options.logfile`. + * @param {Object} options Configuration for this test. + * @returns {function} Test macro asserting that `options.script` performs the + * expected behavior. + */ +helpers.assertHandleRejections = function (options) { + return function (done) { + var child = spawn('node', [options.script]); + + if (process.env.DEBUG) { + child.stdout.pipe(process.stdout); + child.stderr.pipe(process.stdout); + } + + helpers.tryUnlink(options.logfile); + child.on('exit', function () { + fs.readFile(options.logfile, function (err, data) { + assume(err).equals(null); + data = JSON.parse(data); + + assume(data).is.an('object'); + helpers.assertProcessInfo(data.process); + helpers.assertOsInfo(data.os); + helpers.assertTrace(data.trace); + if (options.message) { + assume(data.message).include( + 'unhandledRejection: ' + options.message + ); + } + + done(); + }); + }); + }; +}; diff --git a/test/helpers/scripts/default-rejections.js b/test/helpers/scripts/default-rejections.js new file mode 100644 index 000000000..90ae53281 --- /dev/null +++ b/test/helpers/scripts/default-rejections.js @@ -0,0 +1,30 @@ +/* + * default-rejectionss.js: A test fixture for logging rejections with the default winston logger. + * + * (C) 2011 Charlie Robbins + * MIT LICENCE + * + */ + +var path = require("path"), + winston = require("../../../lib/winston"); + +winston.rejections.handle([ + new winston.transports.File({ + filename: path.join( + __dirname, + "..", + "..", + "fixtures", + "logs", + "default-rejection.log" + ), + handleRejections: true + }) +]); + +winston.info("Log something before error"); + +setTimeout(function() { + Promise.reject(new Error("OH NOES! It rejected!")); +}, 1000); diff --git a/test/helpers/scripts/log-rejections.js b/test/helpers/scripts/log-rejections.js new file mode 100644 index 000000000..b8025a178 --- /dev/null +++ b/test/helpers/scripts/log-rejections.js @@ -0,0 +1,32 @@ +/* + * log-rejections.js: A test fixture for logging rejections in winston. + * + * (C) 2011 Charlie Robbins + * MIT LICENCE + * + */ + +var path = require("path"), + winston = require("../../../lib/winston"); + +var logger = winston.createLogger({ + transports: [ + new winston.transports.File({ + filename: path.join( + __dirname, + "..", + "..", + "fixtures", + "logs", + "rejections.log" + ), + handleRejections: true + }) + ] +}); + +logger.rejections.handle(); + +setTimeout(function() { + Promise.reject(new Error("OH NOES! It rejected!")); +}, 1000); diff --git a/test/helpers/scripts/unhandle-rejections.js b/test/helpers/scripts/unhandle-rejections.js new file mode 100644 index 000000000..98c64dabf --- /dev/null +++ b/test/helpers/scripts/unhandle-rejections.js @@ -0,0 +1,27 @@ +/* + * unhandle-rejections.js: A test fixture for using `.unhandleRejections()` winston. + * + * (C) 2011 Charlie Robbins + * MIT LICENCE + * + */ + +var path = require('path'), + winston = require('../../../lib/winston'); + +var logger = winston.createLogger({ + transports: [ + new winston.transports.File({ + filename: path.join(__dirname, '..', 'logs', 'unhandle-rejections.log') + }) + ] +}); + +logger.transports[0].transport.handleRejections; + +logger.rejections.handle(); +logger.rejections.unhandle(); + +setTimeout(function () { + Promise.reject(new Error('OH NOES! It rejected!')); +}, 200); diff --git a/test/rejection-handler.test.js b/test/rejection-handler.test.js new file mode 100644 index 000000000..c576b2738 --- /dev/null +++ b/test/rejection-handler.test.js @@ -0,0 +1,116 @@ +/* + * rejection-test.js: Tests for rejection data gathering in winston. + * + * (C) 2010 Charlie Robbins + * MIT LICENSE + * + */ + +const stream = require('stream'); +const assume = require('assume'); +const mocha = require('mocha'); +const winston = require('../lib/winston'); +const helpers = require('./helpers'); + +// +// This is an awful and fragile hack that +// needs to be changed ASAP. +// https://github.com/mochajs/mocha/issues/1985 +// +var _runTest = mocha.Runner.prototype.runTest; +mocha.Runner.prototype.runTest = function () { + this.allowUncaught = true; + _runTest.apply(this, arguments); +}; + +describe('UnhandledRejectionHandler', function () { + this.timeout(5000); + + it('has expected methods', function () { + var handler = helpers.rejectionHandler(); + assume(handler.handle).is.a('function'); + assume(handler.unhandle).is.a('function'); + assume(handler.getAllInfo).is.a('function'); + assume(handler.getProcessInfo).is.a('function'); + assume(handler.getOsInfo).is.a('function'); + assume(handler.getTrace).is.a('function'); + }); + + it('new RejectionHandler()', function () { + assume(function () { + new winston.RejectionHandler(); + }).throws(/Logger is required/); + }); + + it('new RejectionHandler(logger)', function () { + var logger = winston.createLogger(); + var handler = new winston.RejectionHandler(logger); + assume(handler.logger).equals(logger); + }); + + it('.getProcessInfo()', function () { + var handler = helpers.rejectionHandler(); + helpers.assertProcessInfo(handler.getProcessInfo()); + }); + + it('.getOsInfo()', function () { + var handler = helpers.rejectionHandler(); + helpers.assertOsInfo(handler.getOsInfo()); + }); + + it('.getTrace(new Error)', function () { + var handler = helpers.rejectionHandler(); + helpers.assertTrace(handler.getTrace(new Error())); + }); + + it('.getTrace()', function () { + var handler = helpers.rejectionHandler(); + helpers.assertTrace(handler.getTrace()); + }); + + it('.handle()', function (done) { + var existing = helpers.clearRejections(); + var writeable = new stream.Writable({ + objectMode: true, + write: function (info) { + assume(info).is.an('object'); + assume(info.error).is.an('error'); + assume(info.error.message).equals('wtf this rejection'); + assume(info.message).includes('unhandledRejection: wtf this rejection'); + assume(info.stack).is.a('string'); + assume(info.process).is.an('object'); + assume(info.os).is.an('object'); + assume(info.trace).is.an('array'); + + existing.restore(); + done(); + } + }); + + var transport = new winston.transports.Stream({ stream: writeable }); + var handler = helpers.rejectionHandler({ + exitOnError: false, + transports: [transport] + }); + + assume(handler.catcher).equals(undefined); + + transport.handleRejections = true; + handler.handle(); + + assume(handler.catcher).is.a('function'); + assume(process.listeners('unhandledRejection')).deep.equals([ + handler.catcher + ]); + + helpers.reject('wtf this rejection').then(done()); + }); + + after(function () { + // + // Restore normal `runTest` functionality + // so that we only affect the current suite. + // + mocha.Runner.prototype.runTest = _runTest; + }); +});