From 02400bc31d804dcf9b7397ea2216c14b79461dfd Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Thu, 12 Nov 2020 11:46:02 -0800 Subject: [PATCH] add error code for test timeout errors other: - addresses some issues with API documentation of errors, featuring an example of how to properly document the `Error` object returned by the `create*` functions in `lib/errors.js` - adds a missing test for the `id` prop of `Runnable` --- lib/errors.js | 95 ++++++++++++++++++++++++++++++++++++-- lib/runnable.js | 15 +++--- test/unit/runnable.spec.js | 32 ++++++++++--- 3 files changed, 122 insertions(+), 20 deletions(-) diff --git a/lib/errors.js b/lib/errors.js index 29f76c61b5..4289676edc 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -17,6 +17,7 @@ const emitWarning = (msg, type) => { if (process.emitWarning) { process.emitWarning(msg, type); } else { + /* istanbul ignore next */ process.nextTick(function() { console.warn(type + ': ' + msg); }); @@ -53,88 +54,129 @@ const warn = msg => { }; /** - * When Mocha throw exceptions (or otherwise errors), it attempts to assign a - * `code` property to the `Error` object, for easier handling. These are the - * potential values of `code`. + * When Mocha throws exceptions (or rejects `Promise`s), it attempts to assign a `code` property to the `Error` object, for easier handling. These are the potential values of `code`. + * @public + * @namespace + * @memberof module:lib/errors */ var constants = { /** * An unrecoverable error. + * @constant + * @default */ FATAL: 'ERR_MOCHA_FATAL', /** * The type of an argument to a function call is invalid + * @constant + * @default */ INVALID_ARG_TYPE: 'ERR_MOCHA_INVALID_ARG_TYPE', /** * The value of an argument to a function call is invalid + * @constant + * @default */ INVALID_ARG_VALUE: 'ERR_MOCHA_INVALID_ARG_VALUE', /** * Something was thrown, but it wasn't an `Error` + * @constant + * @default */ INVALID_EXCEPTION: 'ERR_MOCHA_INVALID_EXCEPTION', /** * An interface (e.g., `Mocha.interfaces`) is unknown or invalid + * @constant + * @default */ INVALID_INTERFACE: 'ERR_MOCHA_INVALID_INTERFACE', /** * A reporter (.e.g, `Mocha.reporters`) is unknown or invalid + * @constant + * @default */ INVALID_REPORTER: 'ERR_MOCHA_INVALID_REPORTER', /** * `done()` was called twice in a `Test` or `Hook` callback + * @constant + * @default */ MULTIPLE_DONE: 'ERR_MOCHA_MULTIPLE_DONE', /** * No files matched the pattern provided by the user + * @constant + * @default */ NO_FILES_MATCH_PATTERN: 'ERR_MOCHA_NO_FILES_MATCH_PATTERN', /** * Known, but unsupported behavior of some kind + * @constant + * @default */ UNSUPPORTED: 'ERR_MOCHA_UNSUPPORTED', /** * Invalid state transition occurring in `Mocha` instance + * @constant + * @default */ INSTANCE_ALREADY_RUNNING: 'ERR_MOCHA_INSTANCE_ALREADY_RUNNING', /** * Invalid state transition occurring in `Mocha` instance + * @constant + * @default */ INSTANCE_ALREADY_DISPOSED: 'ERR_MOCHA_INSTANCE_ALREADY_DISPOSED', /** * Use of `only()` w/ `--forbid-only` results in this error. + * @constant + * @default */ FORBIDDEN_EXCLUSIVITY: 'ERR_MOCHA_FORBIDDEN_EXCLUSIVITY', /** * To be thrown when a user-defined plugin implementation (e.g., `mochaHooks`) is invalid + * @constant + * @default */ INVALID_PLUGIN_IMPLEMENTATION: 'ERR_MOCHA_INVALID_PLUGIN_IMPLEMENTATION', /** * To be thrown when a builtin or third-party plugin definition (the _definition_ of `mochaHooks`) is invalid + * @constant + * @default */ - INVALID_PLUGIN_DEFINITION: 'ERR_MOCHA_INVALID_PLUGIN_DEFINITION' + INVALID_PLUGIN_DEFINITION: 'ERR_MOCHA_INVALID_PLUGIN_DEFINITION', + + /** + * When a runnable exceeds its allowed run time. + * @constant + * @default + */ + TIMEOUT: 'ERR_MOCHA_TIMEOUT' }; +/** + * A set containing all string values of all Mocha error constants, for use by {@link isMochaError}. + * @private + */ const MOCHA_ERRORS = new Set(Object.values(constants)); /** * Creates an error object to be thrown when no files to be tested could be found using specified pattern. * * @public + * @static * @param {string} message - Error message to be displayed. * @param {string} pattern - User-specified argument value. * @returns {Error} instance detailing the error condition @@ -165,6 +207,7 @@ function createInvalidReporterError(message, reporter) { * Creates an error object to be thrown when the interface specified in the options was not found. * * @public + * @static * @param {string} message - Error message to be displayed. * @param {string} ui - User-specified interface value. * @returns {Error} instance detailing the error condition @@ -180,6 +223,7 @@ function createInvalidInterfaceError(message, ui) { * Creates an error object to be thrown when a behavior, option, or parameter is unsupported. * * @public + * @static * @param {string} message - Error message to be displayed. * @returns {Error} instance detailing the error condition */ @@ -193,6 +237,7 @@ function createUnsupportedError(message) { * Creates an error object to be thrown when an argument is missing. * * @public + * @static * @param {string} message - Error message to be displayed. * @param {string} argument - Argument name. * @param {string} expected - Expected argument datatype. @@ -206,6 +251,7 @@ function createMissingArgumentError(message, argument, expected) { * Creates an error object to be thrown when an argument did not use the supported type * * @public + * @static * @param {string} message - Error message to be displayed. * @param {string} argument - Argument name. * @param {string} expected - Expected argument datatype. @@ -224,6 +270,7 @@ function createInvalidArgumentTypeError(message, argument, expected) { * Creates an error object to be thrown when an argument did not use the supported value * * @public + * @static * @param {string} message - Error message to be displayed. * @param {string} argument - Argument name. * @param {string} value - Argument value. @@ -243,6 +290,7 @@ function createInvalidArgumentValueError(message, argument, value, reason) { * Creates an error object to be thrown when an exception was caught, but the `Error` is falsy or undefined. * * @public + * @static * @param {string} message - Error message to be displayed. * @returns {Error} instance detailing the error condition */ @@ -258,6 +306,7 @@ function createInvalidExceptionError(message, value) { * Creates an error object to be thrown when an unrecoverable error occurs. * * @public + * @static * @param {string} message - Error message to be displayed. * @returns {Error} instance detailing the error condition */ @@ -276,6 +325,7 @@ function createFatalError(message, value) { * @param {string} [pluginId] - Name/path of plugin, if any * @throws When `pluginType` is not known * @public + * @static * @returns {Error} */ function createInvalidLegacyPluginError(message, pluginType, pluginId) { @@ -297,6 +347,7 @@ function createInvalidLegacyPluginError(message, pluginType, pluginId) { * @param {string} [pluginId] - Name/path of plugin, if any * @throws When `pluginType` is not known * @public + * @static * @returns {Error} */ function createInvalidPluginError(...args) { @@ -309,6 +360,7 @@ function createInvalidPluginError(...args) { * @param {string} message The error message to be displayed. * @param {boolean} cleanReferencesAfterRun the value of `cleanReferencesAfterRun` * @param {Mocha} instance the mocha instance that throw this error + * @static */ function createMochaInstanceAlreadyDisposedError( message, @@ -325,6 +377,8 @@ function createMochaInstanceAlreadyDisposedError( /** * Creates an error object to be thrown when a mocha object's `run` method is called while a test run is in progress. * @param {string} message The error message to be displayed. + * @static + * @public */ function createMochaInstanceAlreadyRunningError(message, instance) { var err = new Error(message); @@ -333,13 +387,14 @@ function createMochaInstanceAlreadyRunningError(message, instance) { return err; } -/* +/** * Creates an error object to be thrown when done() is called multiple times in a test * * @public * @param {Runnable} runnable - Original runnable * @param {Error} [originalErr] - Original error, if any * @returns {Error} instance detailing the error condition + * @static */ function createMultipleDoneError(runnable, originalErr) { var title; @@ -373,6 +428,7 @@ function createMultipleDoneError(runnable, originalErr) { /** * Creates an error object to be thrown when `.only()` is used with * `--forbid-only`. + * @static * @public * @param {Mocha} mocha - Mocha instance * @returns {Error} Error with code {@link constants.FORBIDDEN_EXCLUSIVITY} @@ -389,6 +445,7 @@ function createForbiddenExclusivityError(mocha) { /** * Creates an error object to be thrown when a plugin definition is invalid + * @static * @param {string} msg - Error message * @param {PluginDefinition} [pluginDef] - Problematic plugin definition * @public @@ -403,6 +460,7 @@ function createInvalidPluginDefinitionError(msg, pluginDef) { /** * Creates an error object to be thrown when a plugin implementation (user code) is invalid + * @static * @param {string} msg - Error message * @param {Object} [opts] - Plugin definition and user-supplied implementation * @param {PluginDefinition} [opts.pluginDef] - Plugin Definition @@ -421,9 +479,26 @@ function createInvalidPluginImplementationError( return err; } +/** + * Creates an error object to be thrown when a runnable exceeds its allowed run time. + * @static + * @param {string} msg - Error message + * @param {number} [timeout] - Timeout in ms + * @param {string} [file] - File, if given + * @returns {MochaTimeoutError} + */ +function createTimeoutError(msg, timeout, file) { + const err = new Error(msg); + err.code = constants.TIMEOUT; + err.timeout = timeout; + err.file = file; + return err; +} + /** * Returns `true` if an error came out of Mocha. * _Can suffer from false negatives, but not false positives._ + * @static * @public * @param {*} err - Error, or anything * @returns {boolean} @@ -449,8 +524,18 @@ module.exports = { createMochaInstanceAlreadyRunningError, createMultipleDoneError, createNoFilesMatchPatternError, + createTimeoutError, createUnsupportedError, deprecate, isMochaError, warn }; + +/** + * The error thrown when a Runnable times out + * @memberof module:lib/errors + * @typedef {Error} MochaTimeoutError + * @property {constants.TIMEOUT} code - Error code + * @property {number?} timeout Timeout in ms + * @property {string?} file Filepath, if given + */ diff --git a/lib/runnable.js b/lib/runnable.js index e65b4c4638..3b59680e6a 100644 --- a/lib/runnable.js +++ b/lib/runnable.js @@ -5,9 +5,11 @@ var Pending = require('./pending'); var debug = require('debug')('mocha:runnable'); var milliseconds = require('ms'); var utils = require('./utils'); -var errors = require('./errors'); -var createInvalidExceptionError = errors.createInvalidExceptionError; -var createMultipleDoneError = errors.createMultipleDoneError; +const { + createInvalidExceptionError, + createMultipleDoneError, + createTimeoutError +} = require('./errors'); /** * Save timer references to avoid Sinon interfering (see GH-237). @@ -422,14 +424,11 @@ Runnable.prototype.run = function(fn) { * @private */ Runnable.prototype._timeoutError = function(ms) { - var msg = - 'Timeout of ' + - ms + - 'ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.'; + let msg = `Timeout of ${ms}ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves.`; if (this.file) { msg += ' (' + this.file + ')'; } - return new Error(msg); + return createTimeoutError(msg, ms, this.file); }; var constants = utils.defineConstants( diff --git a/test/unit/runnable.spec.js b/test/unit/runnable.spec.js index 4f00dadfd7..d4975d4ffe 100644 --- a/test/unit/runnable.spec.js +++ b/test/unit/runnable.spec.js @@ -4,6 +4,7 @@ var Mocha = require('../../lib/mocha'); var Runnable = Mocha.Runnable; var Suite = Mocha.Suite; var sinon = require('sinon'); +const {TIMEOUT} = require('../../lib/errors').constants; var STATE_FAILED = Runnable.constants.STATE_FAILED; describe('Runnable(title, fn)', function() { @@ -476,7 +477,14 @@ describe('Runnable(title, fn)', function() { }); }); - it('should allow a timeout of 0'); + it('should allow a timeout of 0', function(done) { + const runnable = new Runnable('foo', () => {}); + runnable.timeout(0); + runnable.run(err => { + expect(err, 'to be falsy'); + done(); + }); + }); }); describe('when fn returns a promise', function() { @@ -577,11 +585,7 @@ describe('Runnable(title, fn)', function() { runnable.timeout(10); runnable.run(function(err) { - expect( - err.message, - 'to match', - /Timeout of 10ms exceeded.*\(\/some\/path\)$/ - ); + expect(err, 'to satisfy', {code: TIMEOUT, timeout: 10}); done(); }); }); @@ -607,7 +611,7 @@ describe('Runnable(title, fn)', function() { }); runnable.timeout(10); runnable.run(function(err) { - expect(err.message, 'to match', /^Timeout of 10ms/); + expect(err, 'to satisfy', {code: TIMEOUT, timeout: 10}); // timedOut is set *after* this callback is executed process.nextTick(function() { expect(runnable.timedOut, 'to be truthy'); @@ -635,6 +639,7 @@ describe('Runnable(title, fn)', function() { var runnable = new Runnable('foo', function(done) { // normally "this" but it gets around having to muck with a context runnable.skip(); + /* istanbul ignore next */ aborted = false; }); runnable.run(function() { @@ -715,4 +720,17 @@ describe('Runnable(title, fn)', function() { }); }); }); + + describe('interesting property', function() { + describe('id', function() { + it('should have a unique identifier', function() { + expect(new Runnable('foo', () => {}), 'to have property', 'id'); + }); + + it('should have a permanent identifier', function() { + const runnable = new Runnable('foo', () => {}); + expect(runnable.id, 'to be', runnable.id); + }); + }); + }); });