diff --git a/docs/index.md b/docs/index.md index 1cc8047687..12b4b18955 100644 --- a/docs/index.md +++ b/docs/index.md @@ -874,6 +874,12 @@ Use this option to have Mocha check for global variables that are leaked while r > _`--compilers` was removed in v6.0.0. See [further explanation and workarounds][mocha-wiki-compilers]._ +### `--dry-run` + +> _New in v9.0.0._ + +Report tests without executing any of them, neither tests nor hooks. + ### `--exit` > _Updated in v4.0.0._ @@ -2102,6 +2108,7 @@ mocha.setup({ asyncOnly: true, bail: true, checkLeaks: true, + dryRun: true, forbidOnly: true, forbidPending: true, global: ['MyLib'], diff --git a/lib/cli/run-option-metadata.js b/lib/cli/run-option-metadata.js index 33cd15ae08..fc870992c9 100644 --- a/lib/cli/run-option-metadata.js +++ b/lib/cli/run-option-metadata.js @@ -32,6 +32,7 @@ const TYPES = (exports.types = { 'color', 'delay', 'diff', + 'dry-run', 'exit', 'forbid-only', 'forbid-pending', diff --git a/lib/cli/run.js b/lib/cli/run.js index a8c8b619b3..79903327c3 100644 --- a/lib/cli/run.js +++ b/lib/cli/run.js @@ -83,6 +83,10 @@ exports.builder = yargs => description: 'Show diff on failure', group: GROUPS.OUTPUT }, + 'dry-run': { + description: 'Report tests without executing them', + group: GROUPS.RULES + }, exit: { description: 'Force Mocha to quit after tests complete', group: GROUPS.RULES diff --git a/lib/mocha.js b/lib/mocha.js index f1f2e25dd7..62a414a9fa 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -30,7 +30,6 @@ const { EVENT_FILE_POST_REQUIRE, EVENT_FILE_REQUIRE } = Suite.constants; -var sQuote = utils.sQuote; var debug = require('debug')('mocha:mocha'); exports = module.exports = Mocha; @@ -164,6 +163,7 @@ exports.run = function(...args) { * @param {boolean} [options.color] - Color TTY output from reporter? * @param {boolean} [options.delay] - Delay root suite execution? * @param {boolean} [options.diff] - Show diff on failure? + * @param {boolean} [options.dryRun] - Report tests without running them? * @param {string} [options.fgrep] - Test filter given string. * @param {boolean} [options.forbidOnly] - Tests marked `only` fail the suite? * @param {boolean} [options.forbidPending] - Pending tests fail the suite? @@ -200,7 +200,7 @@ function Mocha(options = {}) { .ui(options.ui) .reporter( options.reporter, - options.reporterOption || options.reporterOptions // reporterOptions was previously the only way to specify options to reporter + options.reporterOption || options.reporterOptions // for backwards compability ) .slow(options.slow) .global(options.global); @@ -222,6 +222,7 @@ function Mocha(options = {}) { 'color', 'delay', 'diff', + 'dryRun', 'forbidOnly', 'forbidPending', 'fullTrace', @@ -346,23 +347,19 @@ Mocha.prototype.reporter = function(reporterName, reporterOptions) { reporter = require(path.resolve(utils.cwd(), reporterName)); } catch (_err) { _err.code === 'MODULE_NOT_FOUND' - ? warn(sQuote(reporterName) + ' reporter not found') + ? warn(`'${reporterName}' reporter not found`) : warn( - sQuote(reporterName) + - ' reporter blew up with error:\n' + - err.stack + `'${reporterName}' reporter blew up with error:\n ${err.stack}` ); } } else { - warn( - sQuote(reporterName) + ' reporter blew up with error:\n' + err.stack - ); + warn(`'${reporterName}' reporter blew up with error:\n ${err.stack}`); } } } if (!reporter) { throw createInvalidReporterError( - 'invalid reporter ' + sQuote(reporterName), + `invalid reporter '${reporterName}'`, reporterName ); } @@ -396,10 +393,7 @@ Mocha.prototype.ui = function(ui) { try { bindInterface = require(ui); } catch (err) { - throw createInvalidInterfaceError( - 'invalid interface ' + sQuote(ui), - ui - ); + throw createInvalidInterfaceError(`invalid interface '${ui}'`, ui); } } } @@ -784,6 +778,20 @@ Mocha.prototype.diff = function(diff) { return this; }; +/** + * Enables or disables running tests in dry-run mode. + * + * @public + * @see [CLI option](../#-dry-run) + * @param {boolean} [dryRun=true] - Whether to activate dry-run mode. + * @return {Mocha} this + * @chainable + */ +Mocha.prototype.dryRun = function(dryRun) { + this.options.dryRun = dryRun !== false; + return this; +}; + /** * @summary * Sets timeout threshold value. @@ -1016,6 +1024,7 @@ Mocha.prototype.run = function(fn) { options.files = this.files; const runner = new this._runnerClass(suite, { delay: options.delay, + dryRun: options.dryRun, cleanReferencesAfterRun: this._cleanReferencesAfterRun }); createStatsCollector(runner); diff --git a/lib/runner.js b/lib/runner.js index f493004aea..079ab3066d 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -4,7 +4,6 @@ * Module dependencies. * @private */ -var util = require('util'); var EventEmitter = require('events').EventEmitter; var Pending = require('./pending'); var utils = require('./utils'); @@ -19,8 +18,6 @@ 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 STATE_PENDING = Runnable.constants.STATE_PENDING; -var dQuote = utils.dQuote; -var sQuote = utils.sQuote; var stackFilter = utils.stackTraceFilter(); var stringify = utils.stringify; @@ -140,6 +137,7 @@ class Runner extends EventEmitter { * @param {Suite} suite - Root suite * @param {Object|boolean} [opts] - Options. If `boolean` (deprecated), whether or not to delay execution of root suite until ready. * @param {boolean} [opts.delay] - Whether to delay execution of root suite until ready. + * @param {boolean} [opts.dryRun] - Whether to report tests without running them. * @param {boolean} [opts.cleanReferencesAfterRun] - Whether to clean references to test fns and hooks when a suite is done. */ constructor(suite, opts) { @@ -410,9 +408,8 @@ Runner.prototype.checkGlobals = function(test) { this._globals = this._globals.concat(leaks); if (leaks.length) { - var msg = 'global leak(s) detected: %s'; - var error = new Error(util.format(msg, leaks.map(sQuote).join(', '))); - this.fail(test, error); + var msg = `global leak(s) detected: ${leaks.map(e => `'${e}'`).join(', ')}`; + this.fail(test, new Error(msg)); } }; @@ -479,6 +476,8 @@ Runner.prototype.fail = function(test, err, force) { */ Runner.prototype.hook = function(name, fn) { + if (this._opts.dryRun) return fn(); + var suite = this.suite; var hooks = suite.getHooks(name); var self = this; @@ -557,8 +556,7 @@ Runner.prototype.hook = function(name, fn) { function setHookTitle(hook) { hook.originalTitle = hook.originalTitle || hook.title; if (hook.ctx && hook.ctx.currentTest) { - hook.title = - hook.originalTitle + ' for ' + dQuote(hook.ctx.currentTest.title); + hook.title = `${hook.originalTitle} for "${hook.ctx.currentTest.title}"`; } else { var parentTitle; if (hook.parent.title) { @@ -566,7 +564,7 @@ Runner.prototype.hook = function(name, fn) { } else { parentTitle = hook.parent.root ? '{root}' : ''; } - hook.title = hook.originalTitle + ' in ' + dQuote(parentTitle); + hook.title = `${hook.originalTitle} in "${parentTitle}"`; } } } @@ -612,7 +610,7 @@ Runner.prototype.hooks = function(name, suites, fn) { }; /** - * Run hooks from the top level down. + * Run 'afterEach' hooks from bottom up. * * @param {String} name * @param {Function} fn @@ -624,7 +622,7 @@ Runner.prototype.hookUp = function(name, fn) { }; /** - * Run hooks from the bottom up. + * Run 'beforeEach' hooks from top level down. * * @param {String} name * @param {Function} fn @@ -659,6 +657,8 @@ Runner.prototype.parents = function() { * @private */ Runner.prototype.runTest = function(fn) { + if (this._opts.dryRun) return fn(); + var self = this; var test = this.test; @@ -704,7 +704,6 @@ Runner.prototype.runTests = function(suite, fn) { self.suite = after ? errSuite.parent : errSuite; if (self.suite) { - // call hookUp afterEach self.hookUp(HOOK_TYPE_AFTER_EACH, function(err2, errSuite2) { self.suite = orig; // some hooks may fail even now diff --git a/lib/utils.js b/lib/utils.js index e46eac8d1a..9120347583 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -518,44 +518,6 @@ 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 + '"'; -}; - /** * It's a noop. * @public diff --git a/test/integration/fixtures/options/dry-run/dry-run.fixture.js b/test/integration/fixtures/options/dry-run/dry-run.fixture.js new file mode 100644 index 0000000000..ebb48ad1fa --- /dev/null +++ b/test/integration/fixtures/options/dry-run/dry-run.fixture.js @@ -0,0 +1,35 @@ +'use strict'; + +describe.only('suite1', function() { + it.skip('test1 - report as skipped', function() { }); + + it('test2 - report as passed', function() { }); + + it('test3 - report as passed', function() { + throw new Error('this test should not run'); + }); +}); + +describe('suite2', function () { + before(function() { + throw new Error('this hook should not run'); + }); + beforeEach(function() { + throw new Error('this hook should not run'); + }); + + it.only('test4 - report as passed', function () { + throw new Error('this test should not run'); + }); + + it('test5 - should be ignored', function () { + throw new Error('this test should not run'); + }); + + afterEach(function() { + throw new Error('this hook should not run'); + }); + after(function() { + throw new Error('this hook should not run'); + }); +}); diff --git a/test/integration/options/dryRun.spec.js b/test/integration/options/dryRun.spec.js new file mode 100644 index 0000000000..81325948ee --- /dev/null +++ b/test/integration/options/dryRun.spec.js @@ -0,0 +1,30 @@ +'use strict'; + +var path = require('path').posix; +var helpers = require('../helpers'); +var runMochaJSON = helpers.runMochaJSON; + +describe('--dry-run', function() { + var args = ['--dry-run']; + + it('should only report, but not execute any test', function(done) { + var fixture = path.join('options/dry-run', 'dry-run'); + runMochaJSON(fixture, args, function(err, res) { + if (err) { + return done(err); + } + + expect(res, 'to have passed') + .and( + 'to have passed tests', + 'test2 - report as passed', + 'test3 - report as passed', + 'test4 - report as passed' + ) + .and('to have passed test count', 3) + .and('to have pending test count', 1) + .and('to have failed test count', 0); + done(); + }); + }); +}); diff --git a/test/integration/reporters.spec.js b/test/integration/reporters.spec.js index 944f94f552..17bbcd2f41 100644 --- a/test/integration/reporters.spec.js +++ b/test/integration/reporters.spec.js @@ -5,8 +5,6 @@ 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() { @@ -215,9 +213,7 @@ describe('reporters', function() { return; } - var pattern = - '^Error: invalid or unsupported TAP version: ' + - dQuote(invalidTapVersion); + var pattern = `^Error: invalid or unsupported TAP version: "${invalidTapVersion}"`; expect(res, 'to satisfy', { code: 1, output: new RegExp(pattern, 'm') diff --git a/test/reporters/json-stream.spec.js b/test/reporters/json-stream.spec.js index 58694e3f19..f4610200c1 100644 --- a/test/reporters/json-stream.spec.js +++ b/test/reporters/json-stream.spec.js @@ -3,11 +3,9 @@ var events = require('../../').Runner.constants; var helpers = require('./helpers'); var reporters = require('../../').reporters; -var utils = require('../../lib/utils'); var JSONStream = reporters.JSONStream; var createMockRunner = helpers.createMockRunner; -var dQuote = utils.dQuote; var makeExpectedTest = helpers.makeExpectedTest; var makeRunReporter = helpers.createRunReporterFunction; @@ -71,17 +69,17 @@ describe('JSON Stream reporter', function() { stdout[0], 'to equal', '["pass",{"title":' + - dQuote(expectedTitle) + + `"${expectedTitle}"` + ',"fullTitle":' + - dQuote(expectedFullTitle) + + `"${expectedFullTitle}"` + ',"file":' + - dQuote(expectedFile) + + `"${expectedFile}"` + ',"duration":' + expectedDuration + ',"currentRetry":' + currentRetry + ',"speed":' + - dQuote(expectedSpeed) + + `"${expectedSpeed}"` + '}]\n' ); }); @@ -106,21 +104,21 @@ describe('JSON Stream reporter', function() { stdout[0], 'to equal', '["fail",{"title":' + - dQuote(expectedTitle) + + `"${expectedTitle}"` + ',"fullTitle":' + - dQuote(expectedFullTitle) + + `"${expectedFullTitle}"` + ',"file":' + - dQuote(expectedFile) + + `"${expectedFile}"` + ',"duration":' + expectedDuration + ',"currentRetry":' + currentRetry + ',"speed":' + - dQuote(expectedSpeed) + + `"${expectedSpeed}"` + ',"err":' + - dQuote(expectedErrorMessage) + + `"${expectedErrorMessage}"` + ',"stack":' + - dQuote(expectedErrorStack) + + `"${expectedErrorStack}"` + '}]\n' ); }); @@ -144,19 +142,19 @@ describe('JSON Stream reporter', function() { stdout[0], 'to equal', '["fail",{"title":' + - dQuote(expectedTitle) + + `"${expectedTitle}"` + ',"fullTitle":' + - dQuote(expectedFullTitle) + + `"${expectedFullTitle}"` + ',"file":' + - dQuote(expectedFile) + + `"${expectedFile}"` + ',"duration":' + expectedDuration + ',"currentRetry":' + currentRetry + ',"speed":' + - dQuote(expectedSpeed) + + `"${expectedSpeed}"` + ',"err":' + - dQuote(expectedErrorMessage) + + `"${expectedErrorMessage}"` + ',"stack":null}]\n' ); }); diff --git a/test/unit/mocha.spec.js b/test/unit/mocha.spec.js index 142018b54c..35f7abd327 100644 --- a/test/unit/mocha.spec.js +++ b/test/unit/mocha.spec.js @@ -352,6 +352,20 @@ describe('Mocha', function() { }); }); + describe('dryRun()', function() { + it('should set the dryRun option to true', function() { + mocha.dryRun(); + expect(mocha.options, 'to have property', 'dryRun', true); + }); + + describe('when provided `false` argument', function() { + it('should set the dryRun option to false', function() { + mocha.dryRun(false); + expect(mocha.options, 'to have property', 'dryRun', false); + }); + }); + }); + describe('dispose()', function() { it('should dispose the root suite', function() { mocha.dispose(); diff --git a/test/unit/utils.spec.js b/test/unit/utils.spec.js index 53d4f20f32..9e4068b9bf 100644 --- a/test/unit/utils.spec.js +++ b/test/unit/utils.spec.js @@ -669,24 +669,6 @@ describe('lib/utils', function() { }); }); - describe('sQuote()', 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); - }); - }); - - describe('dQuote()', function() { - var str = 'xxx'; - - it('should return its input as string wrapped in double quotes', function() { - var expected = '"xxx"'; - expect(utils.dQuote(str), 'to be', expected); - }); - }); - describe('createMap()', function() { it('should return an object with a null prototype', function() { expect(Object.getPrototypeOf(utils.createMap()), 'to be', null);