diff --git a/lib/mocha.js b/lib/mocha.js index 9886ded56d..1537196167 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -20,6 +20,7 @@ var createInvalidInterfaceError = errors.createInvalidInterfaceError; var EVENT_FILE_PRE_REQUIRE = Suite.constants.EVENT_FILE_PRE_REQUIRE; var EVENT_FILE_POST_REQUIRE = Suite.constants.EVENT_FILE_POST_REQUIRE; var EVENT_FILE_REQUIRE = Suite.constants.EVENT_FILE_REQUIRE; +var sQuote = utils.sQuote; exports = module.exports = Mocha; @@ -227,24 +228,23 @@ Mocha.prototype.reporter = function(reporter, reporterOptions) { } catch (_err) { _err.code !== 'MODULE_NOT_FOUND' || _err.message.indexOf('Cannot find module') !== -1 - ? console.warn('"' + reporter + '" reporter not found') + ? console.warn(sQuote(reporter) + ' reporter not found') : console.warn( - '"' + - reporter + - '" reporter blew up with error:\n' + + sQuote(reporter) + + ' reporter blew up with error:\n' + err.stack ); } } else { console.warn( - '"' + reporter + '" reporter blew up with error:\n' + err.stack + sQuote(reporter) + ' reporter blew up with error:\n' + err.stack ); } } } if (!_reporter) { throw createInvalidReporterError( - 'invalid reporter "' + reporter + '"', + 'invalid reporter ' + sQuote(reporter), reporter ); } @@ -273,7 +273,7 @@ Mocha.prototype.ui = function(name) { this._ui = require(name); } catch (err) { throw createInvalidInterfaceError( - 'invalid interface "' + name + '"', + 'invalid interface ' + sQuote(name), name ); } diff --git a/lib/runner.js b/lib/runner.js index 6952efa92c..74d8e787e2 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -1,5 +1,9 @@ 'use strict'; +/** + * Module dependencies. + */ +var util = require('util'); var EventEmitter = require('events').EventEmitter; var Pending = require('./pending'); var utils = require('./utils'); @@ -14,6 +18,9 @@ var HOOK_TYPE_BEFORE_ALL = Suite.constants.HOOK_TYPE_BEFORE_ALL; var EVENT_ROOT_SUITE_RUN = Suite.constants.EVENT_ROOT_SUITE_RUN; var STATE_FAILED = Runnable.constants.STATE_FAILED; var STATE_PASSED = Runnable.constants.STATE_PASSED; +var dQuote = utils.dQuote; +var ngettext = utils.ngettext; +var sQuote = utils.sQuote; var stackFilter = utils.stackTraceFilter(); var stringify = utils.stringify; var type = utils.type; @@ -256,13 +263,14 @@ Runner.prototype.checkGlobals = function(test) { leaks = filterLeaks(ok, globals); this._globals = this._globals.concat(leaks); - if (leaks.length > 1) { - this.fail( - test, - new Error('global leaks detected: ' + leaks.join(', ') + '') + if (leaks.length) { + var format = ngettext( + leaks.length, + 'global leak detected: %s', + 'global leaks detected: %s' ); - } else if (leaks.length) { - this.fail(test, new Error('global leak detected: ' + leaks[0])); + var error = new Error(util.format(format, leaks.map(sQuote).join(', '))); + this.fail(test, error); } }; @@ -320,7 +328,7 @@ Runner.prototype.failHook = function(hook, err) { hook.originalTitle = hook.originalTitle || hook.title; if (hook.ctx && hook.ctx.currentTest) { hook.title = - hook.originalTitle + ' for "' + hook.ctx.currentTest.title + '"'; + hook.originalTitle + ' for ' + dQuote(hook.ctx.currentTest.title); } else { var parentTitle; if (hook.parent.title) { @@ -328,7 +336,7 @@ Runner.prototype.failHook = function(hook, err) { } else { parentTitle = hook.parent.root ? '{root}' : ''; } - hook.title = hook.originalTitle + ' in "' + parentTitle + '"'; + hook.title = hook.originalTitle + ' in ' + dQuote(parentTitle); } this.fail(hook, err); diff --git a/lib/utils.js b/lib/utils.js index c6e7245bc2..c637648442 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -13,6 +13,7 @@ var debug = require('debug')('mocha:watch'); var fs = require('fs'); var glob = require('glob'); var path = require('path'); +var util = require('util'); var join = path.join; var he = require('he'); var errors = require('./errors'); @@ -531,7 +532,7 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) { files = glob.sync(filepath); if (!files.length) { throw createNoFilesMatchPatternError( - 'Cannot find any files matching pattern "' + filepath + '"', + 'Cannot find any files matching pattern ' + exports.dQuote(filepath), filepath ); } @@ -565,7 +566,11 @@ exports.lookupFiles = function lookupFiles(filepath, extensions, recursive) { } if (!extensions) { throw createMissingArgumentError( - 'Argument "extensions" required when argument "filepath" is a directory', + util.format( + 'Argument %s required when argument %s is a directory', + exports.sQuote('extensions'), + exports.sQuote('filepath') + ), 'extensions', 'array' ); @@ -739,6 +744,76 @@ exports.clamp = function clamp(value, range) { return Math.min(Math.max(value, range[0]), range[1]); }; +/** + * Single quote text by combining with undirectional ASCII quotation marks. + * + * @description + * Provides a simple means of markup for quoting text to be used in output. + * Use this to quote names of variables, methods, and packages. + * + * package 'foo' cannot be found + * + * @private + * @param {string} str - Value to be quoted. + * @returns {string} quoted value + * @example + * sQuote('n') // => 'n' + */ +exports.sQuote = function(str) { + return "'" + str + "'"; +}; + +/** + * Double quote text by combining with undirectional ASCII quotation marks. + * + * @description + * Provides a simple means of markup for quoting text to be used in output. + * Use this to quote names of datatypes, classes, pathnames, and strings. + * + * argument 'value' must be "string" or "number" + * + * @private + * @param {string} str - Value to be quoted. + * @returns {string} quoted value + * @example + * dQuote('number') // => "number" + */ +exports.dQuote = function(str) { + return '"' + str + '"'; +}; + +/** + * Provides simplistic message translation for dealing with plurality. + * + * @description + * Use this to create messages which need to be singular or plural. + * Some languages have several plural forms, so _complete_ message clauses + * are preferable to generating the message on the fly. + * + * @private + * @param {number} n - Non-negative integer + * @param {string} msg1 - Message to be used in English for `n = 1` + * @param {string} msg2 - Message to be used in English for `n = 0, 2, 3, ...` + * @returns {string} message corresponding to value of `n` + * @example + * var sprintf = require('util').format; + * var pkgs = ['one', 'two']; + * var msg = sprintf( + * ngettext( + * pkgs.length, + * 'cannot load package: %s', + * 'cannot load packages: %s' + * ), + * pkgs.map(sQuote).join(', ') + * ); + * console.log(msg); // => cannot load packages: 'one', 'two' + */ +exports.ngettext = function(n, msg1, msg2) { + if (typeof n === 'number' && n >= 0) { + return n === 1 ? msg1 : msg2; + } +}; + /** * It's a noop. * @public diff --git a/test/integration/reporters.spec.js b/test/integration/reporters.spec.js index d44a222324..944f94f552 100644 --- a/test/integration/reporters.spec.js +++ b/test/integration/reporters.spec.js @@ -5,6 +5,8 @@ var fs = require('fs'); var crypto = require('crypto'); var path = require('path'); var run = require('./helpers').runMocha; +var utils = require('../../lib/utils'); +var dQuote = utils.dQuote; describe('reporters', function() { describe('markdown', function() { @@ -213,13 +215,9 @@ describe('reporters', function() { return; } - function dquote(s) { - return '"' + s + '"'; - } - var pattern = '^Error: invalid or unsupported TAP version: ' + - dquote(invalidTapVersion); + dQuote(invalidTapVersion); expect(res, 'to satisfy', { code: 1, output: new RegExp(pattern, 'm') diff --git a/test/unit/mocha.spec.js b/test/unit/mocha.spec.js index 580533622c..beb5e2b5e0 100644 --- a/test/unit/mocha.spec.js +++ b/test/unit/mocha.spec.js @@ -234,7 +234,7 @@ describe('Mocha', function() { new Mocha(updatedOpts); }; expect(throwError, 'to throw', { - message: 'invalid reporter "invalidReporter"', + message: "invalid reporter 'invalidReporter'", code: 'ERR_MOCHA_INVALID_REPORTER', reporter: 'invalidReporter' }); diff --git a/test/unit/runner.spec.js b/test/unit/runner.spec.js index 6c04f93f31..1adf5a0cbc 100644 --- a/test/unit/runner.spec.js +++ b/test/unit/runner.spec.js @@ -110,7 +110,7 @@ describe('Runner', function() { global.foo = 'bar'; runner.on(EVENT_TEST_FAIL, function(_test, _err) { expect(_test, 'to be', test); - expect(_err, 'to have message', 'global leak detected: foo'); + expect(_err, 'to have message', "global leak detected: 'foo'"); delete global.foo; done(); }); @@ -164,7 +164,7 @@ describe('Runner', function() { global.bar = 'baz'; runner.on(EVENT_TEST_FAIL, function(_test, _err) { expect(_test, 'to be', test); - expect(_err, 'to have message', 'global leaks detected: foo, bar'); + expect(_err, 'to have message', "global leaks detected: 'foo', 'bar'"); delete global.foo; delete global.bar; done(); @@ -194,22 +194,23 @@ describe('Runner', function() { suite.addTest(test); - global.foo = 'bar'; - global.bar = 'baz'; + global.foo = 'whitelisted'; + global.bar = 'detect-me'; runner.on(EVENT_TEST_FAIL, function(_test, _err) { expect(_test.title, 'to be', 'im a test about lions'); - expect(_err, 'to have message', 'global leak detected: bar'); + expect(_err, 'to have message', "global leak detected: 'bar'"); delete global.foo; + delete global.bar; done(); }); runner.checkGlobals(test); }); - it('should emit "fail" when a global beginning with d is introduced', function(done) { + it('should emit "fail" when a global beginning with "d" is introduced', function(done) { global.derp = 'bar'; - runner.on(EVENT_TEST_FAIL, function(test, err) { - expect(test.title, 'to be', 'herp'); - expect(err.message, 'to be', 'global leak detected: derp'); + runner.on(EVENT_TEST_FAIL, function(_test, _err) { + expect(_test.title, 'to be', 'herp'); + expect(_err, 'to have message', "global leak detected: 'derp'"); delete global.derp; done(); }); @@ -562,7 +563,7 @@ describe('Runner', function() { // Fake stack-trace err.stack = [message].concat(stack).join('\n'); - runner.on('fail', function(_hook, _err) { + runner.on(EVENT_TEST_FAIL, function(_hook, _err) { var filteredErrStack = _err.stack.split('\n').slice(1); expect( filteredErrStack.join('\n'), @@ -582,7 +583,7 @@ describe('Runner', function() { // Fake stack-trace err.stack = [message].concat(stack).join('\n'); - runner.on('fail', function(_hook, _err) { + runner.on(EVENT_TEST_FAIL, function(_hook, _err) { var filteredErrStack = _err.stack.split('\n').slice(-3); expect( filteredErrStack.join('\n'), diff --git a/test/unit/utils.spec.js b/test/unit/utils.spec.js index bd0360362d..970c77c125 100644 --- a/test/unit/utils.spec.js +++ b/test/unit/utils.spec.js @@ -706,6 +706,45 @@ describe('lib/utils', function() { }); }); + describe('sQuote/dQuote', function() { + var str = 'xxx'; + + it('should return its input as string wrapped in single quotes', function() { + var expected = "'xxx'"; + expect(utils.sQuote(str), 'to be', expected); + }); + + it('should return its input as string wrapped in double quotes', function() { + var expected = '"xxx"'; + expect(utils.dQuote(str), 'to be', expected); + }); + }); + + describe('ngettext', function() { + var singular = 'singular'; + var plural = 'plural'; + + it("should return plural string if 'n' is 0", function() { + expect(utils.ngettext(0, singular, plural), 'to be', plural); + }); + + it("should return singular string if 'n' is 1", function() { + expect(utils.ngettext(1, singular, plural), 'to be', singular); + }); + + it("should return plural string if 'n' is greater than 1", function() { + var arr = ['aaa', 'bbb']; + expect(utils.ngettext(arr.length, singular, plural), 'to be', plural); + }); + + it("should return undefined if 'n' is not a non-negative integer", function() { + expect(utils.ngettext('', singular, plural), 'to be undefined'); + expect(utils.ngettext(-1, singular, plural), 'to be undefined'); + expect(utils.ngettext(true, singular, plural), 'to be undefined'); + expect(utils.ngettext({}, singular, plural), 'to be undefined'); + }); + }); + describe('createMap', function() { it('should return an object with a null prototype', function() { expect(Object.getPrototypeOf(utils.createMap()), 'to be', null);