From 4ddfcafd07a2aa96ee7c62f72969a1ec651442dc Mon Sep 17 00:00:00 2001 From: "P. Roebuck" Date: Wed, 24 Oct 2018 13:50:28 -0500 Subject: [PATCH 01/18] 3483: Squelch CI Growl-related spawn errors (#3517) * ci(.travis.yml): Squelch Growl-related spawn errors Installed binary needed for Linux desktop notification support. * ci(appveyor.yml): Squelch Growl-related spawn errors Installed GfW package needed for Windows desktop notification support. Fixes #3483 --- .travis.yml | 8 ++++++ appveyor.yml | 69 +++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 94aa85770e..424f9c5f64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,7 @@ +### +### .travis.yml +### + # these are executed in order. each must pass for the next to be run stages: - smoke # this ensures a "user" install works properly @@ -8,6 +12,10 @@ stages: # defaults language: node_js node_js: '10' +addons: + apt: + packages: + - libnotify-bin # `nvm install` happens before the cache is restored, which means # we must install our own npm elsewhere (`~/npm`) before_install: | diff --git a/appveyor.yml b/appveyor.yml index 2d2e1e459d..c8e4863143 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,27 +1,72 @@ -platform: - - x64 +### +### appveyor.yml +### + +## General configuration +version: '{build}' +skip_commits: + message: /\[ci\s+skip\]/ + +## Environment configuration +shallow_clone: true +clone_depth: 1 environment: matrix: - nodejs_version: '10' - nodejs_version: '9' - nodejs_version: '8' - nodejs_version: '6' +matrix: + fast_finish: true install: + ## Manual Growl install + - ps: Add-AppveyorMessage "Installing Growl..." + - ps: $exePath = "$($env:USERPROFILE)\GrowlInstaller.exe" + - ps: (New-Object Net.WebClient).DownloadFile('http://www.growlforwindows.com/gfw/downloads/GrowlInstaller.exe', $exePath) + - ps: mkdir C:\GrowlInstaller | out-null + - ps: 7z x $exePath -oC:\GrowlInstaller | out-null + - ps: cmd /c start /wait msiexec /i C:\GrowlInstaller\Growl_v2.0.msi /quiet + - ps: $env:path = "C:\Program Files (x86)\Growl for Windows;$env:path" + ## Node-related installs + - ps: Add-AppveyorMessage "Installing Node..." + - set PATH=%APPDATA%\npm;C:\MinGW\bin;%PATH% - ps: Install-Product node $env:nodejs_version x64 - - set CI=true - - set PATH=%APPDATA%\npm;c:\MinGW\bin;%PATH% + - ps: Add-AppveyorMessage "Installing npm..." - npm install -g npm@^5 + ## Mocha-related package installs + - ps: Add-AppveyorMessage "Installing Mocha dependencies..." - npm ci --ignore-scripts -matrix: - fast_finish: true -build: off -version: '{build}' -shallow_clone: true -clone_depth: 1 + +## Build configuration +platform: + - x64 +build: script +before_build: + ## Growl requires some time before it's ready to handle notifications + - ps: Start-Process -NoNewWindow Growl + - ps: Add-AppveyorMessage "Started Growl service..." + - ps: Start-Sleep -Milliseconds 2000 +build_script: + ## Placeholder command + - ps: Start-Sleep -Milliseconds 0 + #- ps: Add-AppveyorMessage "Verify Growl responding..." + #- ps: growlnotify test + +## Test configuration +before_test: + - set CI=true test_script: + - ps: Add-AppveyorMessage "Displaying version information" - node --version - npm --version + - ps: Add-AppveyorMessage "Running tests..." - npm start test.node -skip_commits: - message: /\[ci\s+skip\]/ + - ps: Add-AppveyorMessage "Done" + +## Notifications +notifications: + - provider: Email + on_build_success: false + on_build_failure: false + on_build_status_changed: false From adb1f612a8e909c3ce158ab66f75bb4a54a29014 Mon Sep 17 00:00:00 2001 From: Paul Roebuck Date: Fri, 19 Oct 2018 21:38:59 -0500 Subject: [PATCH 02/18] test(test/reporters/base.spec.js): Fix Base reporter test failure due to timeout The `Base` reporter's specification verifies reporter can interpret Chai custom error messages. This test takes ~480ms (lightly loaded CPU), which is _way_ too close to the 500ms timeout. Change doubles this timeout. #3524 --- test/reporters/base.spec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/reporters/base.spec.js b/test/reporters/base.spec.js index 8ff2195f70..f880b7b240 100644 --- a/test/reporters/base.spec.js +++ b/test/reporters/base.spec.js @@ -312,6 +312,7 @@ describe('Base reporter', function() { }); it('should interpret Chai custom error messages', function() { + this.timeout(1000); var chaiExpect = require('chai').expect; try { chaiExpect(43, 'custom error message').to.equal(42); From 741c4c794c89f9cf7c590e8c220c8c16162c0465 Mon Sep 17 00:00:00 2001 From: "JeongHoon Byun (aka Outsider)" Date: Fri, 26 Oct 2018 22:56:13 -0700 Subject: [PATCH 03/18] Update "commander" to correct display of falsy default values (#3529) Additionally, this includes minor updates to mocha's help output (by rewording text and/or specifying default values). These were then collected into the website documentation. --- bin/_mocha | 30 ++++++++--- docs/index.md | 132 +++++++++++++++++++++++----------------------- package-lock.json | 18 +++++-- package.json | 2 +- 4 files changed, 103 insertions(+), 79 deletions(-) diff --git a/bin/_mocha b/bin/_mocha index 41e9871b76..355a91b83c 100755 --- a/bin/_mocha +++ b/bin/_mocha @@ -157,9 +157,17 @@ program .option('-f, --fgrep ', 'only run tests containing ') .option('-gc, --expose-gc', 'expose gc extension') .option('-i, --invert', 'inverts --grep and --fgrep matches') - .option('-r, --require ', 'require the given module') - .option('-s, --slow ', '"slow" test threshold in milliseconds [75]') - .option('-t, --timeout ', 'set test-case timeout in milliseconds [2000]') + .option('-r, --require ', 'require the given module', []) + .option( + '-s, --slow ', + 'specify "slow" test threshold in milliseconds', + 75 + ) + .option( + '-t, --timeout ', + 'specify test timeout threshold in milliseconds', + 2000 + ) .option( '-u, --ui ', `specify user-interface (${interfaceNames.join('|')})`, @@ -204,7 +212,7 @@ program '--inspect-brk', 'activate devtools in chrome and break on the first line' ) - .option('--interfaces', 'display available interfaces') + .option('--interfaces', 'output provided interfaces and exit') .option('--no-deprecation', 'silence deprecation warnings') .option( '--exit', @@ -218,10 +226,11 @@ program .option('--prof', 'log statistical profiling information') .option('--log-timer-events', 'Time events including external callbacks') .option('--recursive', 'include sub directories') - .option('--reporters', 'display available reporters') + .option('--reporters', 'output provided reporters and exit') .option( '--retries ', - 'set numbers of time to retry a failed test case' + 'specify number of times to retry a failed test case', + 0 ) .option( '--throw-deprecation', @@ -246,11 +255,16 @@ program ) .option( '--file ', - 'include a file to be ran during the suite', + 'adds file be loaded prior to suite execution', collect, [] ) - .option('--exclude ', 'a file or glob pattern to ignore', collect, []); + .option( + '--exclude ', + 'adds file or glob pattern to ignore', + collect, + [] + ); program._name = 'mocha'; diff --git a/docs/index.md b/docs/index.md index 5fae36b634..367dba678b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -779,73 +779,71 @@ Mocha supports the `err.expected` and `err.actual` properties of any thrown `Ass ## Usage -```text - Usage: mocha [debug] [options] [files] - - Options: - - -V, --version output the version number - -A, --async-only force all tests to take a callback (async) or return a promise - -c, --colors force enabling of colors - -C, --no-colors force disabling of colors - -G, --growl enable growl notification support - -O, --reporter-options reporter-specific options - -R, --reporter specify the reporter to use (default: spec) - -S, --sort sort test files - -b, --bail bail after first test failure - -d, --debug enable node's debugger, synonym for node --debug - -g, --grep only run tests matching - -f, --fgrep only run tests containing - -gc, --expose-gc expose gc extension - -i, --invert inverts --grep and --fgrep matches - -r, --require require the given module - -s, --slow "slow" test threshold in milliseconds [75] - -t, --timeout set test-case timeout in milliseconds [2000] - -u, --ui specify user-interface (bdd|tdd|qunit|exports) (default: bdd) - -w, --watch watch files in the current working directory for changes - --check-leaks check for global variable leaks - --full-trace display the full stack trace - --compilers :,... use the given module(s) to compile files (default: ) - --debug-brk enable node's debugger breaking on the first line - --globals allow the given comma-delimited global [names] (default: ) - --es_staging enable all staged features - --harmony<_classes,_generators,...> all node --harmony* flags are available - --preserve-symlinks Instructs the module loader to preserve symbolic links when resolving and caching modules - --icu-data-dir include ICU data - --inline-diffs display actual/expected differences inline within each string - --no-diff do not show a diff on failure - --inspect activate devtools in chrome - --inspect-brk activate devtools in chrome and break on the first line - --interfaces display available interfaces - --no-deprecation silence deprecation warnings - --exit force shutdown of the event loop after test run: mocha will call process.exit - --no-timeouts disables timeouts, given implicitly with --debug - --no-warnings silence all node process warnings - --opts specify opts path (default: test/mocha.opts) - --perf-basic-prof enable perf linux profiler (basic support) - --napi-modules enable experimental NAPI modules - --prof log statistical profiling information - --log-timer-events Time events including external callbacks - --recursive include sub directories - --reporters display available reporters - --retries set numbers of time to retry a failed test case - --throw-deprecation throw an exception anytime a deprecated function is used - --trace trace function calls - --trace-deprecation show stack traces on deprecations - --trace-warnings show stack traces on node process warnings - --use_strict enforce strict mode - --watch-extensions ,... specify extensions to monitor with --watch (default: js) - --delay wait for async suite definition - --allow-uncaught enable uncaught errors to propagate - --forbid-only causes test marked with only to fail the suite - --forbid-pending causes pending tests and test marked with skip to fail the suite - --file include a file to be ran during the suite (default: ) - --exclude a file or glob pattern to ignore (default: ) - -h, --help output usage information - - Commands: - - init initialize a client-side mocha setup at +```console +Usage: mocha [debug] [options] [files] + +Options: + -V, --version output the version number + -A, --async-only force all tests to take a callback (async) or return a promise + -c, --colors force enabling of colors + -C, --no-colors force disabling of colors + -G, --growl enable growl notification support + -O, --reporter-options reporter-specific options + -R, --reporter specify the reporter to use (default: "spec") + -S, --sort sort test files + -b, --bail bail after first test failure + -d, --debug enable node's debugger, synonym for node --debug + -g, --grep only run tests matching + -f, --fgrep only run tests containing + -gc, --expose-gc expose gc extension + -i, --invert inverts --grep and --fgrep matches + -r, --require require the given module (default: []) + -s, --slow specify "slow" test threshold in milliseconds (default: 75) + -t, --timeout specify test timeout threshold in milliseconds (default: 2000) + -u, --ui specify user-interface (bdd|tdd|qunit|exports) (default: "bdd") + -w, --watch watch files in the current working directory for changes + --check-leaks check for global variable leaks + --full-trace display the full stack trace + --compilers :,... use the given module(s) to compile files (default: []) + --debug-brk enable node's debugger breaking on the first line + --globals allow the given comma-delimited global [names] (default: []) + --es_staging enable all staged features + --harmony<_classes,_generators,...> all node --harmony* flags are available + --preserve-symlinks Instructs the module loader to preserve symbolic links when resolving and caching modules + --icu-data-dir include ICU data + --inline-diffs display actual/expected differences inline within each string + --no-diff do not show a diff on failure + --inspect activate devtools in chrome + --inspect-brk activate devtools in chrome and break on the first line + --interfaces output provided interfaces and exit + --no-deprecation silence deprecation warnings + --exit force shutdown of the event loop after test run: mocha will call process.exit + --no-timeouts disables timeouts, given implicitly with --debug + --no-warnings silence all node process warnings + --opts specify opts path (default: "test/mocha.opts") + --perf-basic-prof enable perf linux profiler (basic support) + --napi-modules enable experimental NAPI modules + --prof log statistical profiling information + --log-timer-events Time events including external callbacks + --recursive include sub directories + --reporters output provided reporters and exit + --retries specify number of times to retry a failed test case (default: 0) + --throw-deprecation throw an exception anytime a deprecated function is used + --trace trace function calls + --trace-deprecation show stack traces on deprecations + --trace-warnings show stack traces on node process warnings + --use_strict enforce strict mode + --watch-extensions ,... specify extensions to monitor with --watch (default: ["js"]) + --delay wait for async suite definition + --allow-uncaught enable uncaught errors to propagate + --forbid-only causes test marked with only to fail the suite + --forbid-pending causes pending tests and test marked with skip to fail the suite + --file adds file be loaded prior to suite execution (default: []) + --exclude adds file or glob pattern to ignore (default: []) + -h, --help output usage information + +Commands: + init initialize a client-side mocha setup at ``` ### `-w, --watch` diff --git a/package-lock.json b/package-lock.json index 782bb42830..97fdde53a1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2026,9 +2026,9 @@ } }, "commander": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz", - "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==" + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==" }, "comment-regex": { "version": "1.0.1", @@ -5939,6 +5939,12 @@ "uglify-js": "3.3.x" }, "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -17954,6 +17960,12 @@ "source-map": "~0.6.1" }, "dependencies": { + "commander": { + "version": "2.15.1", + "resolved": "http://registry.npmjs.org/commander/-/commander-2.15.1.tgz", + "integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==", + "dev": true + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", diff --git a/package.json b/package.json index 002b55e32b..5cd30f5a17 100644 --- a/package.json +++ b/package.json @@ -461,7 +461,7 @@ }, "dependencies": { "browser-stdout": "1.3.1", - "commander": "2.15.1", + "commander": "2.19.0", "debug": "3.1.0", "diff": "3.5.0", "escape-string-regexp": "1.0.5", From 835ac33e3b76428a3d7a25653b31c93d5f908ec4 Mon Sep 17 00:00:00 2001 From: "P. Roebuck" Date: Tue, 30 Oct 2018 08:52:00 -0500 Subject: [PATCH 04/18] refactor(bin/options.js): Refactor and improve documentation (#3533) --- bin/options.js | 57 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 9 deletions(-) diff --git a/bin/options.js b/bin/options.js index a25a18e28e..0c27ae5fa9 100644 --- a/bin/options.js +++ b/bin/options.js @@ -13,9 +13,54 @@ const fs = require('fs'); module.exports = getOptions; /** - * Get options. + * Default pathname for run-control file. + * + * @constant + * @type {string} + * @default */ +const defaultPathname = 'test/mocha.opts'; +/** + * Reads contents of the run-control file. + * + * @private + * @param {string} pathname - Pathname of run-control file. + * @returns {string} file contents + */ +function readOptionsFile(pathname) { + return fs.readFileSync(pathname, 'utf8'); +} + +/** + * Parses options read from run-control file. + * + * @private + * @param {string} content - Content read from run-control file. + * @returns {string[]} cmdline options (and associated arguments) + */ +function parseOptions(content) { + /* + * Replaces comments with empty strings + * Replaces escaped spaces (e.g., 'xxx\ yyy') with HTML space + * Splits on whitespace, creating array of substrings + * Filters empty string elements from array + * Replaces any HTML space with space + */ + return content + .replace(/^#.*$/gm, '') + .replace(/\\\s/g, '%20') + .split(/\s/) + .filter(Boolean) + .map(value => value.replace(/%20/g, ' ')); +} + +/** + * Prepends options from run-control file to the command line arguments. + * + * @public + * @see {@link https://mochajs.org/#mochaopts|mocha.opts} + */ function getOptions() { if ( process.argv.length === 3 && @@ -26,17 +71,11 @@ function getOptions() { const optsPath = process.argv.indexOf('--opts') === -1 - ? 'test/mocha.opts' + ? defaultPathname : process.argv[process.argv.indexOf('--opts') + 1]; try { - const opts = fs - .readFileSync(optsPath, 'utf8') - .replace(/^#.*$/gm, '') - .replace(/\\\s/g, '%20') - .split(/\s/) - .filter(Boolean) - .map(value => value.replace(/%20/g, ' ')); + const opts = parseOptions(readOptionsFile(optsPath)); process.argv = process.argv .slice(0, 2) From b9b3ac0122347df9b9699413d2025541674c929f Mon Sep 17 00:00:00 2001 From: "P. Roebuck" Date: Tue, 30 Oct 2018 11:59:21 -0500 Subject: [PATCH 05/18] refactor(json-stream.js): Consistent output stream usage (#3532) ### Description of the Change * Made all output directly use `process.stdout`. * `process.stdout` and `console.log` take different routes to get to same place * Corrected ctor name. * Updated documentation. ### Alternate Designs N/A ### Benefits Consistency. [Don't cross the streams](https://www.youtube.com/watch?v=wyKQe_i9yyo)! ### Possible Drawbacks N/A ### Applicable issues Fixes #3526 Fixes #3521 semver-patch --- lib/reporters/json-stream.js | 51 ++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/reporters/json-stream.js b/lib/reporters/json-stream.js index 0edd0cbf88..5b4cfbcebe 100644 --- a/lib/reporters/json-stream.js +++ b/lib/reporters/json-stream.js @@ -9,55 +9,68 @@ var Base = require('./base'); /** - * Expose `List`. + * Expose `JSONStream`. */ -exports = module.exports = List; +exports = module.exports = JSONStream; /** - * Initialize a new `JSONStream` test reporter. + * Constructs a new `JSONStream` reporter instance. * * @public - * @name JSONStream - * @class JSONStream - * @memberof Mocha.reporters + * @class * @extends Mocha.reporters.Base - * @api public - * @param {Runner} runner + * @memberof Mocha.reporters + * @param {Runner} runner - Instance triggers reporter actions. */ -function List(runner) { +function JSONStream(runner) { Base.call(this, runner); var self = this; var total = runner.total; - runner.on('start', function() { - console.log(JSON.stringify(['start', {total: total}])); + runner.once('start', function() { + writeEvent(['start', {total: total}]); }); runner.on('pass', function(test) { - console.log(JSON.stringify(['pass', clean(test)])); + writeEvent(['pass', clean(test)]); }); runner.on('fail', function(test, err) { test = clean(test); test.err = err.message; test.stack = err.stack || null; - console.log(JSON.stringify(['fail', test])); + writeEvent(['fail', test]); }); runner.once('end', function() { - process.stdout.write(JSON.stringify(['end', self.stats])); + writeEvent(['end', self.stats]); }); } /** - * Return a plain-object representation of `test` - * free of cyclic properties etc. + * Mocha event to be written to the output stream. + * @typedef {Array} JSONStream~MochaEvent + */ + +/** + * Writes Mocha event to reporter output stream. + * + * @private + * @param {JSONStream~MochaEvent} event - Mocha event to be output. + */ +function writeEvent(event) { + process.stdout.write(JSON.stringify(event) + '\n'); +} + +/** + * Returns an object literal representation of `test` + * free of cyclic properties, etc. * - * @api private - * @param {Object} test - * @return {Object} + * @private + * @param {Test} test - Instance used as data source. + * @return {Object} object containing pared-down test instance data */ function clean(test) { return { From a371e2f73f8a6781aab6723bb596a31fc275bdf7 Mon Sep 17 00:00:00 2001 From: "P. Roebuck" Date: Fri, 2 Nov 2018 12:48:34 -0500 Subject: [PATCH 06/18] fix(_mocha): Update '--no-timeouts' argument description (#3546) Previously undocumented that use of `--inspect` would disable timeouts. Fixes #3519 --- bin/_mocha | 5 ++++- docs/index.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/bin/_mocha b/bin/_mocha index 355a91b83c..742c5d70d5 100755 --- a/bin/_mocha +++ b/bin/_mocha @@ -218,7 +218,10 @@ program '--exit', 'force shutdown of the event loop after test run: mocha will call process.exit' ) - .option('--no-timeouts', 'disables timeouts, given implicitly with --debug') + .option( + '--no-timeouts', + 'disables timeouts, given implicitly with --debug/--inspect' + ) .option('--no-warnings', 'silence all node process warnings') .option('--opts ', 'specify opts path', 'test/mocha.opts') .option('--perf-basic-prof', 'enable perf linux profiler (basic support)') diff --git a/docs/index.md b/docs/index.md index 367dba678b..fd4ac9aa94 100644 --- a/docs/index.md +++ b/docs/index.md @@ -818,7 +818,7 @@ Options: --interfaces output provided interfaces and exit --no-deprecation silence deprecation warnings --exit force shutdown of the event loop after test run: mocha will call process.exit - --no-timeouts disables timeouts, given implicitly with --debug + --no-timeouts disables timeouts, given implicitly with --debug/--inspect --no-warnings silence all node process warnings --opts specify opts path (default: "test/mocha.opts") --perf-basic-prof enable perf linux profiler (basic support) From 1ded82fb326daae9325860cf18f7d2a710121283 Mon Sep 17 00:00:00 2001 From: "P. Roebuck" Date: Fri, 2 Nov 2018 13:10:10 -0500 Subject: [PATCH 07/18] build(ESLint/Git): Ignore JSDoc output directory (#3544) Prettier-ESLint keeps busying itself with the JSDoc output directory upon any commit, spewing hundreds of errors. This tells both ESLint and Git to ignore the directory. --- .eslintignore | 1 + .gitignore | 1 + 2 files changed, 2 insertions(+) diff --git a/.eslintignore b/.eslintignore index 48c498f279..2226cf1fc2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -2,4 +2,5 @@ coverage/ mocha.js *.fixture.js docs/ +out/ !lib/mocha.js diff --git a/.gitignore b/.gitignore index f11fc29c86..7e1edeb748 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ yarn.lock docs/_site docs/_dist docs/api +out/ .vscode/ From c16fb7680fcabf0ab0fbc670f89b4617058a7cd8 Mon Sep 17 00:00:00 2001 From: "P. Roebuck" Date: Fri, 2 Nov 2018 17:16:15 -0500 Subject: [PATCH 08/18] ci(Travis/Appveyor): Update Node versions in CI matrix (#3543) * ci(.travis.yml,appveyor.yml): Update Node versions in CI matrix Make Node-11 the new default and drop Node-9 from matrix. --- .travis.yml | 6 +++--- appveyor.yml | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 424f9c5f64..f6153f6815 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ stages: # defaults language: node_js -node_js: '10' +node_js: '11' addons: apt: packages: @@ -40,7 +40,7 @@ jobs: - &node script: npm start test.node - node_js: '9' + node_js: '10' - <<: *node node_js: '8' @@ -75,7 +75,7 @@ jobs: - node_modules # npm install, unlike npm ci, doesn't wipe node_modules - <<: *smoke - node_js: '9' + node_js: '10' - <<: *smoke node_js: '8' diff --git a/appveyor.yml b/appveyor.yml index c8e4863143..e9f8eba0c0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -12,8 +12,8 @@ shallow_clone: true clone_depth: 1 environment: matrix: + - nodejs_version: '11' - nodejs_version: '10' - - nodejs_version: '9' - nodejs_version: '8' - nodejs_version: '6' matrix: @@ -69,4 +69,3 @@ notifications: on_build_success: false on_build_failure: false on_build_status_changed: false - From 3e77008b5645dbf3f7ef828bf327cb724eee0c69 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Fri, 2 Nov 2018 15:51:31 -0700 Subject: [PATCH 09/18] update release instructions [ci skip] --- MAINTAINERS.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index bee04f42d1..55e5436cfc 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -279,18 +279,16 @@ This is not necessarily ideal, and we should consider another method of using br 1. Decide whether this is a `patch`, `minor`, or `major` release by the PRs which have been merged since the last release. 1. Checkout `master` in your working copy & pull. 1. Modify `CHANGELOG.md`; follow the existing conventions in that file. Commit this file only; add `[ci skip]` to the commit message to avoid a build. -1. Use `npm version` to bump the version; see `npm version --help` for more info. (Hint--use `-m`: e.g. `npm version patch -m 'Release v%s'`) -1. Push `master` to origin with your new tag; e.g. `git push origin master --tags` +1. Use `npm version` (use `npm@6+`) to bump the version; see `npm version --help` for more info. (Hint--use `-m`: e.g., `npm version patch -m 'Release v%s'`) +1. Push `master` to `origin` with your new tag; e.g. `git push origin master --tags` 1. Copy & paste the added lines to a new GitHub "release". Be sure to add any missing link references (use "preview" button). Save release as draft. 1. Meanwhile, you can check [the build](https://travis-ci.org/mochajs/mocha) on Travis-CI. -1. Once it's green and you're satisfied with the release notes, open your draft release on GitHub, then click "publish" +1. Once the build is green, and you're satisfied with the release notes, open your draft release on GitHub, then click "publish." 1. Back in your working copy, run `npm publish`. -1. Announce the update on Twitter or just tell your dog or something. +1. Announce the update on Twitter or just tell your dog or something. New releases will be automatically tweeted by [@b0neskull](https://twitter.com/b0neskull). *Note: there are too many steps above.* -> As of this writing, `npm version` (using npm@5) is not working well, and you may have to tag manually. Commit the change to the version in `package.json` with a message of the format `Release vX.Y.Z`, then tag the changeset using `vX.Y.Z`. - ## Projects There are [Projects](https://github.com/mochajs/mocha/projects), but we haven't yet settled on how to use them. From 00ca06b0e957ec4f067268c98053782ac5dcb69f Mon Sep 17 00:00:00 2001 From: "JeongHoon Byun (aka Outsider)" Date: Sat, 3 Nov 2018 08:41:27 +0900 Subject: [PATCH 10/18] fix runner to emit start/end event (#3395) Signed-off-by: Outsider --- lib/runner.js | 9 +++++++-- test/unit/mocha.spec.js | 10 ++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/runner.js b/lib/runner.js index d105bca321..dc2092f9fa 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -851,10 +851,15 @@ Runner.prototype.run = function(fn) { filterOnly(rootSuite); } self.started = true; - self.emit('start'); + Runner.immediately(function() { + self.emit('start'); + }); + self.runSuite(rootSuite, function() { debug('finished running'); - self.emit('end'); + Runner.immediately(function() { + self.emit('end'); + }); }); } diff --git a/test/unit/mocha.spec.js b/test/unit/mocha.spec.js index 3f7145834c..7c91add7f3 100644 --- a/test/unit/mocha.spec.js +++ b/test/unit/mocha.spec.js @@ -34,6 +34,16 @@ describe('Mocha', function() { }); } ); + + it('should emit start event', function(done) { + var mocha = new Mocha(blankOpts); + mocha.run().on('start', done); + }); + + it('should emit end event', function(done) { + var mocha = new Mocha(blankOpts); + mocha.run().on('end', done); + }); }); describe('.addFile()', function() { From be17ea5beecfb6fb4dfebcd584a19e0493c77aa5 Mon Sep 17 00:00:00 2001 From: "P. Roebuck" Date: Sat, 3 Nov 2018 20:52:59 -0500 Subject: [PATCH 11/18] Warn that suites cannot return a value (#3550) * Give a `DeprecationWarning` on suite callback returning any value. * Deprecation warning: Show a message only once; use `process.nextTick` when falling back to `console.warn` * Add a test for `utils.deprecate` * style: Make prettier happy * test(deprecate.spec.js): Make PR requested changes for approval (per @boneskull) --- lib/interfaces/common.js | 15 ++++++++-- lib/utils.js | 28 +++++++++++++++++++ test/integration/deprecate.spec.js | 18 ++++++++++++ .../integration/fixtures/deprecate.fixture.js | 9 ++++++ .../suite/suite-returning-value.fixture.js | 5 ++++ test/integration/suite.spec.js | 24 ++++++++++++---- 6 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 test/integration/deprecate.spec.js create mode 100644 test/integration/fixtures/deprecate.fixture.js create mode 100644 test/integration/fixtures/suite/suite-returning-value.fixture.js diff --git a/lib/interfaces/common.js b/lib/interfaces/common.js index 4ca340a608..7997312786 100644 --- a/lib/interfaces/common.js +++ b/lib/interfaces/common.js @@ -1,6 +1,7 @@ 'use strict'; var Suite = require('../suite'); +var utils = require('../utils'); /** * Functions common to more than one interface. @@ -92,6 +93,7 @@ module.exports = function(suites, context, mocha) { /** * Creates a suite. + * * @param {Object} opts Options * @param {string} opts.title Title of Suite * @param {Function} [opts.fn] Suite Function (not always applicable) @@ -109,13 +111,22 @@ module.exports = function(suites, context, mocha) { suite.parent._onlySuites = suite.parent._onlySuites.concat(suite); } if (typeof opts.fn === 'function') { - opts.fn.call(suite); + var result = opts.fn.call(suite); + if (typeof result !== 'undefined') { + utils.deprecate( + 'Deprecation Warning: Suites do not support a return value;' + + opts.title + + ' returned :' + + result + ); + } suites.shift(); } else if (typeof opts.fn === 'undefined' && !suite.pending) { throw new Error( 'Suite "' + suite.fullTitle() + - '" was defined but no callback was supplied. Supply a callback or explicitly skip the suite.' + '" was defined but no callback was supplied. ' + + 'Supply a callback or explicitly skip the suite.' ); } else if (!opts.fn && suite.pending) { suites.shift(); diff --git a/lib/utils.js b/lib/utils.js index e67bf74414..71ea6ce4fb 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -584,6 +584,34 @@ exports.getError = function(err) { return err || exports.undefinedError(); }; +/** + * Show a deprecation warning. Each distinct message is only displayed once. + * + * @param {string} msg + */ +exports.deprecate = function(msg) { + msg = String(msg); + if (seenDeprecationMsg.hasOwnProperty(msg)) { + return; + } + seenDeprecationMsg[msg] = true; + doDeprecationWarning(msg); +}; + +var seenDeprecationMsg = {}; + +var doDeprecationWarning = process.emitWarning + ? function(msg) { + // Node.js v6+ + process.emitWarning(msg, 'DeprecationWarning'); + } + : function(msg) { + // Everything else + process.nextTick(function() { + console.warn(msg); + }); + }; + /** * @summary * This Filter based on `mocha-clean` module.(see: `github.com/rstacruz/mocha-clean`) diff --git a/test/integration/deprecate.spec.js b/test/integration/deprecate.spec.js new file mode 100644 index 0000000000..db806fcde5 --- /dev/null +++ b/test/integration/deprecate.spec.js @@ -0,0 +1,18 @@ +'use strict'; + +var assert = require('assert'); +var run = require('./helpers').runMocha; +var args = []; + +describe('utils.deprecate test', function() { + it('should print unique deprecation only once', function(done) { + run('deprecate.fixture.js', args, function(err, res) { + if (err) { + return done(err); + } + var result = res.output.match(/deprecated thing/g) || []; + assert.equal(result.length, 2); + done(); + }); + }); +}); diff --git a/test/integration/fixtures/deprecate.fixture.js b/test/integration/fixtures/deprecate.fixture.js new file mode 100644 index 0000000000..cf98884600 --- /dev/null +++ b/test/integration/fixtures/deprecate.fixture.js @@ -0,0 +1,9 @@ +'use strict'; + +var utils = require("../../../lib/utils"); + +it('consolidates identical calls to deprecate', function() { + utils.deprecate("suite foo did a deprecated thing"); + utils.deprecate("suite foo did a deprecated thing"); + utils.deprecate("suite bar did a deprecated thing"); +}); diff --git a/test/integration/fixtures/suite/suite-returning-value.fixture.js b/test/integration/fixtures/suite/suite-returning-value.fixture.js new file mode 100644 index 0000000000..8859cfb5d9 --- /dev/null +++ b/test/integration/fixtures/suite/suite-returning-value.fixture.js @@ -0,0 +1,5 @@ +'use strict'; + +describe('a suite returning a value', function () { + return Promise.resolve(); +}); diff --git a/test/integration/suite.spec.js b/test/integration/suite.spec.js index e8bd34382b..338a198f63 100644 --- a/test/integration/suite.spec.js +++ b/test/integration/suite.spec.js @@ -9,8 +9,7 @@ describe('suite w/no callback', function() { it('should throw a helpful error message when a callback for suite is not supplied', function(done) { run('suite/suite-no-callback.fixture.js', args, function(err, res) { if (err) { - done(err); - return; + return done(err); } var result = res.output.match(/no callback was supplied/) || []; assert.equal(result.length, 1); @@ -24,8 +23,7 @@ describe('skipped suite w/no callback', function() { it('should not throw an error when a callback for skipped suite is not supplied', function(done) { run('suite/suite-skipped-no-callback.fixture.js', args, function(err, res) { if (err) { - done(err); - return; + return done(err); } var pattern = new RegExp('Error', 'g'); var result = res.output.match(pattern) || []; @@ -40,8 +38,7 @@ describe('skipped suite w/ callback', function() { it('should not throw an error when a callback for skipped suite is supplied', function(done) { run('suite/suite-skipped-callback.fixture.js', args, function(err, res) { if (err) { - done(err); - return; + return done(err); } var pattern = new RegExp('Error', 'g'); var result = res.output.match(pattern) || []; @@ -50,3 +47,18 @@ describe('skipped suite w/ callback', function() { }); }); }); + +describe('suite returning a value', function() { + this.timeout(2000); + it('should give a deprecation warning for suite callback returning a value', function(done) { + run('suite/suite-returning-value.fixture.js', args, function(err, res) { + if (err) { + return done(err); + } + var pattern = new RegExp('Deprecation Warning', 'g'); + var result = res.output.match(pattern) || []; + assert.equal(result.length, 1); + done(); + }); + }); +}); From f2b62ec250fc765fc831d579ce69d6dc7b3b7661 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Sun, 4 Nov 2018 14:49:45 -0800 Subject: [PATCH 12/18] upgrade jekyll; closes #3548 (#3549) * upgrade jekyll; closes #3548 see https://nvd.nist.gov/vuln/detail/CVE-2018-17567 * add .ruby-version for netlify --- .ruby-version | 1 + Gemfile | 2 +- Gemfile.lock | 16 ++++++++-------- 3 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 .ruby-version diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000000..35cee72dcb --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.4.3 diff --git a/Gemfile b/Gemfile index 743d18eca7..1a8c10648c 100644 --- a/Gemfile +++ b/Gemfile @@ -1,3 +1,3 @@ source "https://rubygems.org" # Added at 2017-12-05 23:01:55 -0800 by boneskull: -gem "jekyll", "~> 3.6" +gem "jekyll", ">= 3.8.4" diff --git a/Gemfile.lock b/Gemfile.lock index ad15098a0c..dd897f793e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -14,7 +14,7 @@ GEM http_parser.rb (0.6.0) i18n (0.9.5) concurrent-ruby (~> 1.0) - jekyll (3.8.3) + jekyll (3.8.4) addressable (~> 2.4) colorator (~> 1.0) em-websocket (~> 0.5) @@ -29,25 +29,25 @@ GEM safe_yaml (~> 1.0) jekyll-sass-converter (1.5.2) sass (~> 3.4) - jekyll-watch (2.0.0) + jekyll-watch (2.1.2) listen (~> 3.0) kramdown (1.17.0) - liquid (4.0.0) + liquid (4.0.1) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) mercenary (0.3.6) - pathutil (0.16.1) + pathutil (0.16.2) forwardable-extended (~> 2.6) public_suffix (3.0.3) rb-fsevent (0.10.3) rb-inotify (0.9.10) ffi (>= 0.5.0, < 2) - rouge (3.2.1) + rouge (3.3.0) ruby_dep (1.5.0) safe_yaml (1.0.4) - sass (3.5.7) + sass (3.6.0) sass-listen (~> 4.0.0) sass-listen (4.0.0) rb-fsevent (~> 0.9, >= 0.9.4) @@ -57,7 +57,7 @@ PLATFORMS ruby DEPENDENCIES - jekyll (~> 3.6) + jekyll (>= 3.8.4) BUNDLED WITH - 1.16.1 + 1.16.6 From 8256b65c35bbd1aa7e6fc7c38b66c1f2585fafe2 Mon Sep 17 00:00:00 2001 From: Marc Udoff Date: Tue, 26 Jun 2018 13:24:23 -0400 Subject: [PATCH 13/18] Show diff in xunit --- lib/reporters/xunit.js | 8 +++++++- test/reporters/xunit.spec.js | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/reporters/xunit.js b/lib/reporters/xunit.js index c1a930d2d8..bd89a37e76 100644 --- a/lib/reporters/xunit.js +++ b/lib/reporters/xunit.js @@ -151,6 +151,8 @@ XUnit.prototype.write = function(line) { * @param {Test} test */ XUnit.prototype.test = function(test) { + Base.useColors = false; + var attrs = { classname: test.parent.fullTitle(), name: test.title, @@ -159,6 +161,10 @@ XUnit.prototype.test = function(test) { if (test.state === 'failed') { var err = test.err; + var diff = + Base.hideDiff || !err.actual || !err.expected + ? '' + : '\n' + Base.generateDiff(err.actual, err.expected); this.write( tag( 'testcase', @@ -168,7 +174,7 @@ XUnit.prototype.test = function(test) { 'failure', {}, false, - escape(err.message) + '\n' + escape(err.stack) + escape(err.message) + escape(diff) + '\n' + escape(err.stack) ) ) ); diff --git a/test/reporters/xunit.spec.js b/test/reporters/xunit.spec.js index 8108c50b78..42e9ca2c6e 100644 --- a/test/reporters/xunit.spec.js +++ b/test/reporters/xunit.spec.js @@ -18,6 +18,8 @@ describe('XUnit reporter', function() { var expectedClassName = 'fullTitle'; var expectedTitle = 'some title'; var expectedMessage = 'some message'; + var expectedDiff = + '\n + expected - actual\n\n -foo\n +bar\n '; var expectedStack = 'some-stack'; var expectedWrite = null; @@ -214,6 +216,8 @@ describe('XUnit reporter', function() { }, duration: 1000, err: { + actual: 'foo', + expected: 'bar', message: expectedMessage, stack: expectedStack } @@ -235,6 +239,8 @@ describe('XUnit reporter', function() { '" time="1">' + expectedMessage + '\n' + + expectedDiff + + '\n' + expectedStack + ''; From 9d27bac168f0ee2c4be3b8e21396efa0c00cdbed Mon Sep 17 00:00:00 2001 From: "JeongHoon Byun (aka Outsider)" Date: Tue, 6 Nov 2018 07:23:15 +0900 Subject: [PATCH 14/18] Extract `runReporter` to capture output under tests (#3528) While refactoring the `dot` reporter test, we created a method for capturing output from running the reporter. It proved so handy at squelching test "noise" that JeongHoon extracted it and applied it to many of the other reporters. --- test/reporters/doc.spec.js | 42 ++++++----------- test/reporters/dot.spec.js | 51 ++++++--------------- test/reporters/helpers.js | 48 ++++++++++++++++++-- test/reporters/json-stream.spec.js | 37 ++++----------- test/reporters/landing.spec.js | 35 +++++--------- test/reporters/list.spec.js | 46 ++++++------------- test/reporters/markdown.spec.js | 22 +++------ test/reporters/min.spec.js | 25 ++++------ test/reporters/nyan.spec.js | 73 ++++++++++++++---------------- test/reporters/progress.spec.js | 18 ++++---- test/reporters/spec.spec.js | 28 ++++-------- test/reporters/tap.spec.js | 47 ++++++------------- 12 files changed, 186 insertions(+), 286 deletions(-) diff --git a/test/reporters/doc.spec.js b/test/reporters/doc.spec.js index e3de653528..857922b83e 100644 --- a/test/reporters/doc.spec.js +++ b/test/reporters/doc.spec.js @@ -4,22 +4,15 @@ var reporters = require('../../').reporters; var Doc = reporters.Doc; var createMockRunner = require('./helpers.js').createMockRunner; +var makeRunReporter = require('./helpers.js').createRunReporterFunction; describe('Doc reporter', function() { - var stdout; - var stdoutWrite; var runner; - beforeEach(function() { - stdout = []; - stdoutWrite = process.stdout.write; - process.stdout.write = function(string, enc, callback) { - stdout.push(string); - stdoutWrite.call(process.stdout, string, enc, callback); - }; - }); + var options = {}; + var runReporter = makeRunReporter(Doc); afterEach(function() { - process.stdout.write = stdoutWrite; + runner = undefined; }); describe('on suite', function() { @@ -32,8 +25,7 @@ describe('Doc reporter', function() { }; it('should log html with indents and expected title', function() { runner = createMockRunner('suite', 'suite', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
\n', '

' + expectedTitle + '

\n', @@ -48,8 +40,7 @@ describe('Doc reporter', function() { }; expectedTitle = '<div>' + expectedTitle + '</div>'; runner = createMockRunner('suite', 'suite', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
\n', '

' + expectedTitle + '

\n', @@ -64,8 +55,7 @@ describe('Doc reporter', function() { }; it('should not log any html', function() { runner = createMockRunner('suite', 'suite', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); expect(stdout, 'to be empty'); }); }); @@ -78,8 +68,7 @@ describe('Doc reporter', function() { }; it('should log expected html with indents', function() { runner = createMockRunner('suite end', 'suite end', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [' \n', '
\n']; expect(stdout, 'to equal', expectedArray); }); @@ -90,8 +79,7 @@ describe('Doc reporter', function() { }; it('should not log any html', function() { runner = createMockRunner('suite end', 'suite end', null, null, suite); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); expect(stdout, 'to be empty'); }); }); @@ -109,8 +97,7 @@ describe('Doc reporter', function() { }; it('should log html with indents and expected title and body', function() { runner = createMockRunner('pass', 'pass', null, null, test); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedTitle + '
\n', '
' + expectedBody + '
\n' @@ -128,8 +115,7 @@ describe('Doc reporter', function() { var expectedEscapedBody = '<div>' + expectedBody + '</div>'; runner = createMockRunner('pass', 'pass', null, null, test); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedEscapedTitle + '
\n', '
' + expectedEscapedBody + '
\n' @@ -158,8 +144,7 @@ describe('Doc reporter', function() { test, expectedError ); - Doc.call(this, runner); - process.stdout.write = stdoutWrite; + var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedTitle + '
\n', '
' +
@@ -190,8 +175,7 @@ describe('Doc reporter', function() {
         test,
         unescapedError
       );
-      Doc.call(this, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter(this, runner, options);
       var expectedArray = [
         '    
' + expectedEscapedTitle + '
\n', '
' +
diff --git a/test/reporters/dot.spec.js b/test/reporters/dot.spec.js
index 0940d8be5f..472ec63d01 100644
--- a/test/reporters/dot.spec.js
+++ b/test/reporters/dot.spec.js
@@ -5,42 +5,17 @@ var Dot = reporters.Dot;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers.js').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Dot reporter', function() {
-  var stdout;
   var runner;
   var useColors;
   var windowWidth;
   var color;
-  var showOutput = false;
-
-  /**
-   * Run reporter using stream reassignment to capture output.
-   *
-   * @param {Object} stubSelf - Reporter-like stub instance
-   * @param {Runner} runner - Mock instance
-   * @param {boolean} [tee=false] - If `true`, echo captured output to screen
-   */
-  function runReporter(stubSelf, runner, tee) {
-    // Reassign stream in order to make a copy of all reporter output
-    var stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      if (tee) {
-        stdoutWrite.call(process.stdout, string, enc, callback);
-      }
-    };
-
-    // Invoke reporter
-    Dot.call(stubSelf, runner);
-
-    // Revert stream reassignment here so reporter output
-    // can't be corrupted if any test assertions throw
-    process.stdout.write = stdoutWrite;
-  }
+  var options = {};
+  var runReporter = makeRunReporter(Dot);
 
   beforeEach(function() {
-    stdout = [];
     useColors = Base.useColors;
     windowWidth = Base.window.width;
     color = Base.color;
@@ -61,7 +36,7 @@ describe('Dot reporter', function() {
   describe('on start', function() {
     it('should write a newline', function() {
       runner = createMockRunner('start', 'start');
-      runReporter({epilogue: function() {}}, runner, showOutput);
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
       var expectedArray = ['\n'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -73,7 +48,7 @@ describe('Dot reporter', function() {
       });
       it('should write a newline followed by a comma', function() {
         runner = createMockRunner('pending', 'pending');
-        runReporter({epilogue: function() {}}, runner, showOutput);
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedArray = ['\n  ', 'pending_' + Base.symbols.comma];
         expect(stdout, 'to equal', expectedArray);
       });
@@ -81,7 +56,7 @@ describe('Dot reporter', function() {
     describe('if window width is equal to or less than 1', function() {
       it('should write a comma', function() {
         runner = createMockRunner('pending', 'pending');
-        runReporter({epilogue: function() {}}, runner, showOutput);
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedArray = ['pending_' + Base.symbols.comma];
         expect(stdout, 'to equal', expectedArray);
       });
@@ -101,7 +76,7 @@ describe('Dot reporter', function() {
       describe('if test speed is fast', function() {
         it('should write a newline followed by a dot', function() {
           runner = createMockRunner('pass', 'pass', null, null, test);
-          runReporter({epilogue: function() {}}, runner, showOutput);
+          var stdout = runReporter({epilogue: function() {}}, runner, options);
           expect(test.speed, 'to equal', 'fast');
           var expectedArray = ['\n  ', 'fast_' + Base.symbols.dot];
           expect(stdout, 'to equal', expectedArray);
@@ -112,7 +87,7 @@ describe('Dot reporter', function() {
       describe('if test speed is fast', function() {
         it('should write a grey dot', function() {
           runner = createMockRunner('pass', 'pass', null, null, test);
-          runReporter({epilogue: function() {}}, runner, showOutput);
+          var stdout = runReporter({epilogue: function() {}}, runner, options);
           expect(test.speed, 'to equal', 'fast');
           var expectedArray = ['fast_' + Base.symbols.dot];
           expect(stdout, 'to equal', expectedArray);
@@ -122,7 +97,7 @@ describe('Dot reporter', function() {
         it('should write a yellow dot', function() {
           test.duration = 2;
           runner = createMockRunner('pass', 'pass', null, null, test);
-          runReporter({epilogue: function() {}}, runner, showOutput);
+          var stdout = runReporter({epilogue: function() {}}, runner, options);
           expect(test.speed, 'to equal', 'medium');
           var expectedArray = ['medium_' + Base.symbols.dot];
           expect(stdout, 'to equal', expectedArray);
@@ -132,7 +107,7 @@ describe('Dot reporter', function() {
         it('should write a bright yellow dot', function() {
           test.duration = 3;
           runner = createMockRunner('pass', 'pass', null, null, test);
-          runReporter({epilogue: function() {}}, runner, showOutput);
+          var stdout = runReporter({epilogue: function() {}}, runner, options);
           expect(test.speed, 'to equal', 'slow');
           var expectedArray = ['bright-yellow_' + Base.symbols.dot];
           expect(stdout, 'to equal', expectedArray);
@@ -152,7 +127,7 @@ describe('Dot reporter', function() {
       });
       it('should write a newline followed by an exclamation mark', function() {
         runner = createMockRunner('fail', 'fail', null, null, test);
-        runReporter({epilogue: function() {}}, runner, showOutput);
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedArray = ['\n  ', 'fail_' + Base.symbols.bang];
         expect(stdout, 'to equal', expectedArray);
       });
@@ -160,7 +135,7 @@ describe('Dot reporter', function() {
     describe('if window width is equal to or less than 1', function() {
       it('should write an exclamation mark', function() {
         runner = createMockRunner('fail', 'fail', null, null, test);
-        runReporter({epilogue: function() {}}, runner, showOutput);
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedArray = ['fail_' + Base.symbols.bang];
         expect(stdout, 'to equal', expectedArray);
       });
@@ -173,7 +148,7 @@ describe('Dot reporter', function() {
       var epilogue = function() {
         epilogueCalled = true;
       };
-      runReporter({epilogue: epilogue}, runner, showOutput);
+      runReporter({epilogue: epilogue}, runner, options);
       expect(epilogueCalled, 'to be', true);
     });
   });
diff --git a/test/reporters/helpers.js b/test/reporters/helpers.js
index e0c6604216..4da36fa600 100644
--- a/test/reporters/helpers.js
+++ b/test/reporters/helpers.js
@@ -155,9 +155,51 @@ function makeExpectedTest(
   };
 }
 
+/**
+ * Creates closure with reference to the reporter class constructor.
+ *
+ * @param {Function} ctor - Reporter class constructor
+ * @return {createRunReporterFunction~runReporter}
+ */
+function createRunReporterFunction(ctor) {
+  /**
+   * Run reporter using stream reassignment to capture output.
+   *
+   * @param {Object} stubSelf - Reporter-like stub instance
+   * @param {Runner} runner - Mock instance
+   * @param {Object} [options] - Reporter configuration settings
+   * @param {boolean} [tee=false] - Whether to echo output to screen
+   * @return {string[]} Lines of output written to `stdout`
+   */
+  var runReporter = function(stubSelf, runner, options, tee) {
+    var stdout = [];
+
+    // Reassign stream in order to make a copy of all reporter output
+    var stdoutWrite = process.stdout.write;
+    process.stdout.write = function(string, enc, callback) {
+      stdout.push(string);
+      if (tee) {
+        stdoutWrite.call(process.stdout, string, enc, callback);
+      }
+    };
+
+    // Invoke reporter
+    ctor.call(stubSelf, runner, options);
+
+    // Revert stream reassignment here so reporter output
+    // can't be corrupted if any test assertions throw
+    process.stdout.write = stdoutWrite;
+
+    return stdout;
+  };
+
+  return runReporter;
+}
+
 module.exports = {
-  createMockRunner: createMockRunner,
-  makeTest: makeTest,
   createElements: createElements,
-  makeExpectedTest: makeExpectedTest
+  createMockRunner: createMockRunner,
+  createRunReporterFunction: createRunReporterFunction,
+  makeExpectedTest: makeExpectedTest,
+  makeTest: makeTest
 };
diff --git a/test/reporters/json-stream.spec.js b/test/reporters/json-stream.spec.js
index 3c5e1831f2..8c9fcfff35 100644
--- a/test/reporters/json-stream.spec.js
+++ b/test/reporters/json-stream.spec.js
@@ -5,12 +5,12 @@ var JSONStream = reporters.JSONStream;
 
 var createMockRunner = require('./helpers').createMockRunner;
 var makeExpectedTest = require('./helpers').makeExpectedTest;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
-describe('Json Stream reporter', function() {
+describe('JSON Stream reporter', function() {
   var runner;
-  var stdout;
-  var stdoutWrite;
-
+  var options = {};
+  var runReporter = makeRunReporter(JSONStream);
   var expectedTitle = 'some title';
   var expectedFullTitle = 'full title';
   var expectedDuration = 1000;
@@ -27,17 +27,8 @@ describe('Json Stream reporter', function() {
     message: expectedErrorMessage
   };
 
-  beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
-  });
-
   afterEach(function() {
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start', function() {
@@ -45,9 +36,7 @@ describe('Json Stream reporter', function() {
       runner = createMockRunner('start', 'start');
       var expectedTotal = 12;
       runner.total = expectedTotal;
-      JSONStream.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       expect(
         stdout[0],
@@ -60,9 +49,7 @@ describe('Json Stream reporter', function() {
   describe('on pass', function() {
     it('should write stringified test data', function() {
       runner = createMockRunner('pass', 'pass', null, null, expectedTest);
-      JSONStream.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       expect(
         stdout[0],
@@ -93,9 +80,7 @@ describe('Json Stream reporter', function() {
           expectedError
         );
 
-        JSONStream.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         expect(
           stdout[0],
@@ -129,8 +114,7 @@ describe('Json Stream reporter', function() {
           expectedError
         );
 
-        JSONStream.call({}, runner);
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter(this, runner, options);
 
         expect(
           stdout[0],
@@ -154,8 +138,7 @@ describe('Json Stream reporter', function() {
   describe('on end', function() {
     it('should write end details', function() {
       runner = createMockRunner('end', 'end');
-      JSONStream.call({}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter(this, runner, options);
       expect(stdout[0], 'to match', /end/);
     });
   });
diff --git a/test/reporters/landing.spec.js b/test/reporters/landing.spec.js
index da4134ce70..1fee291e41 100644
--- a/test/reporters/landing.spec.js
+++ b/test/reporters/landing.spec.js
@@ -5,11 +5,12 @@ var Landing = reporters.Landing;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Landing reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(Landing);
   var useColors;
   var windowWidth;
   var resetCode = '\u001b[0m';
@@ -25,12 +26,6 @@ describe('Landing reporter', function() {
   ];
 
   beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
     useColors = Base.useColors;
     Base.useColors = false;
     windowWidth = Base.window.width;
@@ -40,7 +35,7 @@ describe('Landing reporter', function() {
   afterEach(function() {
     Base.useColors = useColors;
     Base.window.width = windowWidth;
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start', function() {
@@ -48,9 +43,7 @@ describe('Landing reporter', function() {
       var cachedCursor = Base.cursor;
       Base.cursor.hide = function() {};
       runner = createMockRunner('start', 'start');
-      Landing.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       expect(stdout[0], 'to equal', '\n\n\n  ');
       Base.cursor = cachedCursor;
@@ -63,9 +56,7 @@ describe('Landing reporter', function() {
         calledCursorHide = true;
       };
       runner = createMockRunner('start', 'start');
-      Landing.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      runReporter({}, runner, options);
       expect(calledCursorHide, 'to be', true);
 
       Base.cursor = cachedCursor;
@@ -80,9 +71,7 @@ describe('Landing reporter', function() {
         };
         runner = createMockRunner('test end', 'test end', null, null, test);
         runner.total = 12;
-        Landing.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         expect(stdout, 'to equal', expectedArray);
       });
@@ -94,9 +83,7 @@ describe('Landing reporter', function() {
         };
         runner = createMockRunner('test end', 'test end', null, null, test);
 
-        Landing.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         expect(stdout, 'to equal', expectedArray);
       });
@@ -112,16 +99,16 @@ describe('Landing reporter', function() {
       runner = createMockRunner('end', 'end');
 
       var calledEpilogue = false;
-      Landing.call(
+      runReporter(
         {
           epilogue: function() {
             calledEpilogue = true;
           }
         },
-        runner
+        runner,
+        options
       );
 
-      process.stdout.write = stdoutWrite;
       expect(calledEpilogue, 'to be', true);
       expect(calledCursorShow, 'to be', true);
 
diff --git a/test/reporters/list.spec.js b/test/reporters/list.spec.js
index 7b9c74fb43..78895796c4 100644
--- a/test/reporters/list.spec.js
+++ b/test/reporters/list.spec.js
@@ -5,11 +5,12 @@ var List = reporters.List;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('List reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(List);
   var useColors;
   var expectedTitle = 'some title';
   var expectedDuration = 100;
@@ -21,27 +22,20 @@ describe('List reporter', function() {
     slow: function() {}
   };
   beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
     useColors = Base.useColors;
     Base.useColors = false;
   });
 
   afterEach(function() {
     Base.useColors = useColors;
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start and test', function() {
     it('should write expected new line and title to the console', function() {
       runner = createMockRunner('start test', 'start', 'test', null, test);
-      List.call({epilogue: function() {}}, runner);
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
-      process.stdout.write = stdoutWrite;
       var startString = '\n';
       var testString = '    ' + expectedTitle + ': ';
       var expectedArray = [startString, testString];
@@ -51,9 +45,7 @@ describe('List reporter', function() {
   describe('on pending', function() {
     it('should write expected title to the console', function() {
       runner = createMockRunner('pending test', 'pending', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
       expect(stdout[0], 'to equal', '  - ' + expectedTitle + '\n');
     });
@@ -66,9 +58,7 @@ describe('List reporter', function() {
         calledCursorCR = true;
       };
       runner = createMockRunner('pass', 'pass', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      runReporter({epilogue: function() {}}, runner, options);
 
       expect(calledCursorCR, 'to be', true);
 
@@ -81,9 +71,7 @@ describe('List reporter', function() {
       var cachedCursor = Base.cursor;
       Base.cursor.CR = function() {};
       runner = createMockRunner('pass', 'pass', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
       expect(
         stdout[0],
@@ -109,9 +97,7 @@ describe('List reporter', function() {
         calledCursorCR = true;
       };
       runner = createMockRunner('fail', 'fail', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      runReporter({epilogue: function() {}}, runner, options);
 
       expect(calledCursorCR, 'to be', true);
 
@@ -122,9 +108,7 @@ describe('List reporter', function() {
       var expectedErrorCount = 1;
       Base.cursor.CR = function() {};
       runner = createMockRunner('fail', 'fail', null, null, test);
-      List.call({epilogue: function() {}}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
       expect(
         stdout[0],
@@ -140,6 +124,7 @@ describe('List reporter', function() {
       var checked = false;
       var err;
       test = {};
+      runner = createMockRunner('fail', 'fail', null, null, test);
       runner.on = runner.once = function(event, callback) {
         if (!checked && event === 'fail') {
           err = new Error('fake failure object with actual/expected');
@@ -150,9 +135,8 @@ describe('List reporter', function() {
           checked = true;
         }
       };
-      List.call({epilogue: function() {}}, runner);
+      runReporter({epilogue: function() {}}, runner, options);
 
-      process.stdout.write = stdoutWrite;
       expect(typeof err.actual, 'to be', 'string');
       expect(typeof err.expected, 'to be', 'string');
     });
@@ -162,15 +146,15 @@ describe('List reporter', function() {
     it('should call epilogue', function() {
       var calledEpilogue = false;
       runner = createMockRunner('end', 'end');
-      List.call(
+      runReporter(
         {
           epilogue: function() {
             calledEpilogue = true;
           }
         },
-        runner
+        runner,
+        options
       );
-      process.stdout.write = stdoutWrite;
 
       expect(calledEpilogue, 'to be', true);
     });
diff --git a/test/reporters/markdown.spec.js b/test/reporters/markdown.spec.js
index f6fc63f119..842d7c7643 100644
--- a/test/reporters/markdown.spec.js
+++ b/test/reporters/markdown.spec.js
@@ -4,26 +4,18 @@ var reporters = require('../../').reporters;
 var Markdown = reporters.Markdown;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Markdown reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(Markdown);
   var expectedTitle = 'expected title';
   var expectedFullTitle = 'full title';
   var sluggedFullTitle = 'full-title';
 
-  beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
-  });
-
   afterEach(function() {
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe("on 'suite'", function() {
@@ -51,8 +43,7 @@ describe('Markdown reporter', function() {
         expectedSuite
       );
       runner.suite = expectedSuite;
-      Markdown.call({}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       var expectedArray = [
         '# TOC\n',
@@ -97,8 +88,7 @@ describe('Markdown reporter', function() {
       };
       runner = createMockRunner('pass end', 'pass', 'end', null, expectedTest);
       runner.suite = expectedSuite;
-      Markdown.call({}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       var expectedArray = [
         '# TOC\n',
diff --git a/test/reporters/min.spec.js b/test/reporters/min.spec.js
index 30b05126ec..f88adf31d7 100644
--- a/test/reporters/min.spec.js
+++ b/test/reporters/min.spec.js
@@ -4,31 +4,22 @@ var reporters = require('../../').reporters;
 var Min = reporters.Min;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Min reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
-
-  beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
-  });
+  var options = {};
+  var runReporter = makeRunReporter(Min);
 
   afterEach(function() {
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start', function() {
     it('should clear screen then set cursor position', function() {
       runner = createMockRunner('start', 'start');
-      Min.call({epilogue: function() {}}, runner);
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
 
-      process.stdout.write = stdoutWrite;
       var expectedArray = ['\u001b[2J', '\u001b[1;3H'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -38,15 +29,15 @@ describe('Min reporter', function() {
     it('should call epilogue', function() {
       var calledEpilogue = false;
       runner = createMockRunner('end', 'end');
-      Min.call(
+      runReporter(
         {
           epilogue: function() {
             calledEpilogue = true;
           }
         },
-        runner
+        runner,
+        options
       );
-      process.stdout.write = stdoutWrite;
 
       expect(calledEpilogue, 'to be', true);
     });
diff --git a/test/reporters/nyan.spec.js b/test/reporters/nyan.spec.js
index 1296ab1195..96bbda6766 100644
--- a/test/reporters/nyan.spec.js
+++ b/test/reporters/nyan.spec.js
@@ -5,41 +5,33 @@ var NyanCat = reporters.Nyan;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Nyan reporter', function() {
   describe('events', function() {
     var runner;
-    var stdout;
-    var stdoutWrite;
     var calledDraw;
-
-    beforeEach(function() {
-      stdout = [];
-      stdoutWrite = process.stdout.write;
-      process.stdout.write = function(string, enc, callback) {
-        stdout.push(string);
-        stdoutWrite.call(process.stdout, string, enc, callback);
-      };
-    });
+    var options = {};
+    var runReporter = makeRunReporter(NyanCat);
 
     afterEach(function() {
-      process.stdout.write = stdoutWrite;
+      runner = undefined;
     });
 
     describe('on start', function() {
       it('should call draw', function() {
         calledDraw = false;
         runner = createMockRunner('start', 'start');
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {
               calledDraw = true;
             },
             generateColors: function() {}
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledDraw, 'to be', true);
       });
@@ -48,16 +40,16 @@ describe('Nyan reporter', function() {
       it('should call draw', function() {
         calledDraw = false;
         runner = createMockRunner('pending', 'pending');
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {
               calledDraw = true;
             },
             generateColors: function() {}
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledDraw, 'to be', true);
       });
@@ -70,16 +62,16 @@ describe('Nyan reporter', function() {
           slow: function() {}
         };
         runner = createMockRunner('pass', 'pass', null, null, test);
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {
               calledDraw = true;
             },
             generateColors: function() {}
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledDraw, 'to be', true);
       });
@@ -91,16 +83,16 @@ describe('Nyan reporter', function() {
           err: ''
         };
         runner = createMockRunner('fail', 'fail', null, null, test);
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {
               calledDraw = true;
             },
             generateColors: function() {}
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledDraw, 'to be', true);
       });
@@ -109,7 +101,7 @@ describe('Nyan reporter', function() {
       it('should call epilogue', function() {
         var calledEpilogue = false;
         runner = createMockRunner('end', 'end');
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {},
             generateColors: function() {},
@@ -117,28 +109,28 @@ describe('Nyan reporter', function() {
               calledEpilogue = true;
             }
           },
-          runner
+          runner,
+          options
         );
-        process.stdout.write = stdoutWrite;
 
         expect(calledEpilogue, 'to be', true);
       });
       it('should write numberOfLines amount of new lines', function() {
         var expectedNumberOfLines = 4;
         runner = createMockRunner('end', 'end');
-        NyanCat.call(
+        var stdout = runReporter(
           {
             draw: function() {},
             generateColors: function() {},
             epilogue: function() {}
           },
-          runner
+          runner,
+          options
         );
 
         var arrayOfNewlines = stdout.filter(function(value) {
           return value === '\n';
         });
-        process.stdout.write = stdoutWrite;
 
         expect(arrayOfNewlines, 'to have length', expectedNumberOfLines);
       });
@@ -149,16 +141,16 @@ describe('Nyan reporter', function() {
           showCalled = true;
         };
         runner = createMockRunner('end', 'end');
-        NyanCat.call(
+        runReporter(
           {
             draw: function() {},
             generateColors: function() {},
             epilogue: function() {}
           },
-          runner
+          runner,
+          options
         );
 
-        process.stdout.write = stdoutWrite;
         expect(showCalled, 'to be', true);
         Base.cursor.show = cachedShow;
       });
@@ -199,7 +191,6 @@ describe('Nyan reporter', function() {
           cursorUp: function() {}
         });
 
-        process.stdout.write = stdoutWrite;
         var expectedArray = [
           '\u001b[0C',
           '_,------,',
@@ -235,7 +226,6 @@ describe('Nyan reporter', function() {
           cursorUp: function() {}
         });
 
-        // process.stdout.write = stdoutWrite;
         var expectedArray = [
           '\u001b[0C',
           '_,------,',
@@ -276,7 +266,6 @@ describe('Nyan reporter', function() {
       var expectedNumber = 25;
 
       nyanCat.cursorDown(expectedNumber);
-      process.stdout.write = stdoutWrite;
       var expectedArray = ['\u001b[' + expectedNumber + 'B'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -303,7 +292,6 @@ describe('Nyan reporter', function() {
       var expectedNumber = 25;
 
       nyanCat.cursorUp(expectedNumber);
-      process.stdout.write = stdoutWrite;
       var expectedArray = ['\u001b[' + expectedNumber + 'A'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -432,13 +420,16 @@ describe('Nyan reporter', function() {
     var stdoutWrite;
     var stdout;
     var cachedColor;
+    var showOutput = false;
 
     beforeEach(function() {
       stdout = [];
       stdoutWrite = process.stdout.write;
       process.stdout.write = function(string, enc, callback) {
         stdout.push(string);
-        stdoutWrite.call(process.stdout, string, enc, callback);
+        if (showOutput) {
+          stdoutWrite.call(process.stdout, string, enc, callback);
+        }
       };
       cachedColor = Base.color;
       Base.color = function(type, n) {
@@ -496,13 +487,16 @@ describe('Nyan reporter', function() {
   describe('drawRainbow', function() {
     var stdoutWrite;
     var stdout;
+    var showOutput = false;
 
     beforeEach(function() {
       stdout = [];
       stdoutWrite = process.stdout.write;
       process.stdout.write = function(string, enc, callback) {
         stdout.push(string);
-        stdoutWrite.call(process.stdout, string, enc, callback);
+        if (showOutput) {
+          stdoutWrite.call(process.stdout, string, enc, callback);
+        }
       };
     });
 
@@ -524,7 +518,6 @@ describe('Nyan reporter', function() {
         numberOfLines: 1
       });
 
-      process.stdout.write = stdoutWrite;
       var expectedArray = [
         '\u001b[' + expectedWidth + 'C',
         expectedContents,
diff --git a/test/reporters/progress.spec.js b/test/reporters/progress.spec.js
index ef08ea5582..f19bb3988d 100644
--- a/test/reporters/progress.spec.js
+++ b/test/reporters/progress.spec.js
@@ -5,11 +5,13 @@ var Progress = reporters.Progress;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Progress reporter', function() {
   var stdout;
   var stdoutWrite;
   var runner;
+  var runReporter = makeRunReporter(Progress);
 
   beforeEach(function() {
     stdout = [];
@@ -32,9 +34,8 @@ describe('Progress reporter', function() {
         calledCursorHide = true;
       };
       runner = createMockRunner('start', 'start');
-      Progress.call({}, runner);
+      runReporter({}, runner, {});
 
-      process.stdout.write = stdoutWrite;
       expect(calledCursorHide, 'to be', true);
 
       Base.cursor = cachedCursor;
@@ -55,9 +56,7 @@ describe('Progress reporter', function() {
         var expectedOptions = {};
         runner = createMockRunner('test end', 'test end');
         runner.total = expectedTotal;
-        Progress.call({}, runner, expectedOptions);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, expectedOptions);
 
         expect(stdout, 'to equal', []);
 
@@ -93,9 +92,8 @@ describe('Progress reporter', function() {
         };
         runner = createMockRunner('test end', 'test end');
         runner.total = expectedTotal;
-        Progress.call({}, runner, options);
+        var stdout = runReporter({}, runner, options);
 
-        process.stdout.write = stdoutWrite;
         var expectedArray = [
           '\u001b[J',
           '  ' + expectedOpen,
@@ -122,16 +120,16 @@ describe('Progress reporter', function() {
       };
       runner = createMockRunner('end', 'end');
       var calledEpilogue = false;
-      Progress.call(
+      runReporter(
         {
           epilogue: function() {
             calledEpilogue = true;
           }
         },
-        runner
+        runner,
+        {}
       );
 
-      process.stdout.write = stdoutWrite;
       expect(calledEpilogue, 'to be', true);
       expect(calledCursorShow, 'to be', true);
 
diff --git a/test/reporters/spec.spec.js b/test/reporters/spec.spec.js
index 2f4eee33e5..5f7584e33e 100644
--- a/test/reporters/spec.spec.js
+++ b/test/reporters/spec.spec.js
@@ -5,28 +5,23 @@ var Spec = reporters.Spec;
 var Base = reporters.Base;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('Spec reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(Spec);
   var useColors;
   var expectedTitle = 'expectedTitle';
 
   beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
     useColors = Base.useColors;
     Base.useColors = false;
   });
 
   afterEach(function() {
     Base.useColors = useColors;
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on suite', function() {
@@ -35,8 +30,7 @@ describe('Spec reporter', function() {
         title: expectedTitle
       };
       runner = createMockRunner('suite', 'suite', null, null, suite);
-      Spec.call({epilogue: function() {}}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
       var expectedArray = [expectedTitle + '\n'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -47,8 +41,7 @@ describe('Spec reporter', function() {
         title: expectedTitle
       };
       runner = createMockRunner('pending test', 'pending', null, null, suite);
-      Spec.call({epilogue: function() {}}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
       var expectedArray = ['  - ' + expectedTitle + '\n'];
       expect(stdout, 'to equal', expectedArray);
     });
@@ -65,8 +58,7 @@ describe('Spec reporter', function() {
           }
         };
         runner = createMockRunner('pass', 'pass', null, null, test);
-        Spec.call({epilogue: function() {}}, runner);
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedString =
           '  ' +
           Base.symbols.ok +
@@ -90,8 +82,7 @@ describe('Spec reporter', function() {
           }
         };
         runner = createMockRunner('pass', 'pass', null, null, test);
-        Spec.call({epilogue: function() {}}, runner);
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({epilogue: function() {}}, runner, options);
         var expectedString =
           '  ' + Base.symbols.ok + ' ' + expectedTitle + '\n';
         expect(stdout[0], 'to be', expectedString);
@@ -105,8 +96,7 @@ describe('Spec reporter', function() {
         title: expectedTitle
       };
       runner = createMockRunner('fail', 'fail', null, null, test);
-      Spec.call({epilogue: function() {}}, runner);
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({epilogue: function() {}}, runner, options);
       var expectedArray = ['  ' + functionCount + ') ' + expectedTitle + '\n'];
       expect(stdout, 'to equal', expectedArray);
     });
diff --git a/test/reporters/tap.spec.js b/test/reporters/tap.spec.js
index 5030cbde5c..208a88b904 100644
--- a/test/reporters/tap.spec.js
+++ b/test/reporters/tap.spec.js
@@ -4,22 +4,17 @@ var reporters = require('../../').reporters;
 var TAP = reporters.TAP;
 
 var createMockRunner = require('./helpers').createMockRunner;
+var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('TAP reporter', function() {
-  var stdout;
-  var stdoutWrite;
   var runner;
+  var options = {};
+  var runReporter = makeRunReporter(TAP);
   var expectedTitle = 'some title';
   var countAfterTestEnd = 2;
   var test;
 
   beforeEach(function() {
-    stdout = [];
-    stdoutWrite = process.stdout.write;
-    process.stdout.write = function(string, enc, callback) {
-      stdout.push(string);
-      stdoutWrite.call(process.stdout, string, enc, callback);
-    };
     test = {
       fullTitle: function() {
         return expectedTitle;
@@ -29,7 +24,7 @@ describe('TAP reporter', function() {
   });
 
   afterEach(function() {
-    process.stdout.write = stdoutWrite;
+    runner = undefined;
   });
 
   describe('on start', function() {
@@ -43,10 +38,9 @@ describe('TAP reporter', function() {
         expectedString = string;
         return expectedTotal;
       };
-      TAP.call({}, runner);
+      var stdout = runReporter({}, runner, options);
 
       var expectedArray = ['1..' + expectedTotal + '\n'];
-      process.stdout.write = stdoutWrite;
 
       expect(stdout, 'to equal', expectedArray);
       expect(expectedString, 'to be', expectedSuite);
@@ -64,9 +58,7 @@ describe('TAP reporter', function() {
       );
       runner.suite = '';
       runner.grepTotal = function() {};
-      TAP.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       var expectedMessage =
         'ok ' + countAfterTestEnd + ' ' + expectedTitle + ' # SKIP -\n';
@@ -80,9 +72,7 @@ describe('TAP reporter', function() {
 
       runner.suite = '';
       runner.grepTotal = function() {};
-      TAP.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       var expectedMessage =
         'ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n';
@@ -105,6 +95,7 @@ describe('TAP reporter', function() {
         var error = {
           message: expectedErrorMessage
         };
+        runner = createMockRunner('test end fail', 'test end', 'fail');
         runner.on = function(event, callback) {
           if (event === 'test end') {
             callback();
@@ -115,9 +106,7 @@ describe('TAP reporter', function() {
         };
         runner.suite = '';
         runner.grepTotal = function() {};
-        TAP.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         var expectedArray = [
           'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
@@ -142,9 +131,7 @@ describe('TAP reporter', function() {
         );
         runner.suite = '';
         runner.grepTotal = function() {};
-        TAP.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         var expectedArray = [
           'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
@@ -169,6 +156,7 @@ describe('TAP reporter', function() {
           stack: expectedStack,
           message: expectedErrorMessage
         };
+        runner = createMockRunner('test end fail', 'test end', 'fail');
         runner.on = function(event, callback) {
           if (event === 'test end') {
             callback();
@@ -179,9 +167,7 @@ describe('TAP reporter', function() {
         };
         runner.suite = '';
         runner.grepTotal = function() {};
-        TAP.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         var expectedArray = [
           'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
@@ -193,6 +179,7 @@ describe('TAP reporter', function() {
     });
     describe('if there is no error stack or error message', function() {
       it('should write expected message only', function() {
+        runner = createMockRunner('test end fail', 'test end', 'fail');
         var error = {};
         runner.on = runner.once = function(event, callback) {
           if (event === 'test end') {
@@ -204,9 +191,7 @@ describe('TAP reporter', function() {
         };
         runner.suite = '';
         runner.grepTotal = function() {};
-        TAP.call({}, runner);
-
-        process.stdout.write = stdoutWrite;
+        var stdout = runReporter({}, runner, options);
 
         var expectedArray = [
           'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n'
@@ -223,9 +208,7 @@ describe('TAP reporter', function() {
       runner = createMockRunner('fail end pass', 'fail', 'end', 'pass', test);
       runner.suite = '';
       runner.grepTotal = function() {};
-      TAP.call({}, runner);
-
-      process.stdout.write = stdoutWrite;
+      var stdout = runReporter({}, runner, options);
 
       var totalTests = numberOfPasses + numberOfFails;
       var expectedArray = [

From 614d35b58173102f96b6ee20f8fc170df1475db6 Mon Sep 17 00:00:00 2001
From: Christopher Hiller 
Date: Fri, 2 Nov 2018 15:56:37 -0700
Subject: [PATCH 15/18] update list of published files in package.json

- this will fix a problem with npm (potentially) publishing certain files
which should always be ignored; see npm/npm-packlist#14.
- also fixes issue with growl images not getting published!
- add `directories` property for metadata

PRO TIP: use `npm pack --dry-run` to see what would be included in
published tarball.
---
 package.json | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/package.json b/package.json
index 5cd30f5a17..f85057cb46 100644
--- a/package.json
+++ b/package.json
@@ -510,14 +510,18 @@
     "watchify": "^3.7.0"
   },
   "files": [
-    "bin",
-    "images",
-    "lib",
+    "bin/{*mocha,options.js}",
+    "assets/growl/*.png",
+    "lib/**/*.{js,html}",
     "index.js",
     "mocha.css",
     "mocha.js",
     "browser-entry.js"
   ],
+  "directories": {
+    "lib": "lib",
+    "test": "test"
+  },
   "browser": {
     "growl": "./lib/browser/growl.js",
     "tty": "./lib/browser/tty.js",
@@ -527,10 +531,13 @@
     "path": false,
     "supports-color": false
   },
-  "homepage": "https://mochajs.org",
+  "homepage": "https://mochajs.org/",
   "logo": "https://cldup.com/S9uQ-cOLYz.svg",
   "prettier": {
     "singleQuote": true,
     "bracketSpacing": false
+  },
+  "bugs": {
+    "url": "https://github.com/mochajs/mocha/issues/"
   }
 }

From c6f61e6b564993b5476b04a804fa4b8706c35edf Mon Sep 17 00:00:00 2001
From: "P. Roebuck" 
Date: Fri, 9 Nov 2018 06:42:53 -0600
Subject: [PATCH 16/18] Prevent timeout value from skirting limit check (#3536)

* fix(lib/runnable.js): Prevent timeout value from skirting limit check
   - Moved the timestring->value translation code to before the limit check.
   - Found that previous "fix" hadn't actually fixed the correct value, as the wrong upper bound value had been used (off by one error).
   - Further research indicated that some versions of IE had problems with negative timeout values. New code clamps to nonnegative numbers ending at `INT_MAX`.
   - Updated the function documentation.

* feat(lib/utils.js): Add `clamp` function
   - New function clamps a numeric value to a given range.

* test(unit/runnable.spec.js): Updated tests for `#timeout(ms)`
   - Restructured `Runnable#timeout` tests to check both numeric/timestamp input values that:
      - less than lower limit
      - equal to lower limit
      - within limits
      - equal to upper limit
      - greater than upper limit

Closes #1652
---
 lib/runnable.js            | 36 ++++++++++++----
 lib/utils.js               | 11 +++++
 test/unit/runnable.spec.js | 86 +++++++++++++++++++++++++++++++++-----
 3 files changed, 115 insertions(+), 18 deletions(-)

diff --git a/lib/runnable.js b/lib/runnable.js
index 73da817793..2e62a6bc8b 100644
--- a/lib/runnable.js
+++ b/lib/runnable.js
@@ -50,23 +50,43 @@ function Runnable(title, fn) {
 utils.inherits(Runnable, EventEmitter);
 
 /**
- * Set & get timeout `ms`.
+ * Get current timeout value in msecs.
  *
- * @api private
- * @param {number|string} ms
- * @return {Runnable|number} ms or Runnable instance.
+ * @private
+ * @returns {number} current timeout threshold value
+ */
+/**
+ * @summary
+ * Set timeout threshold value (msecs).
+ *
+ * @description
+ * A string argument can use shorthand (e.g., "2s") and will be converted.
+ * The value will be clamped to range [0, 2^31-1].
+ * If clamped value matches either range endpoint, timeouts will be disabled.
+ *
+ * @private
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout#Maximum_delay_value}
+ * @param {number|string} ms - Timeout threshold value.
+ * @returns {Runnable} this
+ * @chainable
  */
 Runnable.prototype.timeout = function(ms) {
   if (!arguments.length) {
     return this._timeout;
   }
-  // see #1652 for reasoning
-  if (ms === 0 || ms > Math.pow(2, 31)) {
-    this._enableTimeouts = false;
-  }
   if (typeof ms === 'string') {
     ms = milliseconds(ms);
   }
+
+  // Clamp to range
+  var INT_MAX = Math.pow(2, 31) - 1;
+  var range = [0, INT_MAX];
+  ms = utils.clamp(ms, range);
+
+  // see #1652 for reasoning
+  if (ms === range[0] || ms === range[1]) {
+    this._enableTimeouts = false;
+  }
   debug('timeout %d', ms);
   this._timeout = ms;
   if (this.timer) {
diff --git a/lib/utils.js b/lib/utils.js
index 71ea6ce4fb..6e78a99b71 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -691,6 +691,17 @@ exports.isPromise = function isPromise(value) {
   return typeof value === 'object' && typeof value.then === 'function';
 };
 
+/**
+ * Clamps a numeric value to an inclusive range.
+ *
+ * @param {number} value - Value to be clamped.
+ * @param {numer[]} range - Two element array specifying [min, max] range.
+ * @returns {number} clamped value
+ */
+exports.clamp = function clamp(value, range) {
+  return Math.min(Math.max(value, range[0]), range[1]);
+};
+
 /**
  * It's a noop.
  * @api
diff --git a/test/unit/runnable.spec.js b/test/unit/runnable.spec.js
index 9ea00b6d07..0fb29db6e5 100644
--- a/test/unit/runnable.spec.js
+++ b/test/unit/runnable.spec.js
@@ -46,18 +46,84 @@ describe('Runnable(title, fn)', function() {
   });
 
   describe('#timeout(ms)', function() {
-    it('should set the timeout', function() {
-      var run = new Runnable();
-      run.timeout(1000);
-      assert(run.timeout() === 1000);
+    var MIN_TIMEOUT = 0;
+    var MAX_TIMEOUT = 2147483647; // INT_MAX (32-bit signed integer)
+
+    describe('when value is less than lower bound', function() {
+      it('should clamp to lower bound given numeric', function() {
+        var run = new Runnable();
+        run.timeout(-1);
+        assert(run.timeout() === MIN_TIMEOUT);
+      });
+      // :TODO: Our internal version of `ms` can't handle negative time,
+      // but package version can. Skip this check until that change is merged.
+      it.skip('should clamp to lower bound given timestamp', function() {
+        var run = new Runnable();
+        run.timeout('-1 ms');
+        assert(run.timeout() === MIN_TIMEOUT);
+      });
     });
-  });
 
-  describe('#timeout(ms) when ms>2^31', function() {
-    it('should set disabled', function() {
-      var run = new Runnable();
-      run.timeout(1e10);
-      assert(run.enableTimeouts() === false);
+    describe('when value is equal to lower bound', function() {
+      it('should set the value and disable timeouts given numeric', function() {
+        var run = new Runnable();
+        run.timeout(MIN_TIMEOUT);
+        assert(run.timeout() === MIN_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+      it('should set the value and disable timeouts given timestamp', function() {
+        var run = new Runnable();
+        run.timeout(MIN_TIMEOUT + 'ms');
+        assert(run.timeout() === MIN_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+    });
+
+    describe('when value is within `setTimeout` bounds', function() {
+      var oneSecond = 1000;
+
+      it('should set the timeout given numeric', function() {
+        var run = new Runnable();
+        run.timeout(oneSecond);
+        assert(run.timeout() === oneSecond);
+        assert(run.enableTimeouts() === true);
+      });
+      it('should set the timeout given timestamp', function() {
+        var run = new Runnable();
+        run.timeout('1s');
+        assert(run.timeout() === oneSecond);
+        assert(run.enableTimeouts() === true);
+      });
+    });
+
+    describe('when value is equal to upper bound', function() {
+      it('should set the value and disable timeout given numeric', function() {
+        var run = new Runnable();
+        run.timeout(MAX_TIMEOUT);
+        assert(run.timeout() === MAX_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+      it('should set the value and disable timeout given timestamp', function() {
+        var run = new Runnable();
+        run.timeout(MAX_TIMEOUT + 'ms');
+        assert(run.timeout() === MAX_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+    });
+
+    describe('when value is greater than `setTimeout` limit', function() {
+      it('should clamp to upper bound given numeric', function() {
+        var run = new Runnable();
+        run.timeout(MAX_TIMEOUT + 1);
+        assert(run.timeout() === MAX_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
+      it('should clamp to upper bound given timestamp', function() {
+        var run = new Runnable();
+        run.timeout('24.9d'); // 2151360000ms
+        assert(run.timeout() === MAX_TIMEOUT);
+        assert(run.enableTimeouts() === false);
+      });
     });
   });
 

From cd87a212177de5b1728d2a43c56c53266df169eb Mon Sep 17 00:00:00 2001
From: "P. Roebuck" 
Date: Sat, 10 Nov 2018 06:09:53 -0600
Subject: [PATCH 17/18] Make TAP reporter TAP13-capable (#3552)

* Refactored code to use `TAPProducer` objects to write output.
* Added TAP specification 13 output producer.
  * Version line
  * Errors and stacktraces inside YAML blocks
* Added TAP specification 12 output producer [default].
* Added `reporterOptions.tapVersion` to target TAP producer.
* Refactored to make use of `runner.stats`
* Refactored to use `process.stdout` stream exclusively.
* Updated to test against both specifications and incorporate work from PR #3528.
* Added integration tests for specification conformance.
---
 lib/reporters/tap.js                          | 264 ++++++++-
 .../integration/fixtures/reporters.fixture.js |  63 ++
 test/integration/reporters.spec.js            | 133 +++++
 test/reporters/tap.spec.js                    | 550 +++++++++++++-----
 4 files changed, 833 insertions(+), 177 deletions(-)
 create mode 100644 test/integration/fixtures/reporters.fixture.js

diff --git a/lib/reporters/tap.js b/lib/reporters/tap.js
index feaf7ea21a..ccff657e6a 100644
--- a/lib/reporters/tap.js
+++ b/lib/reporters/tap.js
@@ -6,7 +6,10 @@
  * Module dependencies.
  */
 
+var util = require('util');
 var Base = require('./base');
+var inherits = require('../utils').inherits;
+var sprintf = util.format;
 
 /**
  * Expose `TAP`.
@@ -15,25 +18,34 @@ var Base = require('./base');
 exports = module.exports = TAP;
 
 /**
- * Initialize a new `TAP` reporter.
+ * Constructs a new TAP reporter with runner instance and reporter options.
  *
  * @public
  * @class
- * @memberof Mocha.reporters
  * @extends Mocha.reporters.Base
- * @api public
- * @param {Runner} runner
+ * @memberof Mocha.reporters
+ * @param {Runner} runner - Instance triggers reporter actions.
+ * @param {Object} [options] - runner options
  */
-function TAP(runner) {
-  Base.call(this, runner);
+function TAP(runner, options) {
+  Base.call(this, runner, options);
 
+  var self = this;
   var n = 1;
-  var passes = 0;
-  var failures = 0;
 
-  runner.on('start', function() {
-    var total = runner.grepTotal(runner.suite);
-    console.log('%d..%d', 1, total);
+  var tapVersion = '12';
+  if (options && options.reporterOptions) {
+    if (options.reporterOptions.tapVersion) {
+      tapVersion = options.reporterOptions.tapVersion.toString();
+    }
+  }
+
+  this._producer = createProducer(tapVersion);
+
+  runner.once('start', function() {
+    var ntests = runner.grepTotal(runner.suite);
+    self._producer.writeVersion();
+    self._producer.writePlan(ntests);
   });
 
   runner.on('test end', function() {
@@ -41,39 +53,233 @@ function TAP(runner) {
   });
 
   runner.on('pending', function(test) {
-    console.log('ok %d %s # SKIP -', n, title(test));
+    self._producer.writePending(n, test);
   });
 
   runner.on('pass', function(test) {
-    passes++;
-    console.log('ok %d %s', n, title(test));
+    self._producer.writePass(n, test);
   });
 
   runner.on('fail', function(test, err) {
-    failures++;
-    console.log('not ok %d %s', n, title(test));
-    if (err.message) {
-      console.log(err.message.replace(/^/gm, '  '));
-    }
-    if (err.stack) {
-      console.log(err.stack.replace(/^/gm, '  '));
-    }
+    self._producer.writeFail(n, test, err);
   });
 
   runner.once('end', function() {
-    console.log('# tests ' + (passes + failures));
-    console.log('# pass ' + passes);
-    console.log('# fail ' + failures);
+    self._producer.writeEpilogue(runner.stats);
   });
 }
 
 /**
- * Return a TAP-safe title of `test`
+ * Inherit from `Base.prototype`.
+ */
+inherits(TAP, Base);
+
+/**
+ * Returns a TAP-safe title of `test`.
  *
- * @api private
- * @param {Object} test
- * @return {String}
+ * @private
+ * @param {Test} test - Test instance.
+ * @return {String} title with any hash character removed
  */
 function title(test) {
   return test.fullTitle().replace(/#/g, '');
 }
+
+/**
+ * Writes newline-terminated formatted string to reporter output stream.
+ *
+ * @private
+ * @param {string} format - `printf`-like format string
+ * @param {...*} [varArgs] - Format string arguments
+ */
+function println(format, varArgs) {
+  var vargs = Array.from(arguments);
+  vargs[0] += '\n';
+  process.stdout.write(sprintf.apply(null, vargs));
+}
+
+/**
+ * Returns a `tapVersion`-appropriate TAP producer instance, if possible.
+ *
+ * @private
+ * @param {string} tapVersion - Version of TAP specification to produce.
+ * @returns {TAPProducer} specification-appropriate instance
+ * @throws {Error} if specification version has no associated producer.
+ */
+function createProducer(tapVersion) {
+  var producers = {
+    '12': new TAP12Producer(),
+    '13': new TAP13Producer()
+  };
+  var producer = producers[tapVersion];
+
+  if (!producer) {
+    throw new Error(
+      'invalid or unsupported TAP version: ' + JSON.stringify(tapVersion)
+    );
+  }
+
+  return producer;
+}
+
+/**
+ * @summary
+ * Constructs a new TAPProducer.
+ *
+ * @description
+ * Only to be used as an abstract base class.
+ *
+ * @private
+ * @constructor
+ */
+function TAPProducer() {}
+
+/**
+ * Writes the TAP version to reporter output stream.
+ *
+ * @abstract
+ */
+TAPProducer.prototype.writeVersion = function() {};
+
+/**
+ * Writes the plan to reporter output stream.
+ *
+ * @abstract
+ * @param {number} ntests - Number of tests that are planned to run.
+ */
+TAPProducer.prototype.writePlan = function(ntests) {
+  println('%d..%d', 1, ntests);
+};
+
+/**
+ * Writes that test passed to reporter output stream.
+ *
+ * @abstract
+ * @param {number} n - Index of test that passed.
+ * @param {Test} test - Instance containing test information.
+ */
+TAPProducer.prototype.writePass = function(n, test) {
+  println('ok %d %s', n, title(test));
+};
+
+/**
+ * Writes that test was skipped to reporter output stream.
+ *
+ * @abstract
+ * @param {number} n - Index of test that was skipped.
+ * @param {Test} test - Instance containing test information.
+ */
+TAPProducer.prototype.writePending = function(n, test) {
+  println('ok %d %s # SKIP -', n, title(test));
+};
+
+/**
+ * Writes that test failed to reporter output stream.
+ *
+ * @abstract
+ * @param {number} n - Index of test that failed.
+ * @param {Test} test - Instance containing test information.
+ * @param {Error} err - Reason the test failed.
+ */
+TAPProducer.prototype.writeFail = function(n, test, err) {
+  println('not ok %d %s', n, title(test));
+};
+
+/**
+ * Writes the summary epilogue to reporter output stream.
+ *
+ * @abstract
+ * @param {Object} stats - Object containing run statistics.
+ */
+TAPProducer.prototype.writeEpilogue = function(stats) {
+  // :TBD: Why is this not counting pending tests?
+  println('# tests ' + (stats.passes + stats.failures));
+  println('# pass ' + stats.passes);
+  // :TBD: Why are we not showing pending results?
+  println('# fail ' + stats.failures);
+};
+
+/**
+ * @summary
+ * Constructs a new TAP12Producer.
+ *
+ * @description
+ * Produces output conforming to the TAP12 specification.
+ *
+ * @private
+ * @constructor
+ * @extends TAPProducer
+ * @see {@link https://testanything.org/tap-specification.html|Specification}
+ */
+function TAP12Producer() {
+  /**
+   * Writes that test failed to reporter output stream, with error formatting.
+   * @override
+   */
+  this.writeFail = function(n, test, err) {
+    TAPProducer.prototype.writeFail.call(this, n, test, err);
+    if (err.message) {
+      println(err.message.replace(/^/gm, '  '));
+    }
+    if (err.stack) {
+      println(err.stack.replace(/^/gm, '  '));
+    }
+  };
+}
+
+/**
+ * Inherit from `TAPProducer.prototype`.
+ */
+inherits(TAP12Producer, TAPProducer);
+
+/**
+ * @summary
+ * Constructs a new TAP13Producer.
+ *
+ * @description
+ * Produces output conforming to the TAP13 specification.
+ *
+ * @private
+ * @constructor
+ * @extends TAPProducer
+ * @see {@link https://testanything.org/tap-version-13-specification.html|Specification}
+ */
+function TAP13Producer() {
+  /**
+   * Writes the TAP version to reporter output stream.
+   * @override
+   */
+  this.writeVersion = function() {
+    println('TAP version 13');
+  };
+
+  /**
+   * Writes that test failed to reporter output stream, with error formatting.
+   * @override
+   */
+  this.writeFail = function(n, test, err) {
+    TAPProducer.prototype.writeFail.call(this, n, test, err);
+    var emitYamlBlock = err.message != null || err.stack != null;
+    if (emitYamlBlock) {
+      println(indent(1) + '---');
+      if (err.message) {
+        println(indent(2) + 'message: |-');
+        println(err.message.replace(/^/gm, indent(3)));
+      }
+      if (err.stack) {
+        println(indent(2) + 'stack: |-');
+        println(err.stack.replace(/^/gm, indent(3)));
+      }
+      println(indent(1) + '...');
+    }
+  };
+
+  function indent(level) {
+    return Array(level + 1).join('  ');
+  }
+}
+
+/**
+ * Inherit from `TAPProducer.prototype`.
+ */
+inherits(TAP13Producer, TAPProducer);
diff --git a/test/integration/fixtures/reporters.fixture.js b/test/integration/fixtures/reporters.fixture.js
new file mode 100644
index 0000000000..ab16bc0eed
--- /dev/null
+++ b/test/integration/fixtures/reporters.fixture.js
@@ -0,0 +1,63 @@
+'use strict';
+
+/**
+ * This file generates a wide range of output to test reporter functionality.
+ */
+
+describe('Animals', function() {
+
+  it('should consume organic material', function(done) { done(); });
+  it('should breathe oxygen', function(done) {
+    // we're a jellyfish
+    var actualBreathe = 'nothing';
+    var expectedBreathe = 'oxygen';
+    expect(actualBreathe, 'to equal', expectedBreathe);
+    done();
+  });
+  it('should be able to move', function(done) { done(); });
+  it('should reproduce sexually', function(done) { done(); });
+  it('should grow from a hollow sphere of cells', function(done) { done(); });
+
+  describe('Vertebrates', function() {
+    describe('Mammals', function() {
+      it('should give birth to live young', function(done) {
+        var expectedMammal = {
+          consumesMaterial: 'organic',
+          breathe: 'oxygen',
+          reproduction: {
+            type: 'sexually',
+            spawnType: 'live',
+          }
+        };
+        var platypus = JSON.parse(JSON.stringify(expectedMammal));
+        platypus['reproduction']['spawnType'] = 'hard-shelled egg';
+
+        expect(platypus, 'to equal', expectedMammal);
+        done();
+      });
+
+      describe('Blue Whale', function() {
+        it('should be the largest of all mammals', function(done) { done(); });
+        it('should have a body in some shade of blue', function(done) {
+          var bodyColor = 'blueish_grey';
+          var shadesOfBlue = ['cyan', 'light_blue', 'blue', 'indigo'];
+          expect(bodyColor, 'to be one of', shadesOfBlue);
+
+          done();
+        });
+      });
+    });
+    describe('Birds', function() {
+      it('should have feathers', function(done) { done(); });
+      it('should lay hard-shelled eggs', function(done) { done(); });
+    });
+  });
+
+  describe('Tardigrades', function() {
+    it('should answer to "water bear"', function(done) { done(); });
+    it('should be able to survive global mass extinction events', function(done) {
+      throw new Error("How do we even test for this without causing one?")
+      done();
+    });
+  });
+});
diff --git a/test/integration/reporters.spec.js b/test/integration/reporters.spec.js
index 4e95622dee..c78da8ab6b 100644
--- a/test/integration/reporters.spec.js
+++ b/test/integration/reporters.spec.js
@@ -99,4 +99,137 @@ describe('reporters', function() {
       });
     });
   });
+
+  describe('tap', function() {
+    var not = function(predicate) {
+      return function() {
+        return !predicate.apply(this, arguments);
+      };
+    };
+    var versionPredicate = function(line) {
+      return line.match(/^TAP version \d+$/) != null;
+    };
+    var planPredicate = function(line) {
+      return line.match(/^1\.\.\d+$/) != null;
+    };
+    var testLinePredicate = function(line) {
+      return line.match(/^not ok/) != null || line.match(/^ok/) != null;
+    };
+    var diagnosticPredicate = function(line) {
+      return line.match(/^#/) != null;
+    };
+    var bailOutPredicate = function(line) {
+      return line.match(/^Bail out!/) != null;
+    };
+    var anythingElsePredicate = function(line) {
+      return (
+        versionPredicate(line) === false &&
+        planPredicate(line) === false &&
+        testLinePredicate(line) === false &&
+        diagnosticPredicate(line) === false &&
+        bailOutPredicate(line) === false
+      );
+    };
+
+    describe('produces valid TAP v13 output', function() {
+      var runFixtureAndValidateOutput = function(fixture, expected) {
+        it('for ' + fixture, function(done) {
+          var args = ['--reporter=tap', '--reporter-options', 'tapVersion=13'];
+
+          run(fixture, args, function(err, res) {
+            if (err) {
+              done(err);
+              return;
+            }
+
+            var expectedVersion = 13;
+            var expectedPlan = '1..' + expected.numTests;
+
+            var outputLines = res.output.split('\n');
+
+            // first line must be version line
+            expect(
+              outputLines[0],
+              'to equal',
+              'TAP version ' + expectedVersion
+            );
+
+            // plan must appear once
+            expect(outputLines, 'to contain', expectedPlan);
+            expect(
+              outputLines.filter(function(l) {
+                return l === expectedPlan;
+              }),
+              'to have length',
+              1
+            );
+            // plan cannot appear in middle of the output
+            var firstTestLine = outputLines.findIndex(testLinePredicate);
+            // there must be at least one test line
+            expect(firstTestLine, 'to be greater than', -1);
+            var lastTestLine =
+              outputLines.length -
+              1 -
+              outputLines
+                .slice()
+                .reverse()
+                .findIndex(testLinePredicate);
+            var planLine = outputLines.findIndex(function(line) {
+              return line === expectedPlan;
+            });
+            expect(
+              planLine < firstTestLine || planLine > lastTestLine,
+              'to equal',
+              true
+            );
+
+            done();
+          });
+        });
+      };
+
+      runFixtureAndValidateOutput('passing.fixture.js', {
+        numTests: 2
+      });
+      runFixtureAndValidateOutput('reporters.fixture.js', {
+        numTests: 12
+      });
+    });
+
+    it('places exceptions correctly in YAML blocks', function(done) {
+      var args = ['--reporter=tap', '--reporter-options', 'tapVersion=13'];
+
+      run('reporters.fixture.js', args, function(err, res) {
+        if (err) {
+          done(err);
+          return;
+        }
+
+        var outputLines = res.output.split('\n');
+
+        for (var i = 0; i + 1 < outputLines.length; i++) {
+          if (
+            testLinePredicate(outputLines[i]) &&
+            testLinePredicate(outputLines[i + 1]) === false
+          ) {
+            var blockLinesStart = i + 1;
+            var blockLinesEnd =
+              i +
+              1 +
+              outputLines.slice(i + 1).findIndex(not(anythingElsePredicate));
+            var blockLines =
+              blockLinesEnd > blockLinesStart
+                ? outputLines.slice(blockLinesStart, blockLinesEnd)
+                : outputLines.slice(blockLinesStart);
+            i += blockLines.length;
+
+            expect(blockLines[0], 'to match', /^\s+---/);
+            expect(blockLines[blockLines.length - 1], 'to match', /^\s+\.\.\./);
+          }
+        }
+
+        done();
+      });
+    });
+  });
 });
diff --git a/test/reporters/tap.spec.js b/test/reporters/tap.spec.js
index 208a88b904..c0a5419d37 100644
--- a/test/reporters/tap.spec.js
+++ b/test/reporters/tap.spec.js
@@ -8,7 +8,6 @@ var makeRunReporter = require('./helpers.js').createRunReporterFunction;
 
 describe('TAP reporter', function() {
   var runner;
-  var options = {};
   var runReporter = makeRunReporter(TAP);
   var expectedTitle = 'some title';
   var countAfterTestEnd = 2;
@@ -25,200 +24,455 @@ describe('TAP reporter', function() {
 
   afterEach(function() {
     runner = undefined;
+    test = undefined;
   });
 
-  describe('on start', function() {
-    it('should hand runners suite into grepTotal and log the total', function() {
+  describe('TAP12 spec', function() {
+    var options = {};
+
+    describe('on start', function() {
       var expectedSuite = 'some suite';
       var expectedTotal = 10;
       var expectedString;
-      runner = createMockRunner('start', 'start');
-      runner.suite = expectedSuite;
-      runner.grepTotal = function(string) {
-        expectedString = string;
-        return expectedTotal;
-      };
-      var stdout = runReporter({}, runner, options);
-
-      var expectedArray = ['1..' + expectedTotal + '\n'];
-
-      expect(stdout, 'to equal', expectedArray);
-      expect(expectedString, 'to be', expectedSuite);
+      var stdout;
+
+      before(function() {
+        runner = createMockRunner('start', 'start');
+        runner.suite = expectedSuite;
+        runner.grepTotal = function(string) {
+          expectedString = string;
+          return expectedTotal;
+        };
+        stdout = runReporter({}, runner, options);
+      });
+
+      it('should not write the TAP specification version', function() {
+        expect(stdout, 'not to contain', 'TAP version');
+      });
+      it('should write the number of tests that it plans to run', function() {
+        var expectedArray = ['1..' + expectedTotal + '\n'];
+        expect(stdout, 'to equal', expectedArray);
+        expect(expectedString, 'to be', expectedSuite);
+      });
     });
-  });
 
-  describe('on pending', function() {
-    it('should write expected message including count and title', function() {
-      runner = createMockRunner(
-        'start test',
-        'test end',
-        'pending',
-        null,
-        test
-      );
-      runner.suite = '';
-      runner.grepTotal = function() {};
-      var stdout = runReporter({}, runner, options);
-
-      var expectedMessage =
-        'ok ' + countAfterTestEnd + ' ' + expectedTitle + ' # SKIP -\n';
-      expect(stdout[0], 'to equal', expectedMessage);
+    describe('on pending', function() {
+      it('should write expected message including count and title', function() {
+        runner = createMockRunner(
+          'start test',
+          'test end',
+          'pending',
+          null,
+          test
+        );
+        runner.suite = '';
+        runner.grepTotal = function() {};
+
+        var stdout = runReporter({}, runner, options);
+
+        var expectedMessage =
+          'ok ' + countAfterTestEnd + ' ' + expectedTitle + ' # SKIP -\n';
+        expect(stdout[0], 'to equal', expectedMessage);
+      });
     });
-  });
 
-  describe('on pass', function() {
-    it('should write expected message including count and title', function() {
-      runner = createMockRunner('start test', 'test end', 'pass', null, test);
+    describe('on pass', function() {
+      it('should write expected message including count and title', function() {
+        runner = createMockRunner('start test', 'test end', 'pass', null, test);
+        runner.suite = '';
+        runner.grepTotal = function() {};
+
+        var stdout = runReporter({}, runner, options);
+
+        var expectedMessage =
+          'ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n';
+        expect(stdout[0], 'to equal', expectedMessage);
+      });
+    });
+
+    describe('on fail', function() {
+      describe('if there is an error message', function() {
+        it('should write expected message and error message', function() {
+          var expectedErrorMessage = 'some error';
+          var error = {
+            message: expectedErrorMessage
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ' + expectedErrorMessage + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is an error stack', function() {
+        it('should write expected message and stack', function() {
+          var expectedStack = 'some stack';
+          var error = {
+            stack: expectedStack
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ' + expectedStack + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is an error stack and error message', function() {
+        it('should write expected message and stack', function() {
+          var expectedStack = 'some stack';
+          var expectedErrorMessage = 'some error';
+          var error = {
+            stack: expectedStack,
+            message: expectedErrorMessage
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
 
-      runner.suite = '';
-      runner.grepTotal = function() {};
-      var stdout = runReporter({}, runner, options);
+          var stdout = runReporter({}, runner, options);
 
-      var expectedMessage =
-        'ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n';
-      expect(stdout[0], 'to equal', expectedMessage);
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ' + expectedErrorMessage + '\n',
+            '  ' + expectedStack + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is no error stack or error message', function() {
+        it('should write expected message only', function() {
+          var error = {};
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = runner.once = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
     });
-  });
 
-  describe('on fail', function() {
-    describe('if there is an error message', function() {
-      it('should write expected message and error message', function() {
-        var expectedTitle = 'some title';
-        var countAfterTestEnd = 2;
-        var expectedErrorMessage = 'some error';
-        var test = {
-          fullTitle: function() {
-            return expectedTitle;
-          },
-          slow: function() {}
-        };
-        var error = {
-          message: expectedErrorMessage
-        };
-        runner = createMockRunner('test end fail', 'test end', 'fail');
-        runner.on = function(event, callback) {
-          if (event === 'test end') {
-            callback();
-          }
-          if (event === 'fail') {
-            callback(test, error);
-          }
-        };
+    describe('on end', function() {
+      it('should write total tests, passes and failures', function() {
+        var numberOfPasses = 1;
+        var numberOfFails = 1;
+        runner = createMockRunner('fail end pass', 'fail', 'end', 'pass', test);
         runner.suite = '';
         runner.grepTotal = function() {};
+
         var stdout = runReporter({}, runner, options);
 
+        var totalTests = numberOfPasses + numberOfFails;
         var expectedArray = [
-          'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
-          '  ' + expectedErrorMessage + '\n'
+          'ok ' + numberOfPasses + ' ' + expectedTitle + '\n',
+          'not ok ' + numberOfFails + ' ' + expectedTitle + '\n',
+          '# tests ' + totalTests + '\n',
+          '# pass ' + numberOfPasses + '\n',
+          '# fail ' + numberOfFails + '\n'
         ];
         expect(stdout, 'to equal', expectedArray);
       });
     });
-    describe('if there is an error stack', function() {
-      it('should write expected message and stack', function() {
-        var expectedStack = 'some stack';
-        var error = {
-          stack: expectedStack
+  });
+
+  describe('TAP13 spec', function() {
+    var options = {
+      reporterOptions: {
+        tapVersion: '13'
+      }
+    };
+
+    describe('on start', function() {
+      var expectedSuite = 'some suite';
+      var expectedTotal = 10;
+      var expectedString;
+      var stdout;
+
+      before(function() {
+        runner = createMockRunner('start', 'start');
+        runner.suite = expectedSuite;
+        runner.grepTotal = function(string) {
+          expectedString = string;
+          return expectedTotal;
         };
+
+        stdout = runReporter({}, runner, options);
+      });
+
+      it('should write the TAP specification version', function() {
+        var tapVersion = options.reporterOptions.tapVersion;
+        var expectedFirstLine = 'TAP version ' + tapVersion + '\n';
+        expect(stdout[0], 'to equal', expectedFirstLine);
+      });
+      it('should write the number of tests that it plans to run', function() {
+        var expectedSecondLine = '1..' + expectedTotal + '\n';
+        expect(stdout[1], 'to equal', expectedSecondLine);
+        expect(expectedString, 'to be', expectedSuite);
+      });
+    });
+
+    describe('on pending', function() {
+      it('should write expected message including count and title', function() {
         runner = createMockRunner(
-          'test end fail',
+          'start test',
           'test end',
-          'fail',
+          'pending',
           null,
-          test,
-          error
+          test
         );
         runner.suite = '';
         runner.grepTotal = function() {};
+
         var stdout = runReporter({}, runner, options);
 
-        var expectedArray = [
-          'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
-          '  ' + expectedStack + '\n'
-        ];
-        expect(stdout, 'to equal', expectedArray);
+        var expectedMessage =
+          'ok ' + countAfterTestEnd + ' ' + expectedTitle + ' # SKIP -\n';
+        expect(stdout[0], 'to equal', expectedMessage);
       });
     });
-    describe('if there is an error stack and error message', function() {
-      it('should write expected message and stack', function() {
-        var expectedTitle = 'some title';
-        var countAfterTestEnd = 2;
-        var expectedStack = 'some stack';
-        var expectedErrorMessage = 'some error';
-        var test = {
-          fullTitle: function() {
-            return expectedTitle;
-          },
-          slow: function() {}
-        };
-        var error = {
-          stack: expectedStack,
-          message: expectedErrorMessage
-        };
-        runner = createMockRunner('test end fail', 'test end', 'fail');
-        runner.on = function(event, callback) {
-          if (event === 'test end') {
-            callback();
-          }
-          if (event === 'fail') {
-            callback(test, error);
-          }
-        };
+
+    describe('on pass', function() {
+      it('should write expected message including count and title', function() {
+        runner = createMockRunner('start test', 'test end', 'pass', null, test);
         runner.suite = '';
         runner.grepTotal = function() {};
+
         var stdout = runReporter({}, runner, options);
 
-        var expectedArray = [
-          'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
-          '  ' + expectedErrorMessage + '\n',
-          '  ' + expectedStack + '\n'
-        ];
-        expect(stdout, 'to equal', expectedArray);
+        var expectedMessage =
+          'ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n';
+        expect(stdout[0], 'to equal', expectedMessage);
       });
     });
-    describe('if there is no error stack or error message', function() {
-      it('should write expected message only', function() {
-        runner = createMockRunner('test end fail', 'test end', 'fail');
-        var error = {};
-        runner.on = runner.once = function(event, callback) {
-          if (event === 'test end') {
-            callback();
-          }
-          if (event === 'fail') {
-            callback(test, error);
-          }
-        };
+
+    describe('on fail', function() {
+      describe('if there is an error message', function() {
+        it('should write expected message and error message', function() {
+          var expectedErrorMessage = 'some error';
+          var error = {
+            message: expectedErrorMessage
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ---\n',
+            '    message: |-\n',
+            '      ' + expectedErrorMessage + '\n',
+            '  ...\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is an error stack', function() {
+        it('should write expected message and stack', function() {
+          var expectedStack = 'some stack';
+          var error = {
+            stack: expectedStack
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ---\n',
+            '    stack: |-\n',
+            '      ' + expectedStack + '\n',
+            '  ...\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is an error stack and error message', function() {
+        it('should write expected message and stack', function() {
+          var expectedStack = 'some stack';
+          var expectedErrorMessage = 'some error';
+          var error = {
+            stack: expectedStack,
+            message: expectedErrorMessage
+          };
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n',
+            '  ---\n',
+            '    message: |-\n',
+            '      ' + expectedErrorMessage + '\n',
+            '    stack: |-\n',
+            '      ' + expectedStack + '\n',
+            '  ...\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+
+      describe('if there is no error stack or error message', function() {
+        it('should write expected message only', function() {
+          var error = {};
+          runner = createMockRunner(
+            'test end fail',
+            'test end',
+            'fail',
+            null,
+            test,
+            error
+          );
+          runner.on = runner.once = function(event, callback) {
+            if (event === 'test end') {
+              callback();
+            } else if (event === 'fail') {
+              callback(test, error);
+            }
+          };
+          runner.suite = '';
+          runner.grepTotal = function() {};
+
+          var stdout = runReporter({}, runner, options);
+
+          var expectedArray = [
+            'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n'
+          ];
+          expect(stdout, 'to equal', expectedArray);
+        });
+      });
+    });
+
+    describe('on end', function() {
+      it('should write total tests, passes and failures', function() {
+        var numberOfPasses = 1;
+        var numberOfFails = 1;
+        runner = createMockRunner('fail end pass', 'fail', 'end', 'pass', test);
         runner.suite = '';
         runner.grepTotal = function() {};
+
         var stdout = runReporter({}, runner, options);
 
+        var totalTests = numberOfPasses + numberOfFails;
         var expectedArray = [
-          'not ok ' + countAfterTestEnd + ' ' + expectedTitle + '\n'
+          'ok ' + numberOfPasses + ' ' + expectedTitle + '\n',
+          'not ok ' + numberOfFails + ' ' + expectedTitle + '\n',
+          '# tests ' + totalTests + '\n',
+          '# pass ' + numberOfPasses + '\n',
+          '# fail ' + numberOfFails + '\n'
         ];
         expect(stdout, 'to equal', expectedArray);
       });
     });
   });
-
-  describe('on end', function() {
-    it('should write total tests, passes and failures', function() {
-      var numberOfPasses = 1;
-      var numberOfFails = 1;
-      runner = createMockRunner('fail end pass', 'fail', 'end', 'pass', test);
-      runner.suite = '';
-      runner.grepTotal = function() {};
-      var stdout = runReporter({}, runner, options);
-
-      var totalTests = numberOfPasses + numberOfFails;
-      var expectedArray = [
-        'ok ' + numberOfPasses + ' ' + expectedTitle + '\n',
-        'not ok ' + numberOfFails + ' ' + expectedTitle + '\n',
-        '# tests ' + totalTests + '\n',
-        '# pass ' + numberOfPasses + '\n',
-        '# fail ' + numberOfFails + '\n'
-      ];
-      expect(stdout, 'to equal', expectedArray);
-    });
-  });
 });

From 593e9b8f1b23578e20fdaa413ddeee1bab294728 Mon Sep 17 00:00:00 2001
From: "P. Roebuck" 
Date: Sat, 10 Nov 2018 06:16:34 -0600
Subject: [PATCH 18/18] Add ability to query Mocha version programmatically
 (#3535)

* Added new public `version` property to `Mocha`, available in both Node and browser.
* Used `browserify` transform to prevent exposing any other package details (for security).
---
 lib/mocha.js      | 14 ++++++++++++++
 package-lock.json | 15 +++++++++++++++
 package.json      | 22 ++++++++++++++--------
 3 files changed, 43 insertions(+), 8 deletions(-)

diff --git a/lib/mocha.js b/lib/mocha.js
index 1700fe9ca7..e414040622 100644
--- a/lib/mocha.js
+++ b/lib/mocha.js
@@ -677,6 +677,20 @@ Mocha.prototype.forbidPending = function() {
   return this;
 };
 
+/**
+ * Mocha version as specified by "package.json".
+ *
+ * @name Mocha#version
+ * @type string
+ * @readonly
+ */
+Object.defineProperty(Mocha.prototype, 'version', {
+  value: require('../package').version,
+  configurable: false,
+  enumerable: true,
+  writable: false
+});
+
 /**
  * Callback to be invoked when test execution is complete.
  *
diff --git a/package-lock.json b/package-lock.json
index 97fdde53a1..72c4a6f2ec 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1316,6 +1316,12 @@
         "inherits": "^2.0.1"
       }
     },
+    "browserify-package-json": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/browserify-package-json/-/browserify-package-json-1.0.1.tgz",
+      "integrity": "sha1-mN3oqlxWH9bT/km7qhArdLOW/eo=",
+      "dev": true
+    },
     "browserify-rsa": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
@@ -13686,6 +13692,15 @@
         "thunkify": "^2.1.2"
       }
     },
+    "package-json-versionify": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/package-json-versionify/-/package-json-versionify-1.0.4.tgz",
+      "integrity": "sha1-WGBYepRIc6a35tJujlH/siMVvxc=",
+      "dev": true,
+      "requires": {
+        "browserify-package-json": "^1.0.0"
+      }
+    },
     "pako": {
       "version": "1.0.6",
       "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.6.tgz",
diff --git a/package.json b/package.json
index f85057cb46..7b1938ffa1 100644
--- a/package.json
+++ b/package.json
@@ -446,10 +446,19 @@
     "type": "git",
     "url": "https://github.com/mochajs/mocha.git"
   },
+  "bugs": {
+    "url": "https://github.com/mochajs/mocha/issues/"
+  },
+  "homepage": "https://mochajs.org/",
+  "logo": "https://cldup.com/S9uQ-cOLYz.svg",
   "bin": {
     "mocha": "./bin/mocha",
     "_mocha": "./bin/_mocha"
   },
+  "directories": {
+    "lib": "./lib",
+    "test": "./test"
+  },
   "engines": {
     "node": ">= 6.0.0"
   },
@@ -502,6 +511,7 @@
     "markdownlint-cli": "^0.9.0",
     "nps": "^5.7.1",
     "nyc": "^11.7.3",
+    "package-json-versionify": "^1.0.4",
     "prettier-eslint-cli": "^4.7.1",
     "rimraf": "^2.5.2",
     "svgo": "^0.7.2",
@@ -518,9 +528,10 @@
     "mocha.js",
     "browser-entry.js"
   ],
-  "directories": {
-    "lib": "lib",
-    "test": "test"
+  "browserify": {
+    "transform": [
+      "package-json-versionify"
+    ]
   },
   "browser": {
     "growl": "./lib/browser/growl.js",
@@ -531,13 +542,8 @@
     "path": false,
     "supports-color": false
   },
-  "homepage": "https://mochajs.org/",
-  "logo": "https://cldup.com/S9uQ-cOLYz.svg",
   "prettier": {
     "singleQuote": true,
     "bracketSpacing": false
-  },
-  "bugs": {
-    "url": "https://github.com/mochajs/mocha/issues/"
   }
 }