Skip to content

Commit 0f546fc

Browse files
authored
Refactor checkGlobals() error message creation (#3711)
* Added sQuote() and dQuote() functions for quoting text inline * Added ngettext() function for message translation for dealing with plurality * Refactored portions of code to use the aforementioned functions * Various fixes to tests
1 parent 2d21fd6 commit 0f546fc

File tree

7 files changed

+155
-34
lines changed

7 files changed

+155
-34
lines changed

lib/mocha.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var createInvalidInterfaceError = errors.createInvalidInterfaceError;
2020
var EVENT_FILE_PRE_REQUIRE = Suite.constants.EVENT_FILE_PRE_REQUIRE;
2121
var EVENT_FILE_POST_REQUIRE = Suite.constants.EVENT_FILE_POST_REQUIRE;
2222
var EVENT_FILE_REQUIRE = Suite.constants.EVENT_FILE_REQUIRE;
23+
var sQuote = utils.sQuote;
2324

2425
exports = module.exports = Mocha;
2526

@@ -227,24 +228,23 @@ Mocha.prototype.reporter = function(reporter, reporterOptions) {
227228
} catch (_err) {
228229
_err.code !== 'MODULE_NOT_FOUND' ||
229230
_err.message.indexOf('Cannot find module') !== -1
230-
? console.warn('"' + reporter + '" reporter not found')
231+
? console.warn(sQuote(reporter) + ' reporter not found')
231232
: console.warn(
232-
'"' +
233-
reporter +
234-
'" reporter blew up with error:\n' +
233+
sQuote(reporter) +
234+
' reporter blew up with error:\n' +
235235
err.stack
236236
);
237237
}
238238
} else {
239239
console.warn(
240-
'"' + reporter + '" reporter blew up with error:\n' + err.stack
240+
sQuote(reporter) + ' reporter blew up with error:\n' + err.stack
241241
);
242242
}
243243
}
244244
}
245245
if (!_reporter) {
246246
throw createInvalidReporterError(
247-
'invalid reporter "' + reporter + '"',
247+
'invalid reporter ' + sQuote(reporter),
248248
reporter
249249
);
250250
}
@@ -273,7 +273,7 @@ Mocha.prototype.ui = function(name) {
273273
this._ui = require(name);
274274
} catch (err) {
275275
throw createInvalidInterfaceError(
276-
'invalid interface "' + name + '"',
276+
'invalid interface ' + sQuote(name),
277277
name
278278
);
279279
}

lib/runner.js

+16-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
'use strict';
22

3+
/**
4+
* Module dependencies.
5+
*/
6+
var util = require('util');
37
var EventEmitter = require('events').EventEmitter;
48
var Pending = require('./pending');
59
var utils = require('./utils');
@@ -14,6 +18,9 @@ var HOOK_TYPE_BEFORE_ALL = Suite.constants.HOOK_TYPE_BEFORE_ALL;
1418
var EVENT_ROOT_SUITE_RUN = Suite.constants.EVENT_ROOT_SUITE_RUN;
1519
var STATE_FAILED = Runnable.constants.STATE_FAILED;
1620
var STATE_PASSED = Runnable.constants.STATE_PASSED;
21+
var dQuote = utils.dQuote;
22+
var ngettext = utils.ngettext;
23+
var sQuote = utils.sQuote;
1724
var stackFilter = utils.stackTraceFilter();
1825
var stringify = utils.stringify;
1926
var type = utils.type;
@@ -257,13 +264,14 @@ Runner.prototype.checkGlobals = function(test) {
257264
leaks = filterLeaks(ok, globals);
258265
this._globals = this._globals.concat(leaks);
259266

260-
if (leaks.length > 1) {
261-
this.fail(
262-
test,
263-
new Error('global leaks detected: ' + leaks.join(', ') + '')
267+
if (leaks.length) {
268+
var format = ngettext(
269+
leaks.length,
270+
'global leak detected: %s',
271+
'global leaks detected: %s'
264272
);
265-
} else if (leaks.length) {
266-
this.fail(test, new Error('global leak detected: ' + leaks[0]));
273+
var error = new Error(util.format(format, leaks.map(sQuote).join(', ')));
274+
this.fail(test, error);
267275
}
268276
};
269277

@@ -321,15 +329,15 @@ Runner.prototype.failHook = function(hook, err) {
321329
hook.originalTitle = hook.originalTitle || hook.title;
322330
if (hook.ctx && hook.ctx.currentTest) {
323331
hook.title =
324-
hook.originalTitle + ' for "' + hook.ctx.currentTest.title + '"';
332+
hook.originalTitle + ' for ' + dQuote(hook.ctx.currentTest.title);
325333
} else {
326334
var parentTitle;
327335
if (hook.parent.title) {
328336
parentTitle = hook.parent.title;
329337
} else {
330338
parentTitle = hook.parent.root ? '{root}' : '';
331339
}
332-
hook.title = hook.originalTitle + ' in "' + parentTitle + '"';
340+
hook.title = hook.originalTitle + ' in ' + dQuote(parentTitle);
333341
}
334342

335343
this.fail(hook, err);

lib/utils.js

+77-2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var debug = require('debug')('mocha:watch');
1313
var fs = require('fs');
1414
var glob = require('glob');
1515
var path = require('path');
16+
var util = require('util');
1617
var join = path.join;
1718
var he = require('he');
1819
var errors = require('./errors');
@@ -530,7 +531,7 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
530531
files = glob.sync(filepath);
531532
if (!files.length) {
532533
throw createNoFilesMatchPatternError(
533-
'Cannot find any files matching pattern "' + filepath + '"',
534+
'Cannot find any files matching pattern ' + exports.dQuote(filepath),
534535
filepath
535536
);
536537
}
@@ -564,7 +565,11 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) {
564565
}
565566
if (!extensions) {
566567
throw createMissingArgumentError(
567-
'Argument "extensions" required when argument "filepath" is a directory',
568+
util.format(
569+
'Argument %s required when argument %s is a directory',
570+
exports.sQuote('extensions'),
571+
exports.sQuote('filepath')
572+
),
568573
'extensions',
569574
'array'
570575
);
@@ -715,6 +720,76 @@ exports.clamp = function clamp(value, range) {
715720
return Math.min(Math.max(value, range[0]), range[1]);
716721
};
717722

723+
/**
724+
* Single quote text by combining with undirectional ASCII quotation marks.
725+
*
726+
* @description
727+
* Provides a simple means of markup for quoting text to be used in output.
728+
* Use this to quote names of variables, methods, and packages.
729+
*
730+
* <samp>package 'foo' cannot be found</samp>
731+
*
732+
* @private
733+
* @param {string} str - Value to be quoted.
734+
* @returns {string} quoted value
735+
* @example
736+
* sQuote('n') // => 'n'
737+
*/
738+
exports.sQuote = function(str) {
739+
return "'" + str + "'";
740+
};
741+
742+
/**
743+
* Double quote text by combining with undirectional ASCII quotation marks.
744+
*
745+
* @description
746+
* Provides a simple means of markup for quoting text to be used in output.
747+
* Use this to quote names of datatypes, classes, pathnames, and strings.
748+
*
749+
* <samp>argument 'value' must be "string" or "number"</samp>
750+
*
751+
* @private
752+
* @param {string} str - Value to be quoted.
753+
* @returns {string} quoted value
754+
* @example
755+
* dQuote('number') // => "number"
756+
*/
757+
exports.dQuote = function(str) {
758+
return '"' + str + '"';
759+
};
760+
761+
/**
762+
* Provides simplistic message translation for dealing with plurality.
763+
*
764+
* @description
765+
* Use this to create messages which need to be singular or plural.
766+
* Some languages have several plural forms, so _complete_ message clauses
767+
* are preferable to generating the message on the fly.
768+
*
769+
* @private
770+
* @param {number} n - Non-negative integer
771+
* @param {string} msg1 - Message to be used in English for `n = 1`
772+
* @param {string} msg2 - Message to be used in English for `n = 0, 2, 3, ...`
773+
* @returns {string} message corresponding to value of `n`
774+
* @example
775+
* var sprintf = require('util').format;
776+
* var pkgs = ['one', 'two'];
777+
* var msg = sprintf(
778+
* ngettext(
779+
* pkgs.length,
780+
* 'cannot load package: %s',
781+
* 'cannot load packages: %s'
782+
* ),
783+
* pkgs.map(sQuote).join(', ')
784+
* );
785+
* console.log(msg); // => cannot load packages: 'one', 'two'
786+
*/
787+
exports.ngettext = function(n, msg1, msg2) {
788+
if (typeof n === 'number' && n >= 0) {
789+
return n === 1 ? msg1 : msg2;
790+
}
791+
};
792+
718793
/**
719794
* It's a noop.
720795
* @public

test/integration/reporters.spec.js

+3-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ var fs = require('fs');
55
var crypto = require('crypto');
66
var path = require('path');
77
var run = require('./helpers').runMocha;
8+
var utils = require('../../lib/utils');
9+
var dQuote = utils.dQuote;
810

911
describe('reporters', function() {
1012
describe('markdown', function() {
@@ -213,13 +215,9 @@ describe('reporters', function() {
213215
return;
214216
}
215217

216-
function dquote(s) {
217-
return '"' + s + '"';
218-
}
219-
220218
var pattern =
221219
'^Error: invalid or unsupported TAP version: ' +
222-
dquote(invalidTapVersion);
220+
dQuote(invalidTapVersion);
223221
expect(res, 'to satisfy', {
224222
code: 1,
225223
output: new RegExp(pattern, 'm')

test/unit/mocha.spec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ describe('Mocha', function() {
234234
new Mocha(updatedOpts);
235235
};
236236
expect(throwError, 'to throw', {
237-
message: 'invalid reporter "invalidReporter"',
237+
message: "invalid reporter 'invalidReporter'",
238238
code: 'ERR_MOCHA_INVALID_REPORTER',
239239
reporter: 'invalidReporter'
240240
});

test/unit/runner.spec.js

+12-11
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ describe('Runner', function() {
117117
global.foo = 'bar';
118118
runner.on(EVENT_TEST_FAIL, function(_test, _err) {
119119
expect(_test, 'to be', test);
120-
expect(_err, 'to have message', 'global leak detected: foo');
120+
expect(_err, 'to have message', "global leak detected: 'foo'");
121121
delete global.foo;
122122
done();
123123
});
@@ -171,7 +171,7 @@ describe('Runner', function() {
171171
global.bar = 'baz';
172172
runner.on(EVENT_TEST_FAIL, function(_test, _err) {
173173
expect(_test, 'to be', test);
174-
expect(_err, 'to have message', 'global leaks detected: foo, bar');
174+
expect(_err, 'to have message', "global leaks detected: 'foo', 'bar'");
175175
delete global.foo;
176176
delete global.bar;
177177
done();
@@ -201,22 +201,23 @@ describe('Runner', function() {
201201

202202
suite.addTest(test);
203203

204-
global.foo = 'bar';
205-
global.bar = 'baz';
204+
global.foo = 'whitelisted';
205+
global.bar = 'detect-me';
206206
runner.on(EVENT_TEST_FAIL, function(_test, _err) {
207207
expect(_test.title, 'to be', 'im a test about lions');
208-
expect(_err, 'to have message', 'global leak detected: bar');
208+
expect(_err, 'to have message', "global leak detected: 'bar'");
209209
delete global.foo;
210+
delete global.bar;
210211
done();
211212
});
212213
runner.checkGlobals(test);
213214
});
214215

215-
it('should emit "fail" when a global beginning with d is introduced', function(done) {
216+
it('should emit "fail" when a global beginning with "d" is introduced', function(done) {
216217
global.derp = 'bar';
217-
runner.on(EVENT_TEST_FAIL, function(test, err) {
218-
expect(test.title, 'to be', 'herp');
219-
expect(err.message, 'to be', 'global leak detected: derp');
218+
runner.on(EVENT_TEST_FAIL, function(_test, _err) {
219+
expect(_test.title, 'to be', 'herp');
220+
expect(_err, 'to have message', "global leak detected: 'derp'");
220221
delete global.derp;
221222
done();
222223
});
@@ -569,7 +570,7 @@ describe('Runner', function() {
569570
// Fake stack-trace
570571
err.stack = [message].concat(stack).join('\n');
571572

572-
runner.on('fail', function(_hook, _err) {
573+
runner.on(EVENT_TEST_FAIL, function(_hook, _err) {
573574
var filteredErrStack = _err.stack.split('\n').slice(1);
574575
expect(
575576
filteredErrStack.join('\n'),
@@ -589,7 +590,7 @@ describe('Runner', function() {
589590
// Fake stack-trace
590591
err.stack = [message].concat(stack).join('\n');
591592

592-
runner.on('fail', function(_hook, _err) {
593+
runner.on(EVENT_TEST_FAIL, function(_hook, _err) {
593594
var filteredErrStack = _err.stack.split('\n').slice(-3);
594595
expect(
595596
filteredErrStack.join('\n'),

test/unit/utils.spec.js

+39
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,45 @@ describe('lib/utils', function() {
706706
});
707707
});
708708

709+
describe('sQuote/dQuote', function() {
710+
var str = 'xxx';
711+
712+
it('should return its input as string wrapped in single quotes', function() {
713+
var expected = "'xxx'";
714+
expect(utils.sQuote(str), 'to be', expected);
715+
});
716+
717+
it('should return its input as string wrapped in double quotes', function() {
718+
var expected = '"xxx"';
719+
expect(utils.dQuote(str), 'to be', expected);
720+
});
721+
});
722+
723+
describe('ngettext', function() {
724+
var singular = 'singular';
725+
var plural = 'plural';
726+
727+
it("should return plural string if 'n' is 0", function() {
728+
expect(utils.ngettext(0, singular, plural), 'to be', plural);
729+
});
730+
731+
it("should return singular string if 'n' is 1", function() {
732+
expect(utils.ngettext(1, singular, plural), 'to be', singular);
733+
});
734+
735+
it("should return plural string if 'n' is greater than 1", function() {
736+
var arr = ['aaa', 'bbb'];
737+
expect(utils.ngettext(arr.length, singular, plural), 'to be', plural);
738+
});
739+
740+
it("should return undefined if 'n' is not a non-negative integer", function() {
741+
expect(utils.ngettext('', singular, plural), 'to be undefined');
742+
expect(utils.ngettext(-1, singular, plural), 'to be undefined');
743+
expect(utils.ngettext(true, singular, plural), 'to be undefined');
744+
expect(utils.ngettext({}, singular, plural), 'to be undefined');
745+
});
746+
});
747+
709748
describe('createMap', function() {
710749
it('should return an object with a null prototype', function() {
711750
expect(Object.getPrototypeOf(utils.createMap()), 'to be', null);

0 commit comments

Comments
 (0)