Skip to content

Commit 792292a

Browse files
boneskullcraigtaub
authored andcommitted
multiple async done() calls result in failure; closes #4151 (#4152)
- added a method in `errors` module to create a "multiple done" err - modernize `multiple-done.spec.js` - refactor errors into constants in `errors` module - remove `Runner#started` prop; replace with `Runner#state` prop + constants - add a catchall `createFatalError()` function to `errors` module; this is called when a test fails twice by other means (unsure what those means are yet) - force color in Travis CI b/c my eyes - remove `Runner#uncaughtEnd`; move logic to `Runner#uncaught`, since we can now rely on the value of `Runner#state`. - upgrade `unexpected-eventemitter` - fix potential listener leak in `Runner#run`
1 parent 5fd44cc commit 792292a

13 files changed

+518
-829
lines changed

lib/cli/collect-files.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const ansi = require('ansi-colors');
55
const debug = require('debug')('mocha:cli:run:helpers');
66
const minimatch = require('minimatch');
77
const utils = require('../utils');
8+
const {NO_FILES_MATCH_PATTERN} = require('../errors').constants;
89

910
/**
1011
* Exports a function that collects test files from CLI parameters.
@@ -34,7 +35,7 @@ module.exports = ({ignore, extension, file, recursive, sort, spec} = {}) => {
3435
try {
3536
newFiles = utils.lookupFiles(arg, extension, recursive);
3637
} catch (err) {
37-
if (err.code === 'ERR_MOCHA_NO_FILES_MATCH_PATTERN') {
38+
if (err.code === NO_FILES_MATCH_PATTERN) {
3839
unmatched.push({message: err.message, pattern: err.pattern});
3940
return;
4041
}

lib/errors.js

Lines changed: 129 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,73 @@
11
'use strict';
2+
3+
var format = require('util').format;
4+
25
/**
6+
* Factory functions to create throwable error objects
37
* @module Errors
48
*/
9+
510
/**
6-
* Factory functions to create throwable error objects
11+
* When Mocha throw exceptions (or otherwise errors), it attempts to assign a
12+
* `code` property to the `Error` object, for easier handling. These are the
13+
* potential values of `code`.
714
*/
15+
var constants = {
16+
/**
17+
* An unrecoverable error.
18+
*/
19+
FATAL: 'ERR_MOCHA_FATAL',
20+
21+
/**
22+
* The type of an argument to a function call is invalid
23+
*/
24+
INVALID_ARG_TYPE: 'ERR_MOCHA_INVALID_ARG_TYPE',
25+
26+
/**
27+
* The value of an argument to a function call is invalid
28+
*/
29+
INVALID_ARG_VALUE: 'ERR_MOCHA_INVALID_ARG_VALUE',
30+
31+
/**
32+
* Something was thrown, but it wasn't an `Error`
33+
*/
34+
INVALID_EXCEPTION: 'ERR_MOCHA_INVALID_EXCEPTION',
35+
36+
/**
37+
* An interface (e.g., `Mocha.interfaces`) is unknown or invalid
38+
*/
39+
INVALID_INTERFACE: 'ERR_MOCHA_INVALID_INTERFACE',
40+
41+
/**
42+
* A reporter (.e.g, `Mocha.reporters`) is unknown or invalid
43+
*/
44+
INVALID_REPORTER: 'ERR_MOCHA_INVALID_REPORTER',
45+
46+
/**
47+
* `done()` was called twice in a `Test` or `Hook` callback
48+
*/
49+
MULTIPLE_DONE: 'ERR_MOCHA_MULTIPLE_DONE',
50+
51+
/**
52+
* No files matched the pattern provided by the user
53+
*/
54+
NO_FILES_MATCH_PATTERN: 'ERR_MOCHA_NO_FILES_MATCH_PATTERN',
55+
56+
/**
57+
* Known, but unsupported behavior of some kind
58+
*/
59+
UNSUPPORTED: 'ERR_MOCHA_UNSUPPORTED',
60+
61+
/**
62+
* Invalid state transition occuring in `Mocha` instance
63+
*/
64+
INSTANCE_ALREADY_RUNNING: 'ERR_MOCHA_INSTANCE_ALREADY_RUNNING',
65+
66+
/**
67+
* Invalid state transition occuring in `Mocha` instance
68+
*/
69+
INSTANCE_ALREADY_DISPOSED: 'ERR_MOCHA_INSTANCE_ALREADY_DISPOSED'
70+
};
871

972
/**
1073
* Creates an error object to be thrown when no files to be tested could be found using specified pattern.
@@ -16,7 +79,7 @@
1679
*/
1780
function createNoFilesMatchPatternError(message, pattern) {
1881
var err = new Error(message);
19-
err.code = 'ERR_MOCHA_NO_FILES_MATCH_PATTERN';
82+
err.code = constants.NO_FILES_MATCH_PATTERN;
2083
err.pattern = pattern;
2184
return err;
2285
}
@@ -31,7 +94,7 @@ function createNoFilesMatchPatternError(message, pattern) {
3194
*/
3295
function createInvalidReporterError(message, reporter) {
3396
var err = new TypeError(message);
34-
err.code = 'ERR_MOCHA_INVALID_REPORTER';
97+
err.code = constants.INVALID_REPORTER;
3598
err.reporter = reporter;
3699
return err;
37100
}
@@ -46,7 +109,7 @@ function createInvalidReporterError(message, reporter) {
46109
*/
47110
function createInvalidInterfaceError(message, ui) {
48111
var err = new Error(message);
49-
err.code = 'ERR_MOCHA_INVALID_INTERFACE';
112+
err.code = constants.INVALID_INTERFACE;
50113
err.interface = ui;
51114
return err;
52115
}
@@ -60,7 +123,7 @@ function createInvalidInterfaceError(message, ui) {
60123
*/
61124
function createUnsupportedError(message) {
62125
var err = new Error(message);
63-
err.code = 'ERR_MOCHA_UNSUPPORTED';
126+
err.code = constants.UNSUPPORTED;
64127
return err;
65128
}
66129

@@ -88,7 +151,7 @@ function createMissingArgumentError(message, argument, expected) {
88151
*/
89152
function createInvalidArgumentTypeError(message, argument, expected) {
90153
var err = new TypeError(message);
91-
err.code = 'ERR_MOCHA_INVALID_ARG_TYPE';
154+
err.code = constants.INVALID_ARG_TYPE;
92155
err.argument = argument;
93156
err.expected = expected;
94157
err.actual = typeof argument;
@@ -107,7 +170,7 @@ function createInvalidArgumentTypeError(message, argument, expected) {
107170
*/
108171
function createInvalidArgumentValueError(message, argument, value, reason) {
109172
var err = new TypeError(message);
110-
err.code = 'ERR_MOCHA_INVALID_ARG_VALUE';
173+
err.code = constants.INVALID_ARG_VALUE;
111174
err.argument = argument;
112175
err.value = value;
113176
err.reason = typeof reason !== 'undefined' ? reason : 'is invalid';
@@ -123,7 +186,22 @@ function createInvalidArgumentValueError(message, argument, value, reason) {
123186
*/
124187
function createInvalidExceptionError(message, value) {
125188
var err = new Error(message);
126-
err.code = 'ERR_MOCHA_INVALID_EXCEPTION';
189+
err.code = constants.INVALID_EXCEPTION;
190+
err.valueType = typeof value;
191+
err.value = value;
192+
return err;
193+
}
194+
195+
/**
196+
* Creates an error object to be thrown when an unrecoverable error occurs.
197+
*
198+
* @public
199+
* @param {string} message - Error message to be displayed.
200+
* @returns {Error} instance detailing the error condition
201+
*/
202+
function createFatalError(message, value) {
203+
var err = new Error(message);
204+
err.code = constants.FATAL;
127205
err.valueType = typeof value;
128206
err.value = value;
129207
return err;
@@ -161,7 +239,7 @@ function createMochaInstanceAlreadyDisposedError(
161239
instance
162240
) {
163241
var err = new Error(message);
164-
err.code = 'ERR_MOCHA_INSTANCE_ALREADY_DISPOSED';
242+
err.code = constants.INSTANCE_ALREADY_DISPOSED;
165243
err.cleanReferencesAfterRun = cleanReferencesAfterRun;
166244
err.instance = instance;
167245
return err;
@@ -173,11 +251,48 @@ function createMochaInstanceAlreadyDisposedError(
173251
*/
174252
function createMochaInstanceAlreadyRunningError(message, instance) {
175253
var err = new Error(message);
176-
err.code = 'ERR_MOCHA_INSTANCE_ALREADY_RUNNING';
254+
err.code = constants.INSTANCE_ALREADY_RUNNING;
177255
err.instance = instance;
178256
return err;
179257
}
180258

259+
/*
260+
* Creates an error object to be thrown when done() is called multiple times in a test
261+
*
262+
* @public
263+
* @param {Runnable} runnable - Original runnable
264+
* @param {Error} [originalErr] - Original error, if any
265+
* @returns {Error} instance detailing the error condition
266+
*/
267+
function createMultipleDoneError(runnable, originalErr) {
268+
var title;
269+
try {
270+
title = format('<%s>', runnable.fullTitle());
271+
if (runnable.parent.root) {
272+
title += ' (of root suite)';
273+
}
274+
} catch (ignored) {
275+
title = format('<%s> (of unknown suite)', runnable.title);
276+
}
277+
var message = format(
278+
'done() called multiple times in %s %s',
279+
runnable.type ? runnable.type : 'unknown runnable',
280+
title
281+
);
282+
if (runnable.file) {
283+
message += format(' of file %s', runnable.file);
284+
}
285+
if (originalErr) {
286+
message += format('; in addition, done() received error: %s', originalErr);
287+
}
288+
289+
var err = new Error(message);
290+
err.code = constants.MULTIPLE_DONE;
291+
err.valueType = typeof originalErr;
292+
err.value = originalErr;
293+
return err;
294+
}
295+
181296
module.exports = {
182297
createInvalidArgumentTypeError: createInvalidArgumentTypeError,
183298
createInvalidArgumentValueError: createInvalidArgumentValueError,
@@ -189,5 +304,8 @@ module.exports = {
189304
createUnsupportedError: createUnsupportedError,
190305
createInvalidPluginError: createInvalidPluginError,
191306
createMochaInstanceAlreadyDisposedError: createMochaInstanceAlreadyDisposedError,
192-
createMochaInstanceAlreadyRunningError: createMochaInstanceAlreadyRunningError
307+
createMochaInstanceAlreadyRunningError: createMochaInstanceAlreadyRunningError,
308+
createFatalError: createFatalError,
309+
createMultipleDoneError: createMultipleDoneError,
310+
constants: constants
193311
};

lib/runnable.js

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@ var Pending = require('./pending');
55
var debug = require('debug')('mocha:runnable');
66
var milliseconds = require('ms');
77
var utils = require('./utils');
8-
var createInvalidExceptionError = require('./errors')
9-
.createInvalidExceptionError;
8+
var errors = require('./errors');
9+
var createInvalidExceptionError = errors.createInvalidExceptionError;
10+
var createMultipleDoneError = errors.createMultipleDoneError;
1011

1112
/**
1213
* Save timer references to avoid Sinon interfering (see GH-237).
@@ -276,7 +277,7 @@ Runnable.prototype.run = function(fn) {
276277
var start = new Date();
277278
var ctx = this.ctx;
278279
var finished;
279-
var emitted;
280+
var errorWasHandled = false;
280281

281282
// Sometimes the ctx exists, but it is not runnable
282283
if (ctx && ctx.runnable) {
@@ -285,17 +286,11 @@ Runnable.prototype.run = function(fn) {
285286

286287
// called multiple times
287288
function multiple(err) {
288-
if (emitted) {
289+
if (errorWasHandled) {
289290
return;
290291
}
291-
emitted = true;
292-
var msg = 'done() called multiple times';
293-
if (err && err.message) {
294-
err.message += " (and Mocha's " + msg + ')';
295-
self.emit('error', err);
296-
} else {
297-
self.emit('error', new Error(msg));
298-
}
292+
errorWasHandled = true;
293+
self.emit('error', createMultipleDoneError(self, err));
299294
}
300295

301296
// finished
@@ -346,7 +341,7 @@ Runnable.prototype.run = function(fn) {
346341
callFnAsync(this.fn);
347342
} catch (err) {
348343
// handles async runnables which actually run synchronously
349-
emitted = true;
344+
errorWasHandled = true;
350345
if (err instanceof Pending) {
351346
return; // done() is already called in this.skip()
352347
} else if (this.allowUncaught) {
@@ -365,7 +360,7 @@ Runnable.prototype.run = function(fn) {
365360
callFn(this.fn);
366361
}
367362
} catch (err) {
368-
emitted = true;
363+
errorWasHandled = true;
369364
if (err instanceof Pending) {
370365
return done();
371366
} else if (this.allowUncaught) {

0 commit comments

Comments
 (0)