diff --git a/package-lock.json b/package-lock.json index a7da24d799447..859a68a4ff4a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "integrity": "sha1-z6I7xYQPkQTOMqZedNt+epdLvuE=", "dev": true, "requires": { - "acorn": "5.5.3", + "acorn": "5.7.1", "css": "2.2.1", "normalize-path": "2.1.1", "source-map": "0.5.7", @@ -18,9 +18,9 @@ }, "dependencies": { "acorn": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.5.3.tgz", - "integrity": "sha512-jd5MkIUlbbmb07nXH0DT3y7rDVtkzDi4XZOUVWAer8ajmF/DTSSbl5oNFyDOl/OXA33Bl79+ypHhl2pN20VeOQ==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", "dev": true } } @@ -36,9 +36,9 @@ } }, "@octokit/rest": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-15.8.1.tgz", - "integrity": "sha512-IpC/ctwwauiiSrnNTHOG4CyAPz5YwEX8wSSGuTBb0M1mJcAYJCaYZr11dSZTB4K2p2XFY4AY5+SZcW5aub3hSQ==", + "version": "15.8.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-15.8.2.tgz", + "integrity": "sha512-hMUDI6NveJE49rGYfNfXT2CiHODhQMfbqFAa2h8TjR3GrfI1wnfSlsYeGZe4D/Qu+Svqlg9eUisoeIvYWz1yZw==", "dev": true, "requires": { "before-after-hook": "1.1.0", @@ -79,9 +79,9 @@ } }, "@types/chai": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.3.tgz", - "integrity": "sha512-f5dXGzOJycyzSMdaXVhiBhauL4dYydXwVpavfQ1mVCaGjR56a9QfklXObUxlIY9bGTmCPHEEZ04I16BZ/8w5ww==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.1.4.tgz", + "integrity": "sha512-h6+VEw2Vr3ORiFCyyJmcho2zALnUq9cvdB/IO8Xs9itrJVCenC7o26A6+m7D0ihTTr65eS259H5/Ghl/VjYs6g==", "dev": true }, "@types/convert-source-map": { @@ -214,9 +214,9 @@ } }, "@types/mocha": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.1.tgz", - "integrity": "sha512-dOrgprHnkDaj1pmrwdcMAf0QRNQzqTB5rxJph+iIQshSmIvtgRqJ0nim8u1vvXU8iOXZrH96+M46JDFTPLingA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-5.2.2.tgz", + "integrity": "sha512-tfg9rh2qQhBW6SBqpvfqTgU6lHWGhQURoTrn7NeDF+CEkO9JGYbkzU23EXu6p3bnvDxLeeSX8ohAA23urvWeNw==", "dev": true }, "@types/node": { @@ -370,15 +370,6 @@ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=", "dev": true }, - "ansi-colors": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", - "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", - "dev": true, - "requires": { - "ansi-wrap": "0.1.0" - } - }, "ansi-cyan": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/ansi-cyan/-/ansi-cyan-0.1.1.tgz", @@ -2132,6 +2123,25 @@ "map-cache": "0.2.2" } }, + "fs-extra": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz", + "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==", + "dev": true, + "requires": { + "graceful-fs": "4.1.11", + "jsonfile": "4.0.0", + "universalify": "0.1.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true + } + } + }, "fs-mkdirp-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-mkdirp-stream/-/fs-mkdirp-stream-1.0.0.tgz", @@ -2590,7 +2600,7 @@ "requires": { "@gulp-sourcemaps/identity-map": "1.0.1", "@gulp-sourcemaps/map-sources": "1.0.0", - "acorn": "5.6.2", + "acorn": "5.7.1", "convert-source-map": "1.5.1", "css": "2.2.1", "debug-fabulous": "1.1.0", @@ -2602,9 +2612,9 @@ }, "dependencies": { "acorn": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.6.2.tgz", - "integrity": "sha512-zUzo1E5dI2Ey8+82egfnttyMlMZ2y0D8xOCO3PNPPlYXpl8NZvF6Qk9L9BEtJs+43FqEmfBViDqc5d1ckRDguw==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.1.tgz", + "integrity": "sha512-d+nbxBUGKg7Arpsvbnlq61mc12ek3EY8EQldM3GPAhWJ1UVxC6TDGbIvUMNU6obBX3i1+ptCIzV4vq0gFPEGVQ==", "dev": true }, "graceful-fs": { @@ -2622,19 +2632,25 @@ } }, "gulp-typescript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-4.0.2.tgz", - "integrity": "sha512-Hhbn5Aa2l3T+tnn0KqsG6RRJmcYEsr3byTL2nBpNBeAK8pqug9Od4AwddU4JEI+hRw7mzZyjRbB8DDWR6paGVA==", + "version": "5.0.0-alpha.1", + "resolved": "https://registry.npmjs.org/gulp-typescript/-/gulp-typescript-5.0.0-alpha.1.tgz", + "integrity": "sha512-B2Zfup9R5p/hvZowVWdthCt/vrDxiwIQ1Ehi/CsHb6qRB66PVhDeF6Yw/d6HF/3wtka/XxI9zsTjiRb+3bsrJQ==", "dev": true, "requires": { - "ansi-colors": "1.1.0", - "plugin-error": "0.1.2", - "source-map": "0.6.1", + "ansi-colors": "2.0.1", + "plugin-error": "1.0.1", + "source-map": "0.7.3", "through2": "2.0.3", "vinyl": "2.1.0", "vinyl-fs": "3.0.3" }, "dependencies": { + "ansi-colors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-2.0.1.tgz", + "integrity": "sha512-qUIXfMVe0LoHCFPD6dGtjDDuVoP7B2DWBXIfd5aN/hGNIZDndQmqCwNjCChzxi8TPPGmBV4TB3XPc0VfgR7iIQ==", + "dev": true + }, "glob-stream": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/glob-stream/-/glob-stream-6.1.0.tgz", @@ -2677,10 +2693,33 @@ "readable-stream": "2.3.6" } }, + "plugin-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-1.0.1.tgz", + "integrity": "sha512-L1zP0dk7vGweZME2i+EeakvUNqSrdiI3F91TwEoYiGrAfUXmVv6fJIq4g82PAXxNsWOp0J7ZqQy/3Szz0ajTxA==", + "dev": true, + "requires": { + "ansi-colors": "1.1.0", + "arr-diff": "4.0.0", + "arr-union": "3.1.0", + "extend-shallow": "3.0.2" + }, + "dependencies": { + "ansi-colors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-1.1.0.tgz", + "integrity": "sha512-SFKX67auSNoVR38N3L+nvsPjOE0bybKTYbkf5tRvushrAPQ9V75huw0ZxBkKVeRU9kqH3d6HA4xTckbwZ4ixmA==", + "dev": true, + "requires": { + "ansi-wrap": "0.1.0" + } + } + } + }, "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", "dev": true }, "unique-stream": { @@ -3438,6 +3477,24 @@ "jsonify": "0.0.0" } }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "4.1.11" + }, + "dependencies": { + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + } + } + }, "jsonify": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", @@ -5721,6 +5778,12 @@ "integrity": "sha1-1ZpKdUJ0R9mqbJHnAmP40mpLEEs=", "dev": true }, + "universalify": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.1.tgz", + "integrity": "sha1-+nG63UQ3r0wUiEHjs7Fl+enlkLc=", + "dev": true + }, "unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", diff --git a/package.json b/package.json index 1845742dc1293..5f1b07e0e9c96 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "@types/minimatch": "latest", "@types/minimist": "latest", "@types/mkdirp": "latest", - "@types/mocha": "^5.2.2", + "@types/mocha": "latest", "@types/node": "8.5.5", "@types/q": "latest", "@types/run-sequence": "latest", diff --git a/scripts/types/mocha/LICENSE b/scripts/types/mocha/LICENSE new file mode 100644 index 0000000000000..21071075c2459 --- /dev/null +++ b/scripts/types/mocha/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. All rights reserved. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/scripts/types/mocha/index.d.ts b/scripts/types/mocha/index.d.ts new file mode 100644 index 0000000000000..1ada77c0a49ea --- /dev/null +++ b/scripts/types/mocha/index.d.ts @@ -0,0 +1,2856 @@ +// Type definitions for mocha 5.2 +// Project: http://mochajs.org/ +// Definitions by: Kazi Manzur Rashid +// otiai10 +// jt000 +// Vadim Macagon +// Andrew Bradley +// Dmitrii Sorin +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.1 + +export = Mocha; +export as namespace Mocha; + +/** + * Mocha API + * + * @see https://mochajs.org/api/mocha + */ +declare class Mocha { + private _growl; + private _reporter; + private _ui; + + constructor(options?: Mocha.MochaOptions); + + suite: Mocha.Suite; + files: string[]; + options: Mocha.MochaInstanceOptions; + + /** + * Enable or disable bailing on the first failure. + * + * @see https://mochajs.org/api/mocha#bail + */ + bail(bail?: boolean): this; + + /** + * Add test `file`. + * + * @see https://mochajs.org/api/mocha#addFile + */ + addFile(file: string): this; + + /** + * Set reporter to one of the built-in reporters. + * + * @see https://mochajs.org/api/mocha#reporter + */ + reporter(reporter: Mocha.Reporter, reporterOptions?: any): this; + + /** + * Set reporter to the provided constructor, one of the built-in reporters, or loads a reporter + * from a module path. Defaults to `"spec"`. + * + * @see https://mochajs.org/api/mocha#reporter + */ + reporter(reporter?: string | Mocha.ReporterConstructor, reporterOptions?: any): this; + + /** + * Set test UI to one of the built-in test interfaces. + * + * @see https://mochajs.org/api/mocha#ui + */ + ui(name: Mocha.Interface): this; + + /** + * Set test UI to one of the built-in test interfaces or loads a test interface from a module + * path. Defaults to `"bdd"`. + * + * @see https://mochajs.org/api/mocha#ui + */ + ui(name?: string): this; + + /** + * Escape string and add it to grep as a RegExp. + * + * @see https://mochajs.org/api/mocha#fgrep + */ + fgrep(str: string): this; + + /** + * Add regexp to grep, if `re` is a string it is escaped. + * + * @see https://mochajs.org/api/mocha#grep + */ + grep(re: string | RegExp): this; + + /** + * Invert `.grep()` matches. + * + * @see https://mochajs.org/api/mocha#invert + */ + invert(): this; + + /** + * Ignore global leaks. + * + * @see https://mochajs.org/api/mocha#ignoreLeaks + */ + ignoreLeaks(ignore: boolean): this; + + /** + * Enable global leak checking. + * + * @see https://mochajs.org/api/mocha#checkLeaks + */ + checkLeaks(): this; + + /** + * Display long stack-trace on failing + * + * @see https://mochajs.org/api/mocha#fullTrace + */ + fullTrace(): this; + + /** + * Enable growl support. + * + * @see https://mochajs.org/api/mocha#growl + */ + growl(): this; + + /** + * Ignore `globals` array or string. + * + * @see https://mochajs.org/api/mocha#globals + */ + globals(globals: string | ReadonlyArray): this; + + /** + * Emit color output. + * + * @see https://mochajs.org/api/mocha#useColors + */ + useColors(colors: boolean): this; + + /** + * Use inline diffs rather than +/-. + * + * @see https://mochajs.org/api/mocha#useInlineDiffs + */ + useInlineDiffs(inlineDiffs: boolean): this; + + /** + * Do not show diffs at all. + * + * @see https://mochajs.org/api/mocha#hideDiff + */ + hideDiff(hideDiff: boolean): this; + + /** + * Set the timeout in milliseconds. + * + * @see https://mochajs.org/api/mocha#timeout + */ + timeout(timeout: string | number): this; + + /** + * Set the number of times to retry failed tests. + * + * @see https://mochajs.org/api/mocha#retries + */ + retries(n: number): this; + + /** + * Set slowness threshold in milliseconds. + * + * @see https://mochajs.org/api/mocha#slow + */ + slow(slow: string | number): this; + + /** + * Enable timeouts. + * + * @see https://mochajs.org/api/mocha#enableTimeouts + */ + enableTimeouts(enabled?: boolean): this; + + /** + * Makes all tests async (accepting a callback) + * + * @see https://mochajs.org/api/mocha#asyncOnly. + */ + asyncOnly(): this; + + /** + * Disable syntax highlighting (in browser). + * + * @see https://mochajs.org/api/mocha#noHighlighting + */ + noHighlighting(): this; + + /** + * Enable uncaught errors to propagate (in browser). + * + * @see https://mochajs.org/api/mocha#allowUncaught + */ + allowUncaught(): boolean; + + /** + * Delay root suite execution. + * + * @see https://mochajs.org/api/mocha#delay + */ + delay(): boolean; + + /** + * Tests marked only fail the suite + * + * @see https://mochajs.org/api/mocha#forbidOnly + */ + forbidOnly(): boolean; + + /** + * Pending tests and tests marked skip fail the suite + * + * @see https://mochajs.org/api/mocha#forbidPending + */ + forbidPending(): boolean; + + /** + * Run tests and invoke `fn()` when complete. + * + * Note that `run` relies on Node's `require` to execute + * the test interface functions and will be subject to the + * cache - if the files are already in the `require` cache, + * they will effectively be skipped. Therefore, to run tests + * multiple times or to run tests in files that are already + * in the `require` cache, make sure to clear them from the + * cache first in whichever manner best suits your needs. + * + * @see https://mochajs.org/api/mocha#run + */ + run(fn?: (failures: number) => void): Mocha.Runner; + + /** + * Load registered files. + * + * @see https://mochajs.org/api/mocha#loadFiles + */ + protected loadFiles(fn?: () => void): void; +} + +declare namespace Mocha { + namespace utils { + /** + * Compute a slug from the given `str`. + * + * @see https://mochajs.org/api/module-utils.html#.slug + */ + function slug(str: string): string; + + /** + * Strip the function definition from `str`, and re-indent for pre whitespace. + * + * @see https://mochajs.org/api/module-utils.html#.clean + */ + function clean(str: string): string; + + /** + * Highlight the given string of `js`. + */ + function highlight(js: string): string; + + /** + * Takes some variable and asks `Object.prototype.toString()` what it thinks it is. + */ + function type(value: any): string; + + /** + * Stringify `value`. Different behavior depending on type of value: + * + * - If `value` is undefined or null, return `'[undefined]'` or `'[null]'`, respectively. + * - If `value` is not an object, function or array, return result of `value.toString()` wrapped in double-quotes. + * - If `value` is an *empty* object, function, or array, returns `'{}'`, `'[Function]'`, or `'[]'` respectively. + * - If `value` has properties, call canonicalize} on it, then return result of `JSON.stringify()` + * + * @see https://mochajs.org/api/module-utils.html#.stringify + */ + function stringify(value: any): string; + + /** + * Return a new Thing that has the keys in sorted order. Recursive. + * + * If the Thing... + * - has already been seen, return string `'[Circular]'` + * - is `undefined`, return string `'[undefined]'` + * - is `null`, return value `null` + * - is some other primitive, return the value + * - is not a primitive or an `Array`, `Object`, or `Function`, return the value of the Thing's `toString()` method + * - is a non-empty `Array`, `Object`, or `Function`, return the result of calling this function again. + * - is an empty `Array`, `Object`, or `Function`, returns `'[]'`, `'{}'`, or `'[Function]'` respectively. + * + * @see https://mochajs.org/api/module-utils.html#.canonicalize + */ + function canonicalize(value: any, stack: any[], typeHint: string): any; + + /** + * Lookup file names at the given `path`. + * + * @see https://mochajs.org/api/Mocha.utils.html#.exports.lookupFiles + */ + function lookupFiles(filepath: string, extensions?: string[], recursive?: boolean): string[]; + + /** + * Generate an undefined error with a message warning the user. + * + * @see https://mochajs.org/api/module-utils.html#.undefinedError + */ + function undefinedError(): Error; + + /** + * Generate an undefined error if `err` is not defined. + * + * @see https://mochajs.org/api/module-utils.html#.getError + */ + function getError(err: Error | undefined): Error; + + /** + * When invoking this function you get a filter function that get the Error.stack as an + * input, and return a prettify output. (i.e: strip Mocha and internal node functions from + * stack trace). + * + * @see https://mochajs.org/api/module-utils.html#.stackTraceFilter + */ + function stackTraceFilter(): (stack: string) => string; + } + + namespace interfaces { + function bdd(suite: Suite): void; + function tdd(suite: Suite): void; + function qunit(suite: Suite): void; + function exports(suite: Suite): void; + } + + // #region Test interface augmentations + + interface HookFunction { + /** + * [bdd, qunit, tdd] Describe a "hook" to execute the given callback `fn`. The name of the + * function is used as the name of the hook. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: Func): void; + + /** + * [bdd, qunit, tdd] Describe a "hook" to execute the given callback `fn`. The name of the + * function is used as the name of the hook. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: AsyncFunc): void; + + /** + * [bdd, qunit, tdd] Describe a "hook" to execute the given `title` and callback `fn`. + * + * - _Only available when invoked via the mocha CLI._ + */ + (name: string, fn?: Func): void; + + /** + * [bdd, qunit, tdd] Describe a "hook" to execute the given `title` and callback `fn`. + * + * - _Only available when invoked via the mocha CLI._ + */ + (name: string, fn?: AsyncFunc): void; + } + + interface SuiteFunction { + /** + * [bdd, tdd] Describe a "suite" with the given `title` and callback `fn` containing + * nested suites. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn: (this: Suite) => void): Suite; + + /** + * [qunit] Describe a "suite" with the given `title`. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string): Suite; + + /** + * [bdd, tdd, qunit] Indicates this suite should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + only: ExclusiveSuiteFunction; + + /** + * [bdd, tdd] Indicates this suite should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + skip: PendingSuiteFunction; + } + + interface ExclusiveSuiteFunction { + /** + * [bdd, tdd] Describe a "suite" with the given `title` and callback `fn` containing + * nested suites. Indicates this suite should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn: (this: Suite) => void): Suite; + + /** + * [qunit] Describe a "suite" with the given `title`. Indicates this suite should be executed + * exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string): Suite; + } + + /** + * [bdd, tdd] Describe a "suite" with the given `title` and callback `fn` containing + * nested suites. Indicates this suite should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + * + * @returns [bdd] `Suite` + * @returns [tdd] `void` + */ + type PendingSuiteFunction = (title: string, fn: (this: Suite) => void) => Suite | void; + + interface TestFunction { + /** + * Describe a specification or test-case with the given callback `fn` acting as a thunk. + * The name of the function is used as the name of the test. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: Func): Test; + + /** + * Describe a specification or test-case with the given callback `fn` acting as a thunk. + * The name of the function is used as the name of the test. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: AsyncFunc): Test; + + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: Func): Test; + + /** + * Describe a specification or test-case with the given `title` and callback `fn` acting + * as a thunk. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: AsyncFunc): Test; + + /** + * Indicates this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + only: ExclusiveTestFunction; + + /** + * Indicates this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + skip: PendingTestFunction; + + /** + * Number of attempts to retry. + * + * - _Only available when invoked via the mocha CLI._ + */ + retries(n: number): void; + } + + interface ExclusiveTestFunction { + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given callback `fn` + * acting as a thunk. The name of the function is used as the name of the test. Indicates + * this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: Func): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given callback `fn` + * acting as a thunk. The name of the function is used as the name of the test. Indicates + * this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: AsyncFunc): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given `title` and + * callback `fn` acting as a thunk. Indicates this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: Func): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given `title` and + * callback `fn` acting as a thunk. Indicates this test should be executed exclusively. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: AsyncFunc): Test; + } + + interface PendingTestFunction { + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given callback `fn` + * acting as a thunk. The name of the function is used as the name of the test. Indicates + * this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: Func): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given callback `fn` + * acting as a thunk. The name of the function is used as the name of the test. Indicates + * this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + (fn: AsyncFunc): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given `title` and + * callback `fn` acting as a thunk. Indicates this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: Func): Test; + + /** + * [bdd, tdd, qunit] Describe a specification or test-case with the given `title` and + * callback `fn` acting as a thunk. Indicates this test should not be executed. + * + * - _Only available when invoked via the mocha CLI._ + */ + (title: string, fn?: AsyncFunc): Test; + } + + /** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ + let afterEach: HookFunction; + + /** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ + let after: HookFunction; + + /** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ + let beforeEach: HookFunction; + + /** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ + let before: HookFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + let describe: SuiteFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + let it: TestFunction; + + /** + * Describes a pending test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + let xit: PendingTestFunction; + + /** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ + let setup: HookFunction; + + /** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ + let suiteSetup: HookFunction; + + /** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ + let suiteTeardown: HookFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + let suite: SuiteFunction; + + /** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ + let teardown: HookFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + let test: TestFunction; + + /** + * Triggers root suite execution. + * + * - _Only available if flag --delay is passed into Mocha._ + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#runWithSuite + */ + function run(): void; + + // #endregion Test interface augmentations + + namespace reporters { + /** + * Initialize a new `Base` reporter. + * + * All other reporters generally inherit from this reporter, providing stats such as test duration, + * number of tests passed / failed, etc. + * + * @see https://mochajs.org/api/Mocha.reporters.Base.html + */ + class Base { + constructor(runner: Runner, options?: MochaOptions); + /** @deprecated Use the overload that accepts `Mocha.Runner` instead. */ + constructor(runner: IRunner, options?: MochaOptions); + + /** + * Test run statistics + */ + stats: Stats; + + /** + * Test failures + */ + failures: Test[]; + + /** + * The configured runner + */ + runner: Runner; + + /** + * Output common epilogue used by many of the bundled reporters. + * + * @see https://mochajs.org/api/Mocha.reporters.Base.html#.Base#epilogue + */ + epilogue(): void; + + done?(failures: number, fn?: (failures: number) => void): void; + } + + namespace Base { + /** + * Enables coloring by default + * + * @see https://mochajs.org/api/module-base#.useColors + */ + let useColors: boolean; + + /** + * Inline diffs instead of +/- + * + * @see https://mochajs.org/api/module-base#.inlineDiffs + */ + let inlineDiffs: boolean; + + /** + * Default color map + * + * @see https://mochajs.org/api/module-base#.colors + */ + const colors: ColorMap; + + /** + * Default color map + * + * @see https://mochajs.org/api/module-base#.colors + */ + interface ColorMap { + // added by Base + pass: number; + fail: number; + "bright pass": number; + "bright fail": number; + "bright yellow": number; + pending: number; + suite: number; + "error title": number; + "error message": number; + "error stack": number; + checkmark: number; + fast: number; + medium: number; + slow: number; + green: number; + light: number; + "diff gutter": number; + "diff added": number; + "diff removed": number; + + // added by Progress + progress: number; + + // added by Landing + plane: number; + "plane crash": number; + runway: number; + + [key: string]: number; + } + + /** + * Default symbol map + * + * @see https://mochajs.org/api/module-base#.symbols + */ + const symbols: SymbolMap; + + /** + * Default symbol map + * + * @see https://mochajs.org/api/module-base#.symbols + */ + interface SymbolMap { + ok: string; + err: string; + dot: string; + comma: string; + bang: string; + [key: string]: string; + } + + /** + * Color `str` with the given `type` (from `colors`) + * + * @see https://mochajs.org/api/module-base#.color + */ + function color(type: string, str: string): string; + + /** + * Expose terminal window size + * + * @see https://mochajs.org/api/module-base#.window + */ + const window: { + width: number; + }; + + /** + * ANSI TTY control sequences common among reporters. + * + * @see https://mochajs.org/api/module-base#.cursor + */ + namespace cursor { + /** + * Hides the cursor + */ + function hide(): void; + + /** + * Shows the cursor + */ + function show(): void; + + /** + * Deletes the current line + */ + function deleteLine(): void; + + /** + * Moves to the beginning of the line + */ + function beginningOfLine(): void; + + /** + * Clears the line and moves to the beginning of the line. + */ + function CR(): void; + } + + /** + * Returns a diff between two strings with colored ANSI output. + * + * @see https://mochajs.org/api/module-base#.generateDiff + */ + function generateDiff(actual: string, expected: string): string; + + /** + * Output the given `failures` as a list. + * + * @see https://mochajs.org/api/Mocha.reporters.Base.html#.exports.list1 + */ + function list(failures: Test[]): void; + } + + /** + * Initialize a new `Dot` matrix test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.Dot.html + */ + class Dot extends Base { + } + + /** + * Initialize a new `Doc` reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.Doc.html + */ + class Doc extends Base { + } + + /** + * Initialize a new `TAP` test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.TAP.html + */ + class TAP extends Base { + } + + /** + * Initialize a new `JSON` reporter + * + * @see https://mochajs.org/api/Mocha.reporters.JSON.html + */ + class JSON extends Base { + } + + /** + * Initialize a new `HTML` reporter. + * + * - _This reporter cannot be used on the console._ + * + * @see https://mochajs.org/api/Mocha.reporters.HTML.html + */ + class HTML extends Base { + /** + * Provide suite URL. + * + * @see https://mochajs.org/api/Mocha.reporters.HTML.html#suiteURL + */ + suiteURL(suite: Suite): string; + + /** + * Provide test URL. + * + * @see https://mochajs.org/api/Mocha.reporters.HTML.html#testURL + */ + testURL(test: Test): string; + + /** + * Adds code toggle functionality for the provided test's list element. + * + * @see https://mochajs.org/api/Mocha.reporters.HTML.html#addCodeToggle + */ + addCodeToggle(el: HTMLLIElement, contents: string): void; + } + + /** + * Initialize a new `List` test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.List.html + */ + class List extends Base { + } + + /** + * Initialize a new `Min` minimal test reporter (best used with --watch). + * + * @see https://mochajs.org/api/Mocha.reporters.Min.html + */ + class Min extends Base { + } + + /** + * Initialize a new `Spec` test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.Spec.html + */ + class Spec extends Base { + } + + /** + * Initialize a new `NyanCat` test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.Nyan.html + */ + class Nyan extends Base { + private colorIndex; + private numberOfLines; + private rainbowColors; + private scoreboardWidth; + private tick; + private trajectories; + private trajectoryWidthMax; + private draw; + private drawScoreboard; + private appendRainbow; + private drawRainbow; + private drawNyanCat; + private face; + private cursorUp; + private cursorDown; + private generateColors; + private rainbowify; + } + + /** + * Initialize a new `XUnit` test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.XUnit.html + */ + class XUnit extends Base { + constructor(runner: Runner, options?: XUnit.MochaOptions); + /** @deprecated Use the overload that accepts `Mocha.Runner` instead. */ + constructor(runner: IRunner, options?: XUnit.MochaOptions); + + /** + * Override done to close the stream (if it's a file). + * + * @see https://mochajs.org/api/Mocha.reporters.XUnit.html#done + */ + done(failures: number, fn: (failures: number) => void): void; + + /** + * Write out the given line. + * + * @see https://mochajs.org/api/Mocha.reporters.XUnit.html#write + */ + write(line: string): void; + + /** + * Output tag for the given `test.` + * + * @see https://mochajs.org/api/Mocha.reporters.XUnit.html#test + */ + test(test: Test): void; + } + + namespace XUnit { + interface MochaOptions extends Mocha.MochaOptions { + reporterOptions?: ReporterOptions; + } + + interface ReporterOptions { + output?: string; + suiteName?: string; + } + } + + /** + * Initialize a new `Markdown` test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.Markdown.html + */ + class Markdown extends Base { + } + + /** + * Initialize a new `Progress` bar test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.Progress.html + */ + class Progress extends Base { + constructor(runner: Runner, options?: Progress.MochaOptions); + /** @deprecated Use the overload that accepts `Mocha.Runner` instead. */ + constructor(runner: IRunner, options?: Progress.MochaOptions); + } + + namespace Progress { + interface MochaOptions extends Mocha.MochaOptions { + reporterOptions?: ReporterOptions; + } + + interface ReporterOptions { + open?: string; + complete?: string; + incomplete?: string; + close?: string; + verbose?: boolean; + } + } + + /** + * Initialize a new `Landing` reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.Landing.html + */ + class Landing extends Base { + } + + /** + * Initialize a new `JSONStream` test reporter. + * + * @see https://mochajs.org/api/Mocha.reporters.JSONStream.html + */ + class JSONStream extends Base { + } + + // value-only aliases + const base: typeof Base; + const dot: typeof Dot; + const doc: typeof Doc; + const tap: typeof TAP; + const json: typeof JSON; + const html: typeof HTML; + const list: typeof List; + const spec: typeof Spec; + const nyan: typeof Nyan; + const xunit: typeof XUnit; + const markdown: typeof Markdown; + const progress: typeof Progress; + const landing: typeof Landing; + // NOTE: not possible to type this correctly: + // const "json-stream": typeof JSONStream; + } + + /** + * Initialize a new `Runnable` with the given `title` and callback `fn`. + * + * @see https://mochajs.org/api/Runnable.html + */ + class Runnable { + private _slow; + private _enableTimeouts; + private _retries; + private _currentRetry; + private _timeout; + private _timeoutError; + + constructor(title: string, fn?: Func | AsyncFunc); + + title: string; + fn: Func | AsyncFunc | undefined; + body: string; + async: boolean; + sync: boolean; + timedOut: boolean; + pending: boolean; + duration?: number; + parent?: Suite; + state?: "failed" | "passed"; + timer?: any; + ctx?: Context; + callback?: Done; + allowUncaught?: boolean; + file?: string; + + /** + * Get test timeout. + * + * @see https://mochajs.org/api/Runnable.html#timeout + */ + timeout(): number; + + /** + * Set test timeout. + * + * @see https://mochajs.org/api/Runnable.html#timeout + */ + timeout(ms: string | number): this; + + /** + * Get test slowness threshold. + * + * @see https://mochajs.org/api/Runnable.html#slow + */ + slow(): number; + + /** + * Set test slowness threshold. + * + * @see https://mochajs.org/api/Runnable.html#slow + */ + slow(ms: string | number): this; + + /** + * Get whether timeouts are enabled. + * + * @see https://mochajs.org/api/Runnable.html#enableTimeouts + */ + enableTimeouts(): boolean; + + /** + * Set whether timeouts are enabled. + * + * @see https://mochajs.org/api/Runnable.html#enableTimeouts + */ + enableTimeouts(enabled: boolean): this; + + /** + * Halt and mark as pending. + */ + skip(): never; + + /** + * Check if this runnable or its parent suite is marked as pending. + * + * @see https://mochajs.org/api/Runnable.html#isPending + */ + isPending(): boolean; + + /** + * Return `true` if this Runnable has failed. + */ + isFailed(): boolean; + + /** + * Return `true` if this Runnable has passed. + */ + isPassed(): boolean; + + /** + * Set or get number of retries. + * + * @see https://mochajs.org/api/Runnable.html#retries + */ + retries(): number; + + /** + * Set or get number of retries. + * + * @see https://mochajs.org/api/Runnable.html#retries + */ + retries(n: number): void; + + /** + * Set or get current retry + * + * @see https://mochajs.org/api/Runnable.html#currentRetry + */ + protected currentRetry(): number; + + /** + * Set or get current retry + * + * @see https://mochajs.org/api/Runnable.html#currentRetry + */ + protected currentRetry(n: number): void; + + /** + * Return the full title generated by recursively concatenating the parent's full title. + */ + fullTitle(): string; + + /** + * Return the title path generated by concatenating the parent's title path with the title. + */ + titlePath(): string[]; + + /** + * Clear the timeout. + * + * @see https://mochajs.org/api/Runnable.html#clearTimeout + */ + clearTimeout(): void; + + /** + * Inspect the runnable void of private properties. + * + * @see https://mochajs.org/api/Runnable.html#inspect + */ + inspect(): string; + + /** + * Reset the timeout. + * + * @see https://mochajs.org/api/Runnable.html#resetTimeout + */ + resetTimeout(): void; + + /** + * Get a list of whitelisted globals for this test run. + * + * @see https://mochajs.org/api/Runnable.html#globals + */ + globals(): string[]; + + /** + * Set a list of whitelisted globals for this test run. + * + * @see https://mochajs.org/api/Runnable.html#globals + */ + globals(globals: ReadonlyArray): void; + + /** + * Run the test and invoke `fn(err)`. + * + * @see https://mochajs.org/api/Runnable.html#run + */ + run(fn: Done): void; + } + + // #region Runnable "error" event + interface Runnable extends NodeJS.EventEmitter { + on(event: "error", listener: (error: any) => void): this; + once(event: "error", listener: (error: any) => void): this; + addListener(event: "error", listener: (error: any) => void): this; + removeListener(event: "error", listener: (error: any) => void): this; + prependListener(event: "error", listener: (error: any) => void): this; + prependOnceListener(event: "error", listener: (error: any) => void): this; + emit(name: "error", error: any): boolean; + } + // #endregion Runnable "error" event + // #region Runnable untyped events + interface Runnable extends NodeJS.EventEmitter { + on(event: string, listener: (...args: any[]) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + emit(name: string, ...args: any[]): boolean; + } + // #endregion Runnable untyped events + + /** + * Test context + * + * @see https://mochajs.org/api/module-Context.html#~Context + */ + class Context { + private _runnable; + + test?: Runnable; + currentTest?: Test; + + /** + * Get the context `Runnable`. + */ + runnable(): Runnable; + + /** + * Set the context `Runnable`. + */ + runnable(runnable: Runnable): this; + /** @deprecated Use the overload that accepts `Mocha.Runnable` instead. */ + runnable(runnable: IRunnable): this; + + /** + * Get test timeout. + */ + timeout(): number; + + /** + * Set test timeout. + */ + timeout(ms: string | number): this; + + /** + * Get whether timeouts are enabled. + */ + enableTimeouts(): boolean; + + /** + * Set whether timeouts are enabled. + */ + enableTimeouts(enabled: boolean): this; + + /** + * Get test slowness threshold. + */ + slow(): number; + + /** + * Set test slowness threshold. + */ + slow(ms: string | number): this; + + /** + * Mark a test as skipped. + */ + skip(): never; + + /** + * Get the number of allowed retries on failed tests. + */ + retries(): number; + + /** + * Set the number of allowed retries on failed tests. + */ + retries(n: number): this; + + [key: string]: any; + } + + /** + * Initialize a `Runner` for the given `suite`. + * + * @see https://mochajs.org/api/Mocha.Runner.html + */ + class Runner { + private _globals; + private _abort; + private _delay; + private _defaultGrep; + private next; + private hookErr; + private prevGlobalsLength; + private nextSuite; + + constructor(suite: Suite, delay: boolean); + + /** @deprecated Use the overload that accepts `Mocha.Suite` instead. */ + constructor(suite: ISuite, delay: boolean); + + suite: Suite; + started: boolean; + total: number; + failures: number; + asyncOnly?: boolean; + allowUncaught?: boolean; + fullStackTrace?: boolean; + forbidOnly?: boolean; + forbidPending?: boolean; + ignoreLeaks?: boolean; + test?: Test; + currentRunnable?: Runnable; + stats?: Stats; // added by reporters + + /** + * Run tests with full titles matching `re`. Updates runner.total + * with number of tests matched. + * + * @see https://mochajs.org/api/Mocha.Runner.html#.Runner#grep + */ + grep(re: RegExp, invert: boolean): this; + + /** + * Returns the number of tests matching the grep search for the + * given suite. + * + * @see https://mochajs.org/api/Mocha.Runner.html#.Runner#grepTotal + */ + grepTotal(suite: Suite): number; + + /** @deprecated Use the overload that accepts `Mocha.Suite` instead. */ + grepTotal(suite: ISuite): number; + + /** + * Gets the allowed globals. + * + * @see https://mochajs.org/api/Mocha.Runner.html#.Runner#globals + */ + globals(): string[]; + + /** + * Allow the given `arr` of globals. + * + * @see https://mochajs.org/api/Mocha.Runner.html#.Runner#globals + */ + globals(arr: ReadonlyArray): this; + + /** + * Run the root suite and invoke `fn(failures)` on completion. + * + * @see https://mochajs.org/api/Mocha.Runner.html#.Runner#run + */ + run(fn?: (failures: number) => void): this; + + /** + * Cleanly abort execution. + * + * @see https://mochajs.org/api/Mocha.Runner.html#.Runner#abort + */ + abort(): this; + + /** + * Handle uncaught exceptions. + * + * @see https://mochajs.org/api/Mocha.Runner.html#uncaught + */ + uncaught(err: any): void; + + /** + * Wrapper for setImmediate, process.nextTick, or browser polyfill. + */ + protected static immediately(callback: Function): void; + + /** + * Return a list of global properties. + * + * @see https://mochajs.org/api/Mocha.Runner.html#globalProps + */ + protected globalProps(): string[]; + + /** + * Check for global variable leaks. + * + * @see https://mochajs.org/api/Mocha.Runner.html#checkGlobals + */ + protected checkGlobals(test: Test): void; + + /** + * Fail the given `test`. + * + * @see https://mochajs.org/api/Mocha.Runner.html#fail + */ + protected fail(test: Test, err: any): void; + + /** + * Fail the given `hook` with `err`. + * + * Hook failures work in the following pattern: + * - If bail, then exit + * - Failed `before` hook skips all tests in a suite and subsuites, + * but jumps to corresponding `after` hook + * - Failed `before each` hook skips remaining tests in a + * suite and jumps to corresponding `after each` hook, + * which is run only once + * - Failed `after` hook does not alter + * execution order + * - Failed `after each` hook skips remaining tests in a + * suite and subsuites, but executes other `after each` + * hooks + * + * @see https://mochajs.org/api/Mocha.Runner.html#failHook + */ + protected failHook(hook: Hook, err: any): void; + + /** + * Run hook `name` callbacks and then invoke `fn()`. + * + * @see https://mochajs.org/api/Mocha.Runner.html#hook + */ + protected hook(name: string, fn: () => void): void; + + /** + * Run hook `name` for the given array of `suites` + * in order, and callback `fn(err, errSuite)`. + * + * @see https://mochajs.org/api/Mocha.Runner.html#hooks + */ + protected hooks(name: string, suites: Suite[], fn: (err?: any, errSuite?: Suite) => void): void; + + /** + * Run hooks from the top level down. + * + * @see https://mochajs.org/api/Mocha.Runner.html#hookUp + */ + protected hookUp(name: string, fn: (err?: any, errSuite?: Suite) => void): void; + + /** + * Run hooks from the bottom up. + * + * @see https://mochajs.org/api/Mocha.Runner.html#hookDown + */ + protected hookDown(name: string, fn: (err?: any, errSuite?: Suite) => void): void; + + /** + * Return an array of parent Suites from closest to furthest. + * + * @see https://mochajs.org/api/Mocha.Runner.html#parents + */ + protected parents(): Suite[]; + + /** + * Run the current test and callback `fn(err)`. + * + * @see https://mochajs.org/api/Mocha.Runner.html#runTest + */ + protected runTest(fn: Done): any; + + /** + * Run tests in the given `suite` and invoke the callback `fn()` when complete. + * + * @see https://mochajs.org/api/Mocha.Runner.html#runTests + */ + protected runTests(suite: Suite, fn: (errSuite?: Suite) => void): void; + + /** + * Run the given `suite` and invoke the callback `fn()` when complete. + * + * @see https://mochajs.org/api/Mocha.Runner.html#runSuite + */ + protected runSuite(suite: Suite, fn: (errSuite?: Suite) => void): void; + } + + // #region Runner "waiting" event + interface Runner { + on(event: "waiting", listener: (rootSuite: Suite) => void): this; + once(event: "waiting", listener: (rootSuite: Suite) => void): this; + addListener(event: "waiting", listener: (rootSuite: Suite) => void): this; + removeListener(event: "waiting", listener: (rootSuite: Suite) => void): this; + prependListener(event: "waiting", listener: (rootSuite: Suite) => void): this; + prependOnceListener(event: "waiting", listener: (rootSuite: Suite) => void): this; + emit(name: "waiting", rootSuite: Suite): boolean; + } + // #endregion Runner "waiting" event + // #region Runner "start" event + interface Runner extends NodeJS.EventEmitter { + on(event: "start", listener: () => void): this; + once(event: "start", listener: () => void): this; + addListener(event: "start", listener: () => void): this; + removeListener(event: "start", listener: () => void): this; + prependListener(event: "start", listener: () => void): this; + prependOnceListener(event: "start", listener: () => void): this; + emit(name: "start"): boolean; + } + // #endregion Runner "start" event + // #region Runner "end" event + interface Runner extends NodeJS.EventEmitter { + on(event: "end", listener: () => void): this; + once(event: "end", listener: () => void): this; + addListener(event: "end", listener: () => void): this; + removeListener(event: "end", listener: () => void): this; + prependListener(event: "end", listener: () => void): this; + prependOnceListener(event: "end", listener: () => void): this; + emit(name: "end"): boolean; + } + // #endregion Runner "end" event + // #region Runner "suite" event + interface Runner extends NodeJS.EventEmitter { + on(event: "suite", listener: (suite: Suite) => void): this; + once(event: "suite", listener: (suite: Suite) => void): this; + addListener(event: "suite", listener: (suite: Suite) => void): this; + removeListener(event: "suite", listener: (suite: Suite) => void): this; + prependListener(event: "suite", listener: (suite: Suite) => void): this; + prependOnceListener(event: "suite", listener: (suite: Suite) => void): this; + emit(name: "suite", suite: Suite): boolean; + } + // #endregion Runner "suite" event + // #region Runner "suite end" event + interface Runner extends NodeJS.EventEmitter { + on(event: "suite end", listener: (suite: Suite) => void): this; + once(event: "suite end", listener: (suite: Suite) => void): this; + addListener(event: "suite end", listener: (suite: Suite) => void): this; + removeListener(event: "suite end", listener: (suite: Suite) => void): this; + prependListener(event: "suite end", listener: (suite: Suite) => void): this; + prependOnceListener(event: "suite end", listener: (suite: Suite) => void): this; + emit(name: "suite end", suite: Suite): boolean; + } + // #endregion Runner "suite end" event + // #region Runner "test" event + interface Runner extends NodeJS.EventEmitter { + on(event: "test", listener: (test: Test) => void): this; + once(event: "test", listener: (test: Test) => void): this; + addListener(event: "test", listener: (test: Test) => void): this; + removeListener(event: "test", listener: (test: Test) => void): this; + prependListener(event: "test", listener: (test: Test) => void): this; + prependOnceListener(event: "test", listener: (test: Test) => void): this; + emit(name: "test", test: Test): boolean; + } + // #endregion Runner "test" event + // #region Runner "test end" event + interface Runner extends NodeJS.EventEmitter { + on(event: "test end", listener: (test: Test) => void): this; + once(event: "test end", listener: (test: Test) => void): this; + addListener(event: "test end", listener: (test: Test) => void): this; + removeListener(event: "test end", listener: (test: Test) => void): this; + prependListener(event: "test end", listener: (test: Test) => void): this; + prependOnceListener(event: "test end", listener: (test: Test) => void): this; + emit(name: "test end", test: Test): boolean; + } + // #endregion Runner "test end" event + // #region Runner "hook" event + interface Runner extends NodeJS.EventEmitter { + on(event: "hook", listener: (hook: Hook) => void): this; + once(event: "hook", listener: (hook: Hook) => void): this; + addListener(event: "hook", listener: (hook: Hook) => void): this; + removeListener(event: "hook", listener: (hook: Hook) => void): this; + prependListener(event: "hook", listener: (hook: Hook) => void): this; + prependOnceListener(event: "hook", listener: (hook: Hook) => void): this; + emit(name: "hook", hook: Hook): boolean; + } + // #endregion Runner "hook" event + // #region Runner "hook end" event + interface Runner extends NodeJS.EventEmitter { + on(event: "hook end", listener: (hook: Hook) => void): this; + once(event: "hook end", listener: (hook: Hook) => void): this; + addListener(event: "hook end", listener: (hook: Hook) => void): this; + removeListener(event: "hook end", listener: (hook: Hook) => void): this; + prependListener(event: "hook end", listener: (hook: Hook) => void): this; + prependOnceListener(event: "hook end", listener: (hook: Hook) => void): this; + emit(name: "hook end", hook: Hook): boolean; + } + // #endregion Runner "hook end" event + // #region Runner "pass" event + interface Runner extends NodeJS.EventEmitter { + on(event: "pass", listener: (test: Test) => void): this; + once(event: "pass", listener: (test: Test) => void): this; + addListener(event: "pass", listener: (test: Test) => void): this; + removeListener(event: "pass", listener: (test: Test) => void): this; + prependListener(event: "pass", listener: (test: Test) => void): this; + prependOnceListener(event: "pass", listener: (test: Test) => void): this; + emit(name: "pass", test: Test): boolean; + } + // #endregion Runner "pass" event + // #region Runner "fail" event + interface Runner extends NodeJS.EventEmitter { + on(event: "fail", listener: (test: Test, err: any) => void): this; + once(event: "fail", listener: (test: Test, err: any) => void): this; + addListener(event: "fail", listener: (test: Test, err: any) => void): this; + removeListener(event: "fail", listener: (test: Test, err: any) => void): this; + prependListener(event: "fail", listener: (test: Test, err: any) => void): this; + prependOnceListener(event: "fail", listener: (test: Test, err: any) => void): this; + emit(name: "fail", test: Test, err: any): boolean; + } + // #endregion Runner "fail" event + // #region Runner "pending" event + interface Runner extends NodeJS.EventEmitter { + on(event: "pending", listener: (test: Test) => void): this; + once(event: "pending", listener: (test: Test) => void): this; + addListener(event: "pending", listener: (test: Test) => void): this; + removeListener(event: "pending", listener: (test: Test) => void): this; + prependListener(event: "pending", listener: (test: Test) => void): this; + prependOnceListener(event: "pending", listener: (test: Test) => void): this; + emit(name: "pending", test: Test): boolean; + } + // #endregion Runner "pending" event + // #region Runner untyped events + interface Runner extends NodeJS.EventEmitter { + on(event: string, listener: (...args: any[]) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + emit(name: string, ...args: any[]): boolean; + } + // #endregion Runner untyped events + + /** + * Initialize a new `Suite` with the given `title` and `ctx`. + * + * @see https://mochajs.org/api/Mocha.Suite.html + */ + class Suite { + private _beforeEach; + private _beforeAll; + private _afterEach; + private _afterAll; + private _timeout; + private _enableTimeouts; + private _slow; + private _bail; + private _retries; + private _onlyTests; + private _onlySuites; + + constructor(title: string, parentContext?: Context); + /** @deprecated Use the overload that accepts `Mocha.Context` instead. */ + constructor(title: string, parentContext?: IContext); + + ctx: Context; + suites: Suite[]; + tests: Test[]; + pending: boolean; + file?: string; + root: boolean; + delayed: boolean; + parent: Suite | undefined; + title: string; + + /** + * Create a new `Suite` with the given `title` and parent `Suite`. When a suite + * with the same title is already present, that suite is returned to provide + * nicer reporter and more flexible meta-testing. + * + * @see https://mochajs.org/api/mocha#.exports.create + */ + static create(parent: Suite, title: string): Suite; + /** @deprecated Use the overload that accepts `Mocha.Suite` instead. */ + static create(parent: ISuite, title: string): Suite; + + /** + * Return a clone of this `Suite`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#clone + */ + clone(): Suite; + + /** + * Get timeout `ms`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#timeout + */ + timeout(): number; + + /** + * Set timeout `ms` or short-hand such as "2s". + * + * @see https://mochajs.org/api/Mocha.Suite.html#timeout + */ + timeout(ms: string | number): this; + + /** + * Get number of times to retry a failed test. + * + * @see https://mochajs.org/api/Mocha.Suite.html#retries + */ + retries(): number; + + /** + * Set number of times to retry a failed test. + * + * @see https://mochajs.org/api/Mocha.Suite.html#retries + */ + retries(n: string | number): this; + + /** + * Get whether timeouts are enabled. + * + * @see https://mochajs.org/api/Mocha.Suite.html#enableTimeouts + */ + enableTimeouts(): boolean; + + /** + * Set whether timeouts are `enabled`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#enableTimeouts + */ + enableTimeouts(enabled: boolean): this; + + /** + * Get slow `ms`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#slow + */ + slow(): number; + + /** + * Set slow `ms` or short-hand such as "2s". + * + * @see https://mochajs.org/api/Mocha.Suite.html#slow + */ + slow(ms: string | number): this; + + /** + * Get whether to bail after first error. + * + * @see https://mochajs.org/api/Mocha.Suite.html#bail + */ + bail(): boolean; + + /** + * Set whether to bail after first error. + * + * @see https://mochajs.org/api/Mocha.Suite.html#bail + */ + bail(bail: boolean): this; + + /** + * Check if this suite or its parent suite is marked as pending. + * + * @see https://mochajs.org/api/Mocha.Suite.html#isPending + */ + isPending(): boolean; + + /** + * Run `fn(test[, done])` before running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeAll + */ + beforeAll(fn?: Func): this; + + /** + * Run `fn(test[, done])` before running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeAll + */ + beforeAll(fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` before running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeAll + */ + beforeAll(title: string, fn?: Func): this; + + /** + * Run `fn(test[, done])` before running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeAll + */ + beforeAll(title: string, fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` after running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterAll + */ + afterAll(fn?: Func): this; + + /** + * Run `fn(test[, done])` after running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterAll + */ + afterAll(fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` after running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterAll + */ + afterAll(title: string, fn?: Func): this; + + /** + * Run `fn(test[, done])` after running tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterAll + */ + afterAll(title: string, fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` before each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeEach + */ + beforeEach(fn?: Func): this; + + /** + * Run `fn(test[, done])` before each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeEach + */ + beforeEach(fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` before each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeEach + */ + beforeEach(title: string, fn?: Func): this; + + /** + * Run `fn(test[, done])` before each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#beforeEach + */ + beforeEach(title: string, fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` after each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterEach + */ + afterEach(fn?: Func): this; + + /** + * Run `fn(test[, done])` after each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterEach + */ + afterEach(fn?: AsyncFunc): this; + + /** + * Run `fn(test[, done])` after each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterEach + */ + afterEach(title: string, fn?: Func): this; + + /** + * Run `fn(test[, done])` after each test case. + * + * @see https://mochajs.org/api/Mocha.Suite.html#afterEach + */ + afterEach(title: string, fn?: AsyncFunc): this; + + /** + * Add a test `suite`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#addSuite + */ + addSuite(suite: Suite): this; + /** @deprecated Use the overload that accepts `Mocha.ISuite` instead. */ + addSuite(suite: ISuite): this; + + /** + * Add a `test` to this suite. + * + * @see https://mochajs.org/api/Mocha.Suite.html#addTest + */ + addTest(test: Test): this; + /** @deprecated Use the overload that accepts `Mocha.ITest` instead. */ + addTest(test: ITest): this; + + /** + * Return the full title generated by recursively concatenating the parent's + * full title. + * + * @see https://mochajs.org/api/Mocha.Suite.html#.Suite#fullTitle + */ + fullTitle(): string; + + /** + * Return the title path generated by recursively concatenating the parent's + * title path. + * + * @see https://mochajs.org/api/Mocha.Suite.html#.Suite#titlePath + */ + titlePath(): string[]; + + /** + * Return the total number of tests. + * + * @see https://mochajs.org/api/Mocha.Suite.html#.Suite#total + */ + total(): number; + + /** + * Iterates through each suite recursively to find all tests. Applies a + * function in the format `fn(test)`. + * + * @see https://mochajs.org/api/Mocha.Suite.html#eachTest + */ + eachTest(fn: (test: Test) => void): this; + + /** + * This will run the root suite if we happen to be running in delayed mode. + * + * @see https://mochajs.org/api/Mocha.Suite.html#run + */ + run(): void; + + /** + * Generic hook-creator. + */ + protected _createHook(title: string, fn?: Func | AsyncFunc): Hook; + } + + // #region Suite "beforeAll" event + interface Suite extends NodeJS.EventEmitter { + on(event: "beforeAll", listener: (hook: Hook) => void): this; + once(event: "beforeAll", listener: (hook: Hook) => void): this; + addListener(event: "beforeAll", listener: (hook: Hook) => void): this; + removeListener(event: "beforeAll", listener: (hook: Hook) => void): this; + prependListener(event: "beforeAll", listener: (hook: Hook) => void): this; + prependOnceListener(event: "beforeAll", listener: (hook: Hook) => void): this; + emit(name: "beforeAll", hook: Hook): boolean; + } + // #endregion Suite "beforeAll" event + // #region Suite "afterAll" event + interface Suite extends NodeJS.EventEmitter { + on(event: "afterAll", listener: (hook: Hook) => void): this; + once(event: "afterAll", listener: (hook: Hook) => void): this; + addListener(event: "afterAll", listener: (hook: Hook) => void): this; + removeListener(event: "afterAll", listener: (hook: Hook) => void): this; + prependListener(event: "afterAll", listener: (hook: Hook) => void): this; + prependOnceListener(event: "afterAll", listener: (hook: Hook) => void): this; + emit(name: "afterAll", hook: Hook): boolean; + } + // #endregion Suite "afterAll" event + // #region Suite "beforeEach" event + interface Suite extends NodeJS.EventEmitter { + on(event: "beforeEach", listener: (hook: Hook) => void): this; + once(event: "beforeEach", listener: (hook: Hook) => void): this; + addListener(event: "beforeEach", listener: (hook: Hook) => void): this; + removeListener(event: "beforeEach", listener: (hook: Hook) => void): this; + prependListener(event: "beforeEach", listener: (hook: Hook) => void): this; + prependOnceListener(event: "beforeEach", listener: (hook: Hook) => void): this; + emit(name: "beforeEach", hook: Hook): boolean; + } + // #endregion Suite "beforeEach" event + // #region Suite "afterEach" event + interface Suite extends NodeJS.EventEmitter { + on(event: "afterEach", listener: (hook: Hook) => void): this; + once(event: "afterEach", listener: (hook: Hook) => void): this; + addListener(event: "afterEach", listener: (hook: Hook) => void): this; + removeListener(event: "afterEach", listener: (hook: Hook) => void): this; + prependListener(event: "afterEach", listener: (hook: Hook) => void): this; + prependOnceListener(event: "afterEach", listener: (hook: Hook) => void): this; + emit(name: "afterEach", hook: Hook): boolean; + } + // #endregion Suite "afterEach" event + // #region Suite "suite" event + interface Suite extends NodeJS.EventEmitter { + on(event: "suite", listener: (suite: Suite) => void): this; + once(event: "suite", listener: (suite: Suite) => void): this; + addListener(event: "suite", listener: (suite: Suite) => void): this; + removeListener(event: "suite", listener: (suite: Suite) => void): this; + prependListener(event: "suite", listener: (suite: Suite) => void): this; + prependOnceListener(event: "suite", listener: (suite: Suite) => void): this; + emit(name: "suite", suite: Suite): boolean; + } + // #endregion Suite "suite" event + // #region Suite "test" event + interface Suite { + on(event: "test", listener: (test: Test) => void): this; + once(event: "test", listener: (test: Test) => void): this; + addListener(event: "test", listener: (test: Test) => void): this; + removeListener(event: "test", listener: (test: Test) => void): this; + prependListener(event: "test", listener: (test: Test) => void): this; + prependOnceListener(event: "test", listener: (test: Test) => void): this; + emit(name: "test", test: Test): boolean; + } + // #endregion Suite "test" event + // #region Suite "run" event + interface Suite extends NodeJS.EventEmitter { + on(event: "run", listener: () => void): this; + once(event: "run", listener: () => void): this; + addListener(event: "run", listener: () => void): this; + removeListener(event: "run", listener: () => void): this; + prependListener(event: "run", listener: () => void): this; + prependOnceListener(event: "run", listener: () => void): this; + emit(name: "run"): boolean; + } + // #endregion Suite "run" event + // #region Suite "pre-require" event + interface Suite extends NodeJS.EventEmitter { + on(event: "pre-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + once(event: "pre-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + addListener(event: "pre-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + removeListener(event: "pre-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + prependListener(event: "pre-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + prependOnceListener(event: "pre-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + emit(name: "pre-require", context: MochaGlobals, file: string, mocha: Mocha): boolean; + } + // #endregion Suite "pre-require" event + // #region Suite "require" event + interface Suite extends NodeJS.EventEmitter { + on(event: "require", listener: (module: any, file: string, mocha: Mocha) => void): this; + once(event: "require", listener: (module: any, file: string, mocha: Mocha) => void): this; + addListener(event: "require", listener: (module: any, file: string, mocha: Mocha) => void): this; + removeListener(event: "require", listener: (module: any, file: string, mocha: Mocha) => void): this; + prependListener(event: "require", listener: (module: any, file: string, mocha: Mocha) => void): this; + prependOnceListener(event: "require", listener: (module: any, file: string, mocha: Mocha) => void): this; + emit(name: "require", module: any, file: string, mocha: Mocha): boolean; + } + // #endregion Suite "require" event + // #region Suite "post-require" event + interface Suite extends NodeJS.EventEmitter { + on(event: "post-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + once(event: "post-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + addListener(event: "post-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + removeListener(event: "post-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + prependListener(event: "post-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + prependOnceListener(event: "post-require", listener: (context: MochaGlobals, file: string, mocha: Mocha) => void): this; + emit(name: "post-require", context: MochaGlobals, file: string, mocha: Mocha): boolean; + } + // #endregion Suite "post-require" event + // #region Suite untyped events + interface Suite extends NodeJS.EventEmitter { + on(event: string, listener: (...args: any[]) => void): this; + once(event: string, listener: (...args: any[]) => void): this; + addListener(event: string, listener: (...args: any[]) => void): this; + removeListener(event: string, listener: (...args: any[]) => void): this; + prependListener(event: string, listener: (...args: any[]) => void): this; + prependOnceListener(event: string, listener: (...args: any[]) => void): this; + emit(name: string, ...args: any[]): boolean; + } + // #endregion Runner untyped events + + /** + * Initialize a new `Hook` with the given `title` and callback `fn` + * + * @see https://mochajs.org/api/Hook.html + */ + class Hook extends Runnable { + private _error; + + type: "hook"; + originalTitle?: string; // added by Runner + + /** + * Get the test `err`. + * + * @see https://mochajs.org/api/Hook.html#error + */ + error(): any; + + /** + * Set the test `err`. + * + * @see https://mochajs.org/api/Hook.html#error + */ + error(err: any): void; + } + + /** + * Initialize a new `Test` with the given `title` and callback `fn`. + * + * @see https://mochajs.org/api/Test.html + */ + class Test extends Runnable { + type: "test"; + speed?: "slow" | "medium" | "fast"; // added by reporters + err?: Error; // added by reporters + clone(): Test; + } + + /** + * Test statistics + */ + interface Stats { + suites: number; + tests: number; + passes: number; + pending: number; + failures: number; + start?: Date; + end?: Date; + duration?: number; + } + + type TestInterface = (suite: Suite) => void; + + interface ReporterConstructor { + new (runner: Runner, options: { reporterOptions?: any; }): reporters.Base; + } + + type Done = (err?: any) => void; + + /** + * Callback function used for tests and hooks. + */ + type Func = (this: Context, done: Done) => void; + + /** + * Async callback function used for tests and hooks. + */ + type AsyncFunc = (this: Context) => PromiseLike; + + /** + * Options to pass to Mocha. + */ + interface MochaOptions { + /** Test interfaces ("bdd", "tdd", "exports", etc.). */ + ui?: Interface; + + /** + * Reporter constructor, built-in reporter name, or reporter module path. Defaults to + * `"spec"`. + */ + reporter?: string | ReporterConstructor; + + /** Options to pass to the reporter. */ + reporterOptions?: any; + + /** Array of accepted globals. */ + globals?: string[]; + + /** timeout in milliseconds. */ + timeout?: number; + + enableTimeouts?: boolean; + + /** number of times to retry failed tests. */ + retries?: number; + + /** bail on the first test failure. */ + bail?: boolean; + + /** milliseconds to wait before considering a test slow. */ + slow?: number; + + /** ignore global leaks. */ + ignoreLeaks?: boolean; + + /** display the full stack trace on failure. */ + fullStackTrace?: boolean; + + /** string or regexp to filter tests with. */ + grep?: string | RegExp; + + /** Enable growl support. */ + growl?: boolean; + + /** Emit color output. */ + useColors?: boolean; + + /** Use inline diffs rather than +/-. */ + inlineDiffs?: boolean; + + /** Do not show diffs at all. */ + hideDiff?: boolean; + + asyncOnly?: boolean; + delay?: boolean; + forbidOnly?: boolean; + forbidPending?: boolean; + noHighlighting?: boolean; + allowUncaught?: boolean; + } + + interface MochaInstanceOptions extends MochaOptions { + files?: string[]; + } + + /** + * Variables added to the global scope by Mocha when run in the CLI. + */ + interface MochaGlobals { + /** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ + before: HookFunction; + + /** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ + after: HookFunction; + + /** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ + beforeEach: HookFunction; + + /** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ + afterEach: HookFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + describe: SuiteFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + context: SuiteFunction; + + /** + * Pending suite. + * + * - _Only available when invoked via the mocha CLI._ + */ + xdescribe: PendingSuiteFunction; + + /** + * Pending suite. + * + * - _Only available when invoked via the mocha CLI._ + */ + xcontext: PendingSuiteFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + it: TestFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + specify: TestFunction; + + /** + * Describes a pending test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + xit: PendingTestFunction; + + /** + * Describes a pending test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + xspecify: PendingTestFunction; + + /** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ + suiteSetup: HookFunction; + + /** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ + suiteTeardown: HookFunction; + + /** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ + setup: HookFunction; + + /** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ + teardown: HookFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + suite: SuiteFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + test: TestFunction; + + run: typeof run; + } + + /** + * Third-party declarations that want to add new entries to the `Reporter` union can + * contribute names here. + */ + interface ReporterContributions { + Base: never; + base: never; + Dot: never; + dot: never; + TAP: never; + tap: never; + JSON: never; + json: never; + HTML: never; + html: never; + List: never; + list: never; + Min: never; + min: never; + Spec: never; + spec: never; + Nyan: never; + nyan: never; + XUnit: never; + xunit: never; + Markdown: never; + markdown: never; + Progress: never; + progress: never; + Landing: never; + landing: never; + JSONStream: never; + "json-stream": never; + } + + type Reporter = keyof ReporterContributions; + + /** + * Third-party declarations that want to add new entries to the `Interface` union can + * contribute names here. + */ + interface InterfaceContributions { + bdd: never; + tdd: never; + qunit: never; + exports: never; + } + + type Interface = keyof InterfaceContributions; + + // #region Deprecations + + /** @deprecated use `Mocha.Context` instead. */ + interface IContext { + test?: IRunnable; + runnable(): IRunnable | undefined; + /** @deprecated `.runnable()` returns `this` in `Mocha.Context`. */ + runnable(runnable: IRunnable): IContext; + timeout(): number; + /** @deprecated `.timeout()` returns `this` in `Mocha.Context`. */ + timeout(timeout: number): IContext; + /** @deprecated `.enableTimeouts()` has additional overloads in `Mocha.Context`. */ + /** @deprecated `.enableTimeouts()` returns `this` in `Mocha.Context`. */ + enableTimeouts(enableTimeouts: boolean): IContext; + /** @deprecated `.slow()` has additional overloads in `Mocha.Context`. */ + /** @deprecated `.slow()` returns `this` in `Mocha.Context`. */ + slow(slow: number): IContext; + /** @deprecated `.skip()` returns `never` in `Mocha.Context`. */ + skip(): IContext; + retries(): number; + /** @deprecated `.retries()` returns `this` in `Mocha.Context`. */ + retries(retries: number): IContext; + } + + /** @deprecated use `Mocha.Suite` instead. */ + interface ISuiteCallbackContext { + /** @deprecated `.timeout()` has additional overloads in `Mocha.Suite`. */ + timeout(ms: number | string): this; + /** @deprecated `.retries()` has additional overloads in `Mocha.Suite`. */ + retries(n: number): this; + /** @deprecated `.slow()` has additional overloads in `Mocha.Suite`. */ + slow(ms: number): this; + } + + /** @deprecated use `Mocha.Context` instead. */ + interface IHookCallbackContext { + /** @deprecated `.skip()` returns `never` in `Mocha.Context`. */ + skip(): this; + /** @deprecated `.timeout()` has additional overloads in `Mocha.Context`. */ + timeout(ms: number | string): this; + [index: string]: any; + } + + /** @deprecated use `Mocha.Context` instead. */ + interface ITestCallbackContext { + /** @deprecated `.skip()` returns `never` in `Mocha.Context`. */ + skip(): this; + /** @deprecated `.timeout()` has additional overloads in `Mocha.Context`. */ + timeout(ms: number | string): this; + /** @deprecated `.retries()` has additional overloads in `Mocha.Context`. */ + retries(n: number): this; + /** @deprecated `.slow()` has additional overloads in `Mocha.Context`. */ + slow(ms: number): this; + [index: string]: any; + } + + /** Partial interface for Mocha's `Runnable` class. */ + /** @deprecated use `Mocha.Runnable` instead. */ + interface IRunnable extends NodeJS.EventEmitter { + title: string; + /** @deprecated `.fn` has type `Func | AsyncFunc` in `Mocha.Runnable`. */ + fn: Function | undefined; + async: boolean; + sync: boolean; + timedOut: boolean; + /** @deprecated `.timeout()` has additional overloads in `Mocha.Runnable`. */ + timeout(n: number | string): this; + duration?: number; + } + + /** Partial interface for Mocha's `Suite` class. */ + /** @deprecated use `Mocha.Suite` instead. */ + interface ISuite { + /** @deprecated `.ctx` has type `Mocha.Context` in `Mocha.Suite`. */ + ctx: IContext; + /** @deprecated `.parent` has type `Mocha.Suite | undefined` in `Mocha.Suite`. */ + parent: ISuite | undefined; + root: boolean; + title: string; + /** @deprecated `.suites` has type `Mocha.Suite[]` in `Mocha.Suite`. */ + suites: ISuite[]; + /** @deprecated `.tests` has type `Mocha.Test[]` in `Mocha.Suite`. */ + tests: ITest[]; + + bail(): boolean; + /** @deprecated `.bail()` returns `this` in `Mocha.Suite`. */ + bail(bail: boolean): ISuite; + fullTitle(): string; + retries(): number; + /** @deprecated `.retries()` returns `this` in `Mocha.Suite`. */ + retries(retries: number): ISuite; + slow(): number; + /** @deprecated `.slow()` returns `this` in `Mocha.Suite`. */ + slow(slow: number): ISuite; + timeout(): number; + /** @deprecated `.timeout()` returns `this` in `Mocha.Suite`. */ + timeout(timeout: number): ISuite; + } + + /** Partial interface for Mocha's `Test` class. */ + /** @deprecated use `Mocha.Test` instead. */ + interface ITest extends IRunnable { + body?: string; + file?: string; + /** @deprecated `.parent` has type `Mocha.Suite | undefined` in `Mocha.Test`. */ + parent?: ISuite; + pending: boolean; + state?: 'failed' | 'passed'; + type: 'test'; + fullTitle(): string; + } + + /** @deprecated use `Mocha.Hook` instead. */ + interface IHook extends IRunnable { + /** @deprecated `.ctx` has type `Mocha.Context` in `Mocha.Runnable`. */ + ctx?: IContext; + /** @deprecated `.parent` has type `Mocha.Suite` in `Mocha.Runnable`. */ + parent?: ISuite; + type: 'hook'; + /** @deprecated `.error()` has additional overloads in `Mocha.Hook`. */ + error(err: Error): void; + } + + /** @deprecated use `Mocha.Context` instead. */ + interface IBeforeAndAfterContext extends IHookCallbackContext { + /** @deprecated `.currentTest` has type `Mocha.Test` in `Mocha.Context`. */ + currentTest?: ITest; + } + + /** @deprecated use `Mocha.Stats` instead. */ + type IStats = Stats; + + /** Partial interface for Mocha's `Runner` class. */ + /** @deprecated use `Mocha.Runner` instead. */ + interface IRunner extends NodeJS.EventEmitter { + asyncOnly?: boolean; + stats?: IStats; + started: boolean; + /** @deprecated `.suite` has type `Mocha.Suite` in `Mocha.Runner`. */ + suite: ISuite; + total: number; + failures: number; + forbidOnly?: boolean; + forbidPending?: boolean; + fullStackTrace?: boolean; + ignoreLeaks?: boolean; + grep(re: RegExp, invert: boolean): this; + /** @deprecated Parameter `suite` has type `Mocha.Suite` in `Mocha.Runner`. */ + grepTotal(suite: ISuite): number; + /** @deprecated `.globals()` has different overloads in `Mocha.Runner`. */ + globals(arr: ReadonlyArray): this | string[]; + abort(): this; + run(fn?: (failures: number) => void): this; + } + + /** @deprecated use `Mocha.SuiteFunction` instead. */ + interface IContextDefinition { + /** @deprecated use `Mocha.SuiteFunction` instead. */ + (description: string, callback: (this: ISuiteCallbackContext) => void): ISuite; + /** @deprecated use `Mocha.SuiteFunction` instead. */ + only(description: string, callback: (this: ISuiteCallbackContext) => void): ISuite; + /** @deprecated use `Mocha.SuiteFunction` instead. */ + skip(description: string, callback: (this: ISuiteCallbackContext) => void): void; + } + + /** @deprecated use `Mocha.TestFunction` instead. */ + interface ITestDefinition { + /** @deprecated use `Mocha.TestFunction` instead. */ + /** @deprecated `Mocha.TestFunction` does not allow mixing `done` with a return type of `PromiseLike`. */ + (expectation: string, callback?: (this: ITestCallbackContext, done: MochaDone) => PromiseLike | void): ITest; + /** @deprecated use `Mocha.TestFunction` instead. */ + /** @deprecated `Mocha.TestFunction#only` does not allow mixing `done` with a return type of `PromiseLike`. */ + only(expectation: string, callback?: (this: ITestCallbackContext, done: MochaDone) => PromiseLike | void): ITest; + /** @deprecated use `Mocha.TestFunction` instead. */ + /** @deprecated `Mocha.TestFunction#skip` does not allow mixing `done` with a return type of `PromiseLike`. */ + skip(expectation: string, callback?: (this: ITestCallbackContext, done: MochaDone) => PromiseLike | void): void; + } + + // #endregion +} + +declare global { + // #region Test interface augmentations + + /** + * Triggers root suite execution. + * + * - _Only available if flag --delay is passed into Mocha._ + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#runWithSuite + */ + function run(): void; + + /** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ + var before: Mocha.HookFunction; + + /** + * Execute before running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#before + */ + var suiteSetup: Mocha.HookFunction; + + /** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ + var after: Mocha.HookFunction; + + /** + * Execute after running tests. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#after + */ + var suiteTeardown: Mocha.HookFunction; + + /** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ + var beforeEach: Mocha.HookFunction; + + /** + * Execute before each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#beforeEach + */ + var setup: Mocha.HookFunction; + + /** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ + var afterEach: Mocha.HookFunction; + + /** + * Execute after each test case. + * + * - _Only available when invoked via the mocha CLI._ + * + * @see https://mochajs.org/api/global.html#afterEach + */ + var teardown: Mocha.HookFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + var describe: Mocha.SuiteFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + var context: Mocha.SuiteFunction; + + /** + * Describe a "suite" containing nested suites and tests. + * + * - _Only available when invoked via the mocha CLI._ + */ + var suite: Mocha.SuiteFunction; + + /** + * Pending suite. + * + * - _Only available when invoked via the mocha CLI._ + */ + var xdescribe: Mocha.PendingSuiteFunction; + + /** + * Pending suite. + * + * - _Only available when invoked via the mocha CLI._ + */ + var xcontext: Mocha.PendingSuiteFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + var it: Mocha.TestFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + var specify: Mocha.TestFunction; + + /** + * Describes a test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + var test: Mocha.TestFunction; + + /** + * Describes a pending test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + var xit: Mocha.PendingTestFunction; + + /** + * Describes a pending test case. + * + * - _Only available when invoked via the mocha CLI._ + */ + var xspecify: Mocha.PendingTestFunction; + + // #endregion Test interface augmentations + + // #region Reporter augmentations + + // Forward declaration for `HTMLLIElement` from lib.dom.d.ts. + // Required by Mocha.reporters.HTML. + // NOTE: Mocha *must not* have a direct dependency on DOM types. + // tslint:disable-next-line no-empty-interface + interface HTMLLIElement { } + + // Augments the DOM `Window` object when lib.dom.d.ts is loaded. + // tslint:disable-next-line no-empty-interface + interface Window extends Mocha.MochaGlobals { } + + namespace NodeJS { + // Forward declaration for `NodeJS.EventEmitter` from node.d.ts. + // Required by Mocha.Runnable, Mocha.Runner, and Mocha.Suite. + // NOTE: Mocha *must not* have a direct dependency on @types/node. + // tslint:disable-next-line no-empty-interface + interface EventEmitter { } + + // Augments NodeJS's `global` object when node.d.ts is loaded + // tslint:disable-next-line no-empty-interface + interface Global extends Mocha.MochaGlobals { } + } + + // #endregion Reporter augmentations + + // #region Browser augmentations + + /** + * Mocha global. + * + * - _Only supported in the browser._ + */ + const mocha: BrowserMocha; + + interface BrowserMocha extends Mocha { + /** + * Function to allow assertion libraries to throw errors directly into mocha. + * This is useful when running tests in a browser because window.onerror will + * only receive the 'message' attribute of the Error. + * + * - _Only supported in the browser._ + */ + throwError(err: any): never; + + /** + * Setup mocha with the given settings options. + * + * - _Only supported in the browser._ + */ + setup(opts?: Mocha.Interface | MochaSetupOptions): this; + } + + /** + * Options to pass to `mocha.setup` in the browser. + */ + interface MochaSetupOptions extends Mocha.MochaOptions { + // TODO: This does not seem to be supported according to the source. Should it be removed? + require?: string[]; + fullTrace?: boolean; + } + + // #endregion Browser augmentations + + // #region Deprecations + + /** @deprecated use `Mocha.DoneCallback` instead. */ + type MochaDone = Mocha.Done; + + /** @deprecated use `Mocha.ReporterConstructor` instead. */ + type ReporterConstructor = Mocha.ReporterConstructor; + + // #endregion Deprecations +} diff --git a/scripts/types/mocha/lib/interfaces/common.d.ts b/scripts/types/mocha/lib/interfaces/common.d.ts new file mode 100644 index 0000000000000..1deb9712e6bf9 --- /dev/null +++ b/scripts/types/mocha/lib/interfaces/common.d.ts @@ -0,0 +1,109 @@ +import Mocha = require("../../"); + +export = common; + +declare function common(suites: Mocha.Suite[], context: Mocha.MochaGlobals, mocha: Mocha): common.CommonFunctions; + +declare namespace common { + export interface CommonFunctions { + /** + * This is only present if flag --delay is passed into Mocha. It triggers + * root suite execution. + */ + runWithSuite(suite: Mocha.Suite): () => void; + + /** + * Execute before running tests. + */ + before(fn?: Mocha.Func | Mocha.AsyncFunc): void; + + /** + * Execute before running tests. + */ + before(name: string, fn?: Mocha.Func | Mocha.AsyncFunc): void; + + /** + * Execute after running tests. + */ + after(fn?: Mocha.Func | Mocha.AsyncFunc): void; + + /** + * Execute after running tests. + */ + after(name: string, fn?: Mocha.Func | Mocha.AsyncFunc): void; + + /** + * Execute before each test case. + */ + beforeEach(fn?: Mocha.Func | Mocha.AsyncFunc): void; + + /** + * Execute before each test case. + */ + beforeEach(name: string, fn?: Mocha.Func | Mocha.AsyncFunc): void; + + /** + * Execute after each test case. + */ + afterEach(fn?: Mocha.Func | Mocha.AsyncFunc): void; + + /** + * Execute after each test case. + */ + afterEach(name: string, fn?: Mocha.Func | Mocha.AsyncFunc): void; + + suite: SuiteFunctions; + test: TestFunctions; + } + + export interface CreateOptions { + /** Title of suite */ + title: string; + + /** Suite function */ + fn?: (this: Mocha.Suite) => void; + + /** Is suite pending? */ + pending?: boolean; + + /** Filepath where this Suite resides */ + file?: string; + + /** Is suite exclusive? */ + isOnly?: boolean; + } + + export interface SuiteFunctions { + /** + * Create an exclusive Suite; convenience function + */ + only(opts: CreateOptions): Mocha.Suite; + + /** + * Create a Suite, but skip it; convenience function + */ + skip(opts: CreateOptions): Mocha.Suite; + + /** + * Creates a suite. + */ + create(opts: CreateOptions): Mocha.Suite; + } + + export interface TestFunctions { + /** + * Exclusive test-case. + */ + only(mocha: Mocha, test: Mocha.Test): Mocha.Test; + + /** + * Pending test case. + */ + skip(title: string): void; + + /** + * Number of retry attempts + */ + retries(n: number): void; + } +} diff --git a/scripts/types/mocha/lib/ms.d.ts b/scripts/types/mocha/lib/ms.d.ts new file mode 100644 index 0000000000000..890da5b8c5ab5 --- /dev/null +++ b/scripts/types/mocha/lib/ms.d.ts @@ -0,0 +1,17 @@ +export = milliseconds; + +/** + * Parse the given `str` and return milliseconds. + * + * @see {@link https://mochajs.org/api/module-milliseconds.html} + * @see {@link https://mochajs.org/api/module-milliseconds.html#~parse} + */ +declare function milliseconds(val: string): number; + +/** + * Format for `ms`. + * + * @see {@link https://mochajs.org/api/module-milliseconds.html} + * @see {@link https://mochajs.org/api/module-milliseconds.html#~format} + */ +declare function milliseconds(val: number): string; diff --git a/scripts/types/mocha/package.json b/scripts/types/mocha/package.json new file mode 100644 index 0000000000000..e45485e803b4c --- /dev/null +++ b/scripts/types/mocha/package.json @@ -0,0 +1,5 @@ +{ + "name": "@types/mocha", + "private": true, + "version": "5.2.1" +} diff --git a/src/testRunner/parallel/host.ts b/src/testRunner/parallel/host.ts index 99e9b541b8539..87d193656c3cb 100644 --- a/src/testRunner/parallel/host.ts +++ b/src/testRunner/parallel/host.ts @@ -1,613 +1,606 @@ // tslint:disable no-unnecessary-type-assertion (TODO: tslint can't find node types) -if (typeof describe === "undefined") { - (global as any).describe = undefined; // If launched without mocha for parallel mode, we still need a global describe visible to satisfy the parsing of the unit tests - (global as any).it = undefined; -} namespace Harness.Parallel.Host { + export function start() { + // tslint:disable-next-line:variable-name + const Mocha = require("mocha") as typeof import("mocha"); + const Base = Mocha.reporters.Base; + const color = Base.color; + const cursor = Base.cursor; + const ms = require("mocha/lib/ms") as typeof import("mocha/lib/ms"); + const readline = require("readline") as typeof import("readline"); + const os = require("os") as typeof import("os"); + const tty = require("tty") as typeof import("tty"); + const isatty = tty.isatty(1) && tty.isatty(2); + const path = require("path") as typeof import("path"); + const { fork } = require("child_process") as typeof import("child_process"); + const { statSync } = require("fs") as typeof import("fs"); - interface ChildProcessPartial { - send(message: ParallelHostMessage, callback?: (error: Error) => void): boolean; - on(event: "error", listener: (err: Error) => void): this; - on(event: "exit", listener: (code: number, signal: string) => void): this; - on(event: "message", listener: (message: ParallelClientMessage) => void): this; - kill(signal?: string): void; - currentTasks?: {file: string}[]; // Custom monkeypatch onto child process handle - accumulatedOutput: string; // Custom monkeypatch with process output - stderr: PartialStream; - stdout: PartialStream; - } + const perfData = readSavedPerfData(configOption); + const newTasks: Task[] = []; + let tasks: Task[] = []; + let unknownValue: string | undefined; + let totalCost = 0; - interface PartialStream { - on(event: "data", listener: (chunk: Buffer) => void): this; - } + class RemoteSuite extends Mocha.Suite { + suites: RemoteSuite[]; + suiteMap = ts.createMap(); + tests: RemoteTest[]; + constructor(title: string) { + super(title); + this.pending = false; + this.delayed = false; + } + addSuite(suite: RemoteSuite) { + super.addSuite(suite); + this.suiteMap.set(suite.title, suite); + return this; + } + addTest(test: RemoteTest) { + return super.addTest(test); + } + } - interface ProgressBarsOptions { - open: string; - close: string; - complete: string; - incomplete: string; - width: number; - noColors: boolean; - } - interface ProgressBar { - lastN?: number; - title?: string; - progressColor?: string; - text?: string; - } + class RemoteTest extends Mocha.Test { + info: ErrorInfo | TestInfo; + constructor(info: ErrorInfo | TestInfo) { + super(info.name[info.name.length - 1]); + this.info = info; + this.state = "error" in info ? "failed" : "passed"; + this.pending = false; + } + } - const perfdataFileNameFragment = ".parallelperf"; - function perfdataFileName(target?: string) { - return `${perfdataFileNameFragment}${target ? `.${target}` : ""}.json`; - } - function readSavedPerfData(target?: string): {[testHash: string]: number} | undefined { - const perfDataContents = IO.readFile(perfdataFileName(target)); - if (perfDataContents) { - return JSON.parse(perfDataContents); + interface Worker { + process: import("child_process").ChildProcess; + accumulatedOutput: string; + currentTasks?: {file: string}[]; + timer?: any; } - return undefined; - } - function hashName(runner: TestRunnerKind | "unittest", test: string) { - return `tsrunner-${runner}://${test}`; - } + interface ProgressBarsOptions { + open: string; + close: string; + complete: string; + incomplete: string; + width: number; + noColors: boolean; + } - let tasks: { runner: TestRunnerKind | "unittest", file: string, size: number }[] = []; - const newTasks: { runner: TestRunnerKind | "unittest", file: string, size: number }[] = []; - let unknownValue: string | undefined; - export function start() { - const perfData = readSavedPerfData(configOption); - let totalCost = 0; - if (runUnitTests) { - (global as any).describe = (suiteName: string) => { - // Note, sub-suites are not indexed (we assume such granularity is not required) - let size = 0; - if (perfData) { - size = perfData[hashName("unittest", suiteName)]; - if (size === undefined) { - newTasks.push({ runner: "unittest", file: suiteName, size: 0 }); - unknownValue = suiteName; - return; + interface ProgressBar { + lastN?: number; + title?: string; + progressColor?: string; + text?: string; + } + + class ProgressBars { + public readonly _options: Readonly; + private _enabled: boolean; + private _lineCount: number; + private _progressBars: ProgressBar[]; + constructor(options?: Partial) { + if (!options) options = {}; + const open = options.open || "["; + const close = options.close || "]"; + const complete = options.complete || "▬"; + const incomplete = options.incomplete || Base.symbols.dot; + const maxWidth = Base.window.width - open.length - close.length - 34; + const width = minMax(options.width || maxWidth, 10, maxWidth); + this._options = { + open, + complete, + incomplete, + close, + width, + noColors: options.noColors || false + }; + + this._progressBars = []; + this._lineCount = 0; + this._enabled = false; + } + enable() { + if (!this._enabled) { + process.stdout.write(os.EOL); + this._enabled = true; + } + } + disable() { + if (this._enabled) { + process.stdout.write(os.EOL); + this._enabled = false; + } + } + update(index: number, percentComplete: number, color: string, title: string | undefined, titleColor?: string) { + percentComplete = minMax(percentComplete, 0, 1); + + const progressBar = this._progressBars[index] || (this._progressBars[index] = { }); + const width = this._options.width; + const n = Math.floor(width * percentComplete); + const i = width - n; + if (n === progressBar.lastN && title === progressBar.title && color === progressBar.progressColor) { + return; + } + + progressBar.lastN = n; + progressBar.title = title; + progressBar.progressColor = color; + + let progress = " "; + progress += this._color("progress", this._options.open); + progress += this._color(color, fill(this._options.complete, n)); + progress += this._color("progress", fill(this._options.incomplete, i)); + progress += this._color("progress", this._options.close); + + if (title) { + progress += this._color(titleColor || "progress", " " + title); + } + + if (progressBar.text !== progress) { + progressBar.text = progress; + this._render(index); + } + } + private _render(index: number) { + if (!this._enabled || !isatty) { + return; + } + + cursor.hide(); + readline.moveCursor(process.stdout, -process.stdout.columns!, -this._lineCount); + let lineCount = 0; + const numProgressBars = this._progressBars.length; + for (let i = 0; i < numProgressBars; i++) { + if (i === index) { + readline.clearLine(process.stdout, 1); + process.stdout.write(this._progressBars[i].text + os.EOL); + } + else { + readline.moveCursor(process.stdout, -process.stdout.columns!, +1); } + + lineCount++; } - tasks.push({ runner: "unittest", file: suiteName, size }); - totalCost += size; - }; + + this._lineCount = lineCount; + cursor.show(); + } + private _color(type: string, text: string) { + return type && !this._options.noColors ? color(type, text) : text; + } } - else { - (global as any).describe = ts.noop; + + const perfdataFileNameFragment = ".parallelperf"; + + function perfdataFileName(target?: string) { + return `${perfdataFileNameFragment}${target ? `.${target}` : ""}.json`; } - setTimeout(() => startDelayed(perfData, totalCost), 0); // Do real startup on next tick, so all unit tests have been collected - } + function readSavedPerfData(target?: string): {[testHash: string]: number} | undefined { + const perfDataContents = IO.readFile(perfdataFileName(target)); + if (perfDataContents) { + return JSON.parse(perfDataContents); + } + return undefined; + } - function startDelayed(perfData: {[testHash: string]: number} | undefined, totalCost: number) { - initializeProgressBarsDependencies(); - console.log(`Discovered ${tasks.length} unittest suites` + (newTasks.length ? ` and ${newTasks.length} new suites.` : ".")); - console.log("Discovering runner-based tests..."); - const discoverStart = +(new Date()); - const { statSync }: { statSync(path: string): { size: number }; } = require("fs"); - const path: { join: (...args: string[]) => string } = require("path"); - for (const runner of runners) { - for (const test of runner.enumerateTestFiles()) { - const file = typeof test === "string" ? test : test.file; - let size: number; - if (!perfData) { - try { - size = statSync(path.join(runner.workingDirectory, file)).size; - } - catch { - // May be a directory + function hashName(runner: TestRunnerKind | "unittest", test: string) { + return `tsrunner-${runner}://${test}`; + } + + function startDelayed(perfData: {[testHash: string]: number} | undefined, totalCost: number) { + console.log(`Discovered ${tasks.length} unittest suites` + (newTasks.length ? ` and ${newTasks.length} new suites.` : ".")); + console.log("Discovering runner-based tests..."); + const discoverStart = +(new Date()); + for (const runner of runners) { + for (const test of runner.enumerateTestFiles()) { + const file = typeof test === "string" ? test : test.file; + let size: number; + if (!perfData) { try { - size = IO.listFiles(path.join(runner.workingDirectory, file), /.*/g, { recursive: true }).reduce((acc, elem) => acc + statSync(elem).size, 0); + size = statSync(path.join(runner.workingDirectory, file)).size; } catch { - // Unknown test kind, just return 0 and let the historical analysis take over after one run - size = 0; + // May be a directory + try { + size = IO.listFiles(path.join(runner.workingDirectory, file), /.*/g, { recursive: true }).reduce((acc, elem) => acc + statSync(elem).size, 0); + } + catch { + // Unknown test kind, just return 0 and let the historical analysis take over after one run + size = 0; + } } } - } - else { - const hashedName = hashName(runner.kind(), file); - size = perfData[hashedName]; - if (size === undefined) { - size = 0; - unknownValue = hashedName; - newTasks.push({ runner: runner.kind(), file, size }); - continue; + else { + const hashedName = hashName(runner.kind(), file); + size = perfData[hashedName]; + if (size === undefined) { + size = 0; + unknownValue = hashedName; + newTasks.push({ runner: runner.kind(), file, size }); + continue; + } } + tasks.push({ runner: runner.kind(), file, size }); + totalCost += size; } - tasks.push({ runner: runner.kind(), file, size }); - totalCost += size; } - } - tasks.sort((a, b) => a.size - b.size); - tasks = tasks.concat(newTasks); - const batchCount = workerCount; - const packfraction = 0.9; - const chunkSize = 1000; // ~1KB or 1s for sending batches near the end of a test - const batchSize = (totalCost / workerCount) * packfraction; // Keep spare tests for unittest thread in reserve - console.log(`Discovered ${tasks.length} test files in ${+(new Date()) - discoverStart}ms.`); - console.log(`Starting to run tests using ${workerCount} threads...`); - const { fork }: { fork(modulePath: string, args?: string[], options?: {}): ChildProcessPartial; } = require("child_process"); - - const totalFiles = tasks.length; - let passingFiles = 0; - let failingFiles = 0; - let errorResults: ErrorInfo[] = []; - let passingResults: { name: string[] }[] = []; - let totalPassing = 0; - const startTime = Date.now(); - - const progressBars = new ProgressBars({ noColors }); - const progressUpdateInterval = 1 / progressBars._options.width; - let nextProgress = progressUpdateInterval; - - const newPerfData: {[testHash: string]: number} = {}; - - const workers: ChildProcessPartial[] = []; - const defaultTimeout = globalTimeout !== undefined - ? globalTimeout - : mocha && mocha.suite && mocha.suite._timeout - ? mocha.suite._timeout - : 20000; // 20 seconds - let closedWorkers = 0; - for (let i = 0; i < workerCount; i++) { - // TODO: Just send the config over the IPC channel or in the command line arguments - const config: TestConfig = { light: lightMode, listenForWork: true, runUnitTests, stackTraceLimit }; - const configPath = ts.combinePaths(taskConfigsFolder, `task-config${i}.json`); - IO.writeFile(configPath, JSON.stringify(config)); - const child = fork(__filename, [`--config="${configPath}"`], { stdio: ["pipe", "pipe", "pipe", "ipc"] }); - child.accumulatedOutput = ""; - const appendOutput = (d: Buffer) => { - child.accumulatedOutput += d.toString(); - }; - child.stderr.on("data", appendOutput); - child.stdout.on("data", appendOutput); - let currentTimeout = defaultTimeout; - const killChild = () => { - child.kill(); - console.error(`Worker exceeded ${currentTimeout}ms timeout ${child.currentTasks && child.currentTasks.length ? `while running test '${child.currentTasks[0].file}'.` : `during test setup.`}`); - return process.exit(2); - }; - let timer = setTimeout(killChild, currentTimeout); - const timeoutStack: number[] = []; - child.on("error", err => { - console.error("Unexpected error in child process:"); - console.error(err); - return process.exit(2); - }); - child.on("exit", (code, _signal) => { - clearTimeout(timer); - if (code !== 0) { - console.error(`Test worker process exited with nonzero exit code! Output: -${child.accumulatedOutput}`); + tasks.sort((a, b) => a.size - b.size); + tasks = tasks.concat(newTasks); + const batchCount = workerCount; + const packfraction = 0.9; + const chunkSize = 1000; // ~1KB or 1s for sending batches near the end of a test + const batchSize = (totalCost / workerCount) * packfraction; // Keep spare tests for unittest thread in reserve + console.log(`Discovered ${tasks.length} test files in ${+(new Date()) - discoverStart}ms.`); + console.log(`Starting to run tests using ${workerCount} threads...`); + + const totalFiles = tasks.length; + let passingFiles = 0; + let failingFiles = 0; + let errorResults: ErrorInfo[] = []; + let passingResults: { name: string[] }[] = []; + let totalPassing = 0; + const startDate = new Date(); + + const progressBars = new ProgressBars({ noColors }); + const progressUpdateInterval = 1 / progressBars._options.width; + let nextProgress = progressUpdateInterval; + + const newPerfData: {[testHash: string]: number} = {}; + + const workers: Worker[] = []; + let closedWorkers = 0; + for (let i = 0; i < workerCount; i++) { + // TODO: Just send the config over the IPC channel or in the command line arguments + const config: TestConfig = { light: lightMode, listenForWork: true, runUnitTests, stackTraceLimit, timeout: globalTimeout }; + const configPath = ts.combinePaths(taskConfigsFolder, `task-config${i}.json`); + IO.writeFile(configPath, JSON.stringify(config)); + const worker: Worker = { + process: fork(__filename, [`--config="${configPath}"`], { stdio: ["pipe", "pipe", "pipe", "ipc"] }), + accumulatedOutput: "", + currentTasks: undefined, + timer: undefined + }; + const appendOutput = (d: Buffer) => { + worker.accumulatedOutput += d.toString(); + console.log(`[Worker ${i}]`, d.toString()); + }; + worker.process.stderr.on("data", appendOutput); + worker.process.stdout.on("data", appendOutput); + const killChild = (timeout: TaskTimeout) => { + worker.process.kill(); + console.error(`Worker exceeded ${timeout.duration}ms timeout ${worker.currentTasks && worker.currentTasks.length ? `while running test '${worker.currentTasks[0].file}'.` : `during test setup.`}`); return process.exit(2); - } - }); - child.on("message", (data: ParallelClientMessage) => { - switch (data.type) { - case "error": { - console.error(`Test worker encounted unexpected error${data.payload.name ? ` during the execution of test ${data.payload.name}` : ""} and was forced to close: - Message: ${data.payload.error} - Stack: ${data.payload.stack}`); + }; + worker.process.on("error", err => { + console.error("Unexpected error in child process:"); + console.error(err); + return process.exit(2); + }); + worker.process.on("exit", (code, _signal) => { + if (code !== 0) { + console.error(`Test worker process exited with nonzero exit code! Output: + ${worker.accumulatedOutput}`); return process.exit(2); } - case "timeout": { - clearTimeout(timer); - if (data.payload.duration === "reset") { - currentTimeout = timeoutStack.pop() || defaultTimeout; - } - else { - timeoutStack.push(currentTimeout); - currentTimeout = data.payload.duration; - } - timer = setTimeout(killChild, currentTimeout); // Reset timeout on timeout update, for when a timeout changes while a suite is executing - break; - } - case "progress": - case "result": { - clearTimeout(timer); - timer = setTimeout(killChild, currentTimeout); - if (child.currentTasks) { - child.currentTasks.shift(); - } - totalPassing += data.payload.passing; - if (data.payload.errors.length) { - errorResults = errorResults.concat(data.payload.errors); - passingResults = passingResults.concat(data.payload.passes); - failingFiles++; - } - else { - passingResults = passingResults.concat(data.payload.passes); - passingFiles++; + }); + worker.process.on("message", (data: ParallelClientMessage) => { + switch (data.type) { + case "error": { + console.error(`Test worker encounted unexpected error${data.payload.name ? ` during the execution of test ${data.payload.name}` : ""} and was forced to close: + Message: ${data.payload.error} + Stack: ${data.payload.stack}`); + return process.exit(2); } - newPerfData[hashName(data.payload.runner, data.payload.file)] = data.payload.duration; - - const progress = (failingFiles + passingFiles) / totalFiles; - if (progress >= nextProgress) { - while (nextProgress < progress) { - nextProgress += progressUpdateInterval; + case "timeout": { + if (worker.timer) clearTimeout(worker.timer); + if (data.payload.duration === "reset") { + worker.timer = undefined; } - updateProgress(progress, errorResults.length ? `${errorResults.length} failing` : `${totalPassing} passing`, errorResults.length ? "fail" : undefined); - } - - if (data.type === "result") { - if (tasks.length === 0) { - // No more tasks to distribute - child.send({ type: "close" }); - closedWorkers++; - clearTimeout(timer); - if (closedWorkers === workerCount) { - outputFinalResult(); - } - return; + else { + worker.timer = setTimeout(killChild, data.payload.duration, data.payload); } - // Send tasks in blocks if the tasks are small - const taskList = [tasks.pop()!]; - while (tasks.length && taskList.reduce((p, c) => p + c.size, 0) < chunkSize) { - taskList.push(tasks.pop()!); + break; + } + case "progress": + case "result": { + if (worker.currentTasks) { + worker.currentTasks.shift(); } - child.currentTasks = taskList; - if (taskList.length === 1) { - child.send({ type: "test", payload: taskList[0] } as ParallelHostMessage); // TODO: GH#18217 + totalPassing += data.payload.passing; + if (data.payload.errors.length) { + errorResults = errorResults.concat(data.payload.errors); + passingResults = passingResults.concat(data.payload.passes); + failingFiles++; } else { - child.send({ type: "batch", payload: taskList } as ParallelHostMessage); // TODO: GH#18217 + passingResults = passingResults.concat(data.payload.passes); + passingFiles++; + } + newPerfData[hashName(data.payload.task.runner, data.payload.task.file)] = data.payload.duration; + + const progress = (failingFiles + passingFiles) / totalFiles; + if (progress >= nextProgress) { + while (nextProgress < progress) { + nextProgress += progressUpdateInterval; + } + updateProgress(progress, errorResults.length ? `${errorResults.length} failing` : `${totalPassing} passing`, errorResults.length ? "fail" : undefined); + } + + if (data.type === "result") { + if (tasks.length === 0) { + // No more tasks to distribute + worker.process.send({ type: "close" }); + closedWorkers++; + if (closedWorkers === workerCount) { + outputFinalResult(); + } + return; + } + // Send tasks in blocks if the tasks are small + const taskList = [tasks.pop()!]; + while (tasks.length && taskList.reduce((p, c) => p + c.size, 0) < chunkSize) { + taskList.push(tasks.pop()!); + } + worker.currentTasks = taskList; + if (taskList.length === 1) { + worker.process.send({ type: "test", payload: taskList[0] } as ParallelHostMessage); // TODO: GH#18217 + } + else { + worker.process.send({ type: "batch", payload: taskList } as ParallelHostMessage); // TODO: GH#18217 + } } } } - } - }); - workers.push(child); - } + }); + workers.push(worker); + } - // It's only really worth doing an initial batching if there are a ton of files to go through (and they have estimates) - if (totalFiles > 1000 && batchSize > 0) { - console.log("Batching initial test lists..."); - const batches: { runner: TestRunnerKind | "unittest", file: string, size: number }[][] = new Array(batchCount); - const doneBatching = new Array(batchCount); - let scheduledTotal = 0; - batcher: while (true) { - for (let i = 0; i < batchCount; i++) { - if (tasks.length <= workerCount) { // Keep a small reserve even in the suboptimally packed case - console.log(`Suboptimal packing detected: no tests remain to be stolen. Reduce packing fraction from ${packfraction} to fix.`); - break batcher; - } - if (doneBatching[i]) { - continue; - } - if (!batches[i]) { - batches[i] = []; + // It's only really worth doing an initial batching if there are a ton of files to go through (and they have estimates) + if (totalFiles > 1000 && batchSize > 0) { + console.log("Batching initial test lists..."); + const batches: { runner: TestRunnerKind | "unittest", file: string, size: number }[][] = new Array(batchCount); + const doneBatching = new Array(batchCount); + let scheduledTotal = 0; + batcher: while (true) { + for (let i = 0; i < batchCount; i++) { + if (tasks.length <= workerCount) { // Keep a small reserve even in the suboptimally packed case + console.log(`Suboptimal packing detected: no tests remain to be stolen. Reduce packing fraction from ${packfraction} to fix.`); + break batcher; + } + if (doneBatching[i]) { + continue; + } + if (!batches[i]) { + batches[i] = []; + } + const total = batches[i].reduce((p, c) => p + c.size, 0); + if (total >= batchSize) { + doneBatching[i] = true; + continue; + } + const task = tasks.pop()!; + batches[i].push(task); + scheduledTotal += task.size; } - const total = batches[i].reduce((p, c) => p + c.size, 0); - if (total >= batchSize) { - doneBatching[i] = true; - continue; + for (let j = 0; j < batchCount; j++) { + if (!doneBatching[j]) { + continue batcher; + } } - const task = tasks.pop()!; - batches[i].push(task); - scheduledTotal += task.size; + break; + } + const prefix = `Batched into ${batchCount} groups`; + if (unknownValue) { + console.log(`${prefix}. Unprofiled tests including ${unknownValue} will be run first.`); + } + else { + console.log(`${prefix} with approximate total ${perfData ? "time" : "file sizes"} of ${perfData ? ms(batchSize) : `${Math.floor(batchSize)} bytes`} in each group. (${(scheduledTotal / totalCost * 100).toFixed(1)}% of total tests batched)`); } - for (let j = 0; j < batchCount; j++) { - if (!doneBatching[j]) { - continue batcher; + for (const worker of workers) { + const payload = batches.pop(); + if (payload) { + worker.currentTasks = payload; + worker.process.send({ type: "batch", payload }); + } + else { // Out of batches, send off just one test + const payload = tasks.pop()!; + ts.Debug.assert(!!payload); // The reserve kept above should ensure there is always an initial task available, even in suboptimal scenarios + worker.currentTasks = [payload]; + worker.process.send({ type: "test", payload }); } } - break; - } - const prefix = `Batched into ${batchCount} groups`; - if (unknownValue) { - console.log(`${prefix}. Unprofiled tests including ${unknownValue} will be run first.`); } else { - console.log(`${prefix} with approximate total ${perfData ? "time" : "file sizes"} of ${perfData ? ms(batchSize) : `${Math.floor(batchSize)} bytes`} in each group. (${(scheduledTotal / totalCost * 100).toFixed(1)}% of total tests batched)`); - } - for (const worker of workers) { - const payload = batches.pop(); - if (payload) { - worker.currentTasks = payload; - worker.send({ type: "batch", payload }); - } - else { // Out of batches, send off just one test - const payload = tasks.pop()!; - ts.Debug.assert(!!payload); // The reserve kept above should ensure there is always an initial task available, even in suboptimal scenarios - worker.currentTasks = [payload]; - worker.send({ type: "test", payload }); + for (let i = 0; i < workerCount; i++) { + const task = tasks.pop()!; + workers[i].currentTasks = [task]; + workers[i].process.send({ type: "test", payload: task }); } } - } - else { - for (let i = 0; i < workerCount; i++) { - const task = tasks.pop()!; - workers[i].currentTasks = [task]; - workers[i].send({ type: "test", payload: task }); - } - } - - progressBars.enable(); - updateProgress(0); - let duration: number; - function completeBar() { - const isPartitionFail = failingFiles !== 0; - const summaryColor = isPartitionFail ? "fail" : "green"; - const summarySymbol = isPartitionFail ? base.symbols.err : base.symbols.ok; + progressBars.enable(); + updateProgress(0); + let duration: number; + let endDate: Date; - const summaryTests = (isPartitionFail ? totalPassing + "/" + (errorResults.length + totalPassing) : totalPassing) + " passing"; - const summaryDuration = "(" + ms(duration) + ")"; - const savedUseColors = base.useColors; - base.useColors = !noColors; + function completeBar() { + const isPartitionFail = failingFiles !== 0; + const summaryColor = isPartitionFail ? "fail" : "green"; + const summarySymbol = isPartitionFail ? Base.symbols.err : Base.symbols.ok; - const summary = color(summaryColor, summarySymbol + " " + summaryTests) + " " + color("light", summaryDuration); - base.useColors = savedUseColors; + const summaryTests = (isPartitionFail ? totalPassing + "/" + (errorResults.length + totalPassing) : totalPassing) + " passing"; + const summaryDuration = "(" + ms(duration) + ")"; + const savedUseColors = Base.useColors; + Base.useColors = !noColors; - updateProgress(1, summary); - } + const summary = color(summaryColor, summarySymbol + " " + summaryTests) + " " + color("light", summaryDuration); + Base.useColors = savedUseColors; - function updateProgress(percentComplete: number, title?: string, titleColor?: string) { - let progressColor = "pending"; - if (failingFiles) { - progressColor = "fail"; + updateProgress(1, summary); } - progressBars.update( - 0, - percentComplete, - progressColor, - title, - titleColor - ); - } - - function outputFinalResult() { - duration = Date.now() - startTime; - completeBar(); - progressBars.disable(); - - const reporter = new base(); - const stats = reporter.stats; - const failures = reporter.failures; - stats.passes = totalPassing; - stats.failures = errorResults.length; - stats.tests = totalPassing + errorResults.length; - stats.duration = duration; - for (const failure of errorResults) { - failures.push(makeMochaTest(failure)); - } - if (noColors) { - const savedUseColors = base.useColors; - base.useColors = false; - reporter.epilogue(); - base.useColors = savedUseColors; - } - else { - reporter.epilogue(); - } + function updateProgress(percentComplete: number, title?: string, titleColor?: string) { + let progressColor = "pending"; + if (failingFiles) { + progressColor = "fail"; + } - IO.writeFile(perfdataFileName(configOption), JSON.stringify(newPerfData, null, 4)); // tslint:disable-line:no-null-keyword - - if (Utils.getExecutionEnvironment() !== Utils.ExecutionEnvironment.Browser && process.env.CI === "true") { - const xunitReport = new xunit({ on: ts.noop, once: ts.noop }, { reporterOptions: { output: "./TEST-results.xml" } }); - xunitReport.stats = reporter.stats; - xunitReport.failures = reporter.failures; - const rootAttrs: {[index: string]: any} = { - name: "Tests", - tests: stats.tests, - failures: stats.failures, - errors: stats.failures, - skipped: stats.tests - stats.failures - stats.passes, - timestamp: (new Date()).toUTCString(), - time: (stats.duration / 1000) || 0 - }; - xunitReport.write(`` + "\n"); - xunitReport.write(` `${k}="${escape("" + rootAttrs[k])}"`).join(" ")}>`); - [...failures, ...ts.map(passingResults, makeMochaTest)].forEach(t => { - xunitReport.test(t); - }); - xunitReport.write(""); - xunitReport.done(failures, (f: any[]) => { - process.exit(f.length); - }); - } - else { - process.exit(failures.length); + progressBars.update( + 0, + percentComplete, + progressColor, + title, + titleColor + ); } - } + function outputFinalResult() { + function patchStats(stats: Mocha.Stats) { + Object.defineProperties(stats, { + start: { + configurable: true, enumerable: true, + get() { return startDate; }, + set(_: Date) { /*do nothing*/ } + }, + end: { + configurable: true, enumerable: true, + get() { return endDate; }, + set(_: Date) { /*do nothing*/ } + }, + duration: { + configurable: true, enumerable: true, + get() { return duration; }, + set(_: number) { /*do nothing*/ } + } + }); + } - function makeMochaTest(test: ErrorInfo | TestInfo) { - return { - state: (test as ErrorInfo).error ? "failed" : "passed", - parent: { - fullTitle: () => { - return test.name.slice(0, test.name.length - 1).join(" "); + function rebuildSuite(failures: ErrorInfo[], passes: TestInfo[]) { + const root = new RemoteSuite(""); + for (const result of [...failures, ...passes] as (ErrorInfo | TestInfo)[]) { + getSuite(root, result.name.slice(0, -1)).addTest(new RemoteTest(result)); } - }, - title: test.name[test.name.length - 1], - fullTitle: () => { - return test.name.join(" "); - }, - titlePath: () => { - return test.name; - }, - isPending: () => false, - err: (test as ErrorInfo).error ? { - message: (test as ErrorInfo).error, - stack: (test as ErrorInfo).stack - } : undefined - }; - } + return root; + function getSuite(parent: RemoteSuite, titlePath: string[]): Mocha.Suite { + const title = titlePath[0]; + let suite = parent.suiteMap.get(title); + if (!suite) parent.addSuite(suite = new RemoteSuite(title)); + return titlePath.length === 1 ? suite : getSuite(suite, titlePath.slice(1)); + } + } - (global as any).describe = ts.noop as any; // Disable unit tests + function rebuildError(result: ErrorInfo) { + const error = new Error(result.error); + error.stack = result.stack; + return error; + } - return; - } + function replaySuite(runner: Mocha.Runner, suite: RemoteSuite) { + runner.emit("suite", suite); + for (const test of suite.tests) { + replayTest(runner, test); + } + for (const child of suite.suites) { + replaySuite(runner, child); + } + runner.emit("suite end", suite); + } - let mocha: any; - let base: any; - let xunit: any; - let color: any; - let cursor: any; - let readline: any; - let os: any; - let tty: { isatty(x: number): boolean }; - let isatty: boolean; - - const s = 1000; - const m = s * 60; - const h = m * 60; - const d = h * 24; - function ms(ms: number) { - let result = ""; - if (ms >= d) { - const count = Math.floor(ms / d); - result += count + "d"; - ms -= count * d; - } - if (ms >= h) { - const count = Math.floor(ms / h); - result += count + "h"; - ms -= count * h; - } - if (ms >= m) { - const count = Math.floor(ms / m); - result += count + "m"; - ms -= count * m; - } - if (ms >= s) { - const count = Math.round(ms / s); - result += count + "s"; - return result; - } - if (ms > 0) { - result += Math.round(ms) + "ms"; - } - return result; - } + function replayTest(runner: Mocha.Runner, test: RemoteTest) { + runner.emit("test", test); + if (test.isFailed()) { + runner.emit("fail", test, "error" in test.info ? rebuildError(test.info) : new Error("Unknown error")); + } + else { + runner.emit("pass", test); + } + runner.emit("test end", test); + } - function initializeProgressBarsDependencies() { - mocha = require("mocha"); - base = mocha.reporters.Base; - xunit = mocha.reporters.xunit; - color = base.color; - cursor = base.cursor; - readline = require("readline"); - os = require("os"); - tty = require("tty"); - isatty = tty.isatty(1) && tty.isatty(2); - } + endDate = new Date(); + duration = +endDate - +startDate; + completeBar(); + progressBars.disable(); - class ProgressBars { - public readonly _options: Readonly; - private _enabled: boolean; - private _lineCount: number; - private _progressBars: ProgressBar[]; - constructor(options?: Partial) { - if (!options) options = {}; - const open = options.open || "["; - const close = options.close || "]"; - const complete = options.complete || "▬"; - const incomplete = options.incomplete || base.symbols.dot; - const maxWidth = base.window.width - open.length - close.length - 34; - const width = minMax(options.width || maxWidth, 10, maxWidth); - this._options = { - open, - complete, - incomplete, - close, - width, - noColors: options.noColors || false - }; - - this._progressBars = []; - this._lineCount = 0; - this._enabled = false; - } - enable() { - if (!this._enabled) { - process.stdout.write(os.EOL); - this._enabled = true; - } - } - disable() { - if (this._enabled) { - process.stdout.write(os.EOL); - this._enabled = false; - } - } - update(index: number, percentComplete: number, color: string, title: string | undefined, titleColor?: string) { - percentComplete = minMax(percentComplete, 0, 1); - - const progressBar = this._progressBars[index] || (this._progressBars[index] = { }); - const width = this._options.width; - const n = Math.floor(width * percentComplete); - const i = width - n; - if (n === progressBar.lastN && title === progressBar.title && color === progressBar.progressColor) { - return; - } + const replayRunner = new Mocha.Runner(new Mocha.Suite(""), /*delay*/ false); + replayRunner.started = true; - progressBar.lastN = n; - progressBar.title = title; - progressBar.progressColor = color; + const consoleReporter = new Base(replayRunner); + patchStats(consoleReporter.stats); - let progress = " "; - progress += this._color("progress", this._options.open); - progress += this._color(color, fill(this._options.complete, n)); - progress += this._color("progress", fill(this._options.incomplete, i)); - progress += this._color("progress", this._options.close); + let xunitReporter: import("mocha").reporters.XUnit | undefined; + if (Utils.getExecutionEnvironment() !== Utils.ExecutionEnvironment.Browser && process.env.CI === "true") { + xunitReporter = new Mocha.reporters.XUnit(replayRunner, { reporterOptions: { suiteName: "Tests", output: "./TEST-results.xml" } }); + patchStats(xunitReporter.stats); + xunitReporter.write(`\n`); + } - if (title) { - progress += this._color(titleColor || "progress", " " + title); - } + const savedUseColors = Base.useColors; + if (noColors) Base.useColors = false; + replayRunner.started = true; + replayRunner.emit("start"); + replaySuite(replayRunner, rebuildSuite(errorResults, passingResults)); + replayRunner.emit("end"); + consoleReporter.epilogue(); + if (noColors) Base.useColors = savedUseColors; - if (progressBar.text !== progress) { - progressBar.text = progress; - this._render(index); - } - } - private _render(index: number) { - if (!this._enabled || !isatty) { - return; - } + IO.writeFile(perfdataFileName(configOption), JSON.stringify(newPerfData, null, 4)); // tslint:disable-line:no-null-keyword - cursor.hide(); - readline.moveCursor(process.stdout, -process.stdout.columns!, -this._lineCount); - let lineCount = 0; - const numProgressBars = this._progressBars.length; - for (let i = 0; i < numProgressBars; i++) { - if (i === index) { - readline.clearLine(process.stdout, 1); - process.stdout.write(this._progressBars[i].text + os.EOL); + if (xunitReporter) { + xunitReporter.done(errorResults.length, failures => process.exit(failures)); } else { - readline.moveCursor(process.stdout, -process.stdout.columns!, +1); + process.exit(errorResults.length); } + } + } - lineCount++; + function fill(ch: string, size: number) { + let s = ""; + while (s.length < size) { + s += ch; } - this._lineCount = lineCount; - cursor.show(); + return s.length > size ? s.substr(0, size) : s; } - private _color(type: string, text: string) { - return type && !this._options.noColors ? color(type, text) : text; + + function minMax(value: number, min: number, max: number) { + if (value < min) return min; + if (value > max) return max; + return value; } - } - function fill(ch: string, size: number) { - let s = ""; - while (s.length < size) { - s += ch; + function shimDiscoveryInterface(context: Mocha.MochaGlobals) { + shimNoopTestInterface(context); + + const perfData = readSavedPerfData(configOption); + context.describe = addSuite as Mocha.SuiteFunction; + + function addSuite(title: string) { + // Note, sub-suites are not indexed (we assume such granularity is not required) + let size = 0; + if (perfData) { + size = perfData[hashName("unittest", title)]; + if (size === undefined) { + newTasks.push({ runner: "unittest", file: title, size: 0 }); + unknownValue = title; + return; + } + } + tasks.push({ runner: "unittest", file: title, size }); + totalCost += size; + } } - return s.length > size ? s.substr(0, size) : s; - } + if (runUnitTests) { + shimDiscoveryInterface(global); + } + else { + shimNoopTestInterface(global); + } - function minMax(value: number, min: number, max: number) { - if (value < min) return min; - if (value > max) return max; - return value; + setTimeout(() => startDelayed(perfData, totalCost), 0); // Do real startup on next tick, so all unit tests have been collected } } diff --git a/src/testRunner/parallel/shared.ts b/src/testRunner/parallel/shared.ts index 46904fac896a5..a98f62abdbed2 100644 --- a/src/testRunner/parallel/shared.ts +++ b/src/testRunner/parallel/shared.ts @@ -1,16 +1,90 @@ /// /// namespace Harness.Parallel { - export type ParallelTestMessage = { type: "test", payload: { runner: TestRunnerKind | "unittest", file: string } } | never; - export type ParallelBatchMessage = { type: "batch", payload: ParallelTestMessage["payload"][] } | never; - export type ParallelCloseMessage = { type: "close" } | never; + export interface RunnerTask { + runner: TestRunnerKind; + file: string; + size: number; + } + + export interface UnitTestTask { + runner: "unittest"; + file: string; + size: number; + } + + export type Task = RunnerTask | UnitTestTask; + + export interface TestInfo { + name: string[]; + } + + export interface ErrorInfo { + name: string[]; + error: string; + stack: string; + } + + export interface TaskTimeout { + duration: number | "reset"; + } + + export interface TaskResult { + passing: number; + errors: ErrorInfo[]; + passes: TestInfo[]; + duration: number; + task: Task; + } + + export interface ParallelTestMessage { + type: "test"; + payload: Task; + } + + export interface ParallelBatchMessage { + type: "batch"; + payload: Task[]; + } + + export interface ParallelCloseMessage { + type: "close"; + } + export type ParallelHostMessage = ParallelTestMessage | ParallelCloseMessage | ParallelBatchMessage; - export type ParallelErrorMessage = { type: "error", payload: { error: string, stack: string, name?: string[] } } | never; - export type TestInfo = { name: string[] } | never; - export type ErrorInfo = ParallelErrorMessage["payload"] & TestInfo; - export type ParallelResultMessage = { type: "result", payload: { passing: number, errors: ErrorInfo[], passes: TestInfo[], duration: number, runner: TestRunnerKind | "unittest", file: string } } | never; - export type ParallelBatchProgressMessage = { type: "progress", payload: ParallelResultMessage["payload"] } | never; - export type ParallelTimeoutChangeMessage = { type: "timeout", payload: { duration: number | "reset" } } | never; + export interface ParallelErrorMessage { + type: "error"; + payload: { error: string, stack: string, name?: string[] }; + } + + export interface ParallelResultMessage { + type: "result"; + payload: TaskResult; + } + + export interface ParallelBatchProgressMessage { + type: "progress"; + payload: TaskResult; + } + + export interface ParallelTimeoutChangeMessage { + type: "timeout"; + payload: TaskTimeout; + } + export type ParallelClientMessage = ParallelErrorMessage | ParallelResultMessage | ParallelBatchProgressMessage | ParallelTimeoutChangeMessage; + + export function shimNoopTestInterface(global: Mocha.MochaGlobals) { + global.before = ts.noop; + global.after = ts.noop; + global.beforeEach = ts.noop; + global.afterEach = ts.noop; + global.describe = global.context = ((_: any, __: any) => { /*empty*/ }) as Mocha.SuiteFunction; + global.describe.skip = global.xdescribe = global.xcontext = ts.noop as Mocha.PendingSuiteFunction; + global.describe.only = ts.noop as Mocha.ExclusiveSuiteFunction; + global.it = global.specify = ((_: any, __: any) => { /*empty*/ }) as Mocha.TestFunction; + global.it.skip = global.xit = global.xspecify = ts.noop as Mocha.PendingTestFunction; + global.it.only = ts.noop as Mocha.ExclusiveTestFunction; + } } \ No newline at end of file diff --git a/src/testRunner/parallel/worker.ts b/src/testRunner/parallel/worker.ts index 1606e30465730..7579242ffaa47 100644 --- a/src/testRunner/parallel/worker.ts +++ b/src/testRunner/parallel/worker.ts @@ -1,312 +1,300 @@ // tslint:disable no-unnecessary-type-assertion (TODO: tslint can't find node types) namespace Harness.Parallel.Worker { - let errors: ErrorInfo[] = []; - let passes: TestInfo[] = []; - let passing = 0; - - type MochaCallback = (this: Mocha.ISuiteCallbackContext, done: MochaDone) => void; - type Callable = () => void; - - type Executor = {name: string, callback: MochaCallback, kind: "suite" | "test"} | never; - - function resetShimHarnessAndExecute(runner: RunnerBase) { - errors = []; - passes = []; - passing = 0; - testList.length = 0; - const start = +(new Date()); - runner.initializeTests(); - testList.forEach(({ name, callback, kind }) => executeCallback(name, callback, kind)); - return { errors, passes, passing, duration: +(new Date()) - start }; - } - - - let beforeEachFunc: Callable; - const namestack: string[] = []; - let testList: Executor[] = []; - function shimMochaHarness() { - (global as any).before = undefined; - (global as any).after = undefined; - (global as any).beforeEach = undefined; - (global as any).describe = ((name, callback) => { - testList.push({ name, callback, kind: "suite" }); - }) as Mocha.IContextDefinition; - (global as any).describe.skip = ts.noop; - (global as any).it = ((name, callback) => { - if (!testList) { - throw new Error("Tests must occur within a describe block"); + export function start() { + function hookUncaughtExceptions() { + if (!exceptionsHooked) { + process.on("uncaughtException", handleUncaughtException); + process.on("unhandledRejection", handleUncaughtException); + exceptionsHooked = true; } - testList.push({ name, callback: callback!, kind: "test" }); - }) as Mocha.ITestDefinition; - (global as any).it.skip = ts.noop; - } - - function setTimeoutAndExecute(timeout: number | undefined, f: () => void) { - if (timeout !== undefined) { - const timeoutMsg: ParallelTimeoutChangeMessage = { type: "timeout", payload: { duration: timeout } }; - process.send!(timeoutMsg); } - f(); - if (timeout !== undefined) { - // Reset timeout - const timeoutMsg: ParallelTimeoutChangeMessage = { type: "timeout", payload: { duration: "reset" } }; - process.send!(timeoutMsg); - } - } - function executeSuiteCallback(name: string, callback: MochaCallback) { - let timeout: number | undefined; - const fakeContext: Mocha.ISuiteCallbackContext = { - retries() { return this; }, - slow() { return this; }, - timeout(n: number) { - timeout = n; - return this; - }, - }; - namestack.push(name); - let beforeFunc: Callable | undefined; - (before as any) = (cb: Callable) => beforeFunc = cb; - let afterFunc: Callable | undefined; - (after as any) = (cb: Callable) => afterFunc = cb; - const savedBeforeEach = beforeEachFunc; - (beforeEach as any) = (cb: Callable) => beforeEachFunc = cb; - const savedTestList = testList; - - testList = []; - try { - callback.call(fakeContext); - } - catch (e) { - errors.push({ error: `Error executing suite: ${e.message}`, stack: e.stack, name: [...namestack] }); - return cleanup(); - } - try { - if (beforeFunc) { - beforeFunc(); + function unhookUncaughtExceptions() { + if (exceptionsHooked) { + process.removeListener("uncaughtException", handleUncaughtException); + process.removeListener("unhandledRejection", handleUncaughtException); + exceptionsHooked = false; } } - catch (e) { - errors.push({ error: `Error executing before function: ${e.message}`, stack: e.stack, name: [...namestack] }); - return cleanup(); - } - finally { - beforeFunc = undefined; - } - setTimeoutAndExecute(timeout, () => { - testList.forEach(({ name, callback, kind }) => executeCallback(name, callback, kind)); - }); + let exceptionsHooked = false; + hookUncaughtExceptions(); - try { - if (afterFunc) { - afterFunc(); + // tslint:disable-next-line:variable-name - Capitalization is aligned with the global `Mocha` namespace for typespace/namespace references. + const Mocha = require("mocha") as typeof import("mocha"); + + /** + * Mixin helper. + * @param base The base class constructor. + * @param mixins The mixins to apply to the constructor. + */ + function mixin any>(base: T, ...mixins: ((klass: T) => T)[]) { + for (const mixin of mixins) { + base = mixin(base); } + return base; } - catch (e) { - errors.push({ error: `Error executing after function: ${e.message}`, stack: e.stack, name: [...namestack] }); + + /** + * Mixes in overrides for `resetTimeout` and `clearTimeout` to support parallel test execution in a worker. + */ + function Timeout(base: T) { + return class extends (base as typeof Mocha.Runnable) { + resetTimeout() { + this.clearTimeout(); + if (this.enableTimeouts()) { + sendMessage({ type: "timeout", payload: { duration: this.timeout() || 1e9 } }); + this.timer = true; + } + } + clearTimeout() { + if (this.timer) { + sendMessage({ type: "timeout", payload: { duration: "reset" } }); + this.timer = false; + } + } + } as T; } - finally { - afterFunc = undefined; - cleanup(); + + /** + * Mixes in an override for `clone` to support parallel test execution in a worker. + */ + function Clone(base: T) { + return class extends (base as new (...args: any[]) => { clone(): any; }) { + clone() { + const cloned = super.clone(); + Object.setPrototypeOf(cloned, this.constructor.prototype); + return cloned; + } + } as T; } - function cleanup() { - testList.length = 0; - testList = savedTestList; - beforeEachFunc = savedBeforeEach; - namestack.pop(); + + /** + * A `Mocha.Suite` subclass to support parallel test execution in a worker. + */ + class Suite extends mixin(Mocha.Suite, Clone) { + _createHook(title: string, fn?: Mocha.Func | Mocha.AsyncFunc) { + const hook = super._createHook(title, fn); + Object.setPrototypeOf(hook, Hook.prototype); + return hook; + } } - } - function executeCallback(name: string, callback: MochaCallback, kind: "suite" | "test") { - if (kind === "suite") { - executeSuiteCallback(name, callback); + /** + * A `Mocha.Hook` subclass to support parallel test execution in a worker. + */ + class Hook extends mixin(Mocha.Hook, Timeout) { } - else { - executeTestCallback(name, callback); + + /** + * A `Mocha.Test` subclass to support parallel test execution in a worker. + */ + class Test extends mixin(Mocha.Test, Timeout, Clone) { } - } - function executeTestCallback(name: string, callback: MochaCallback) { - let timeout: number | undefined; - const fakeContext: Mocha.ITestCallbackContext = { - skip() { return this; }, - timeout(n: number) { - timeout = n; - const timeoutMsg: ParallelTimeoutChangeMessage = { type: "timeout", payload: { duration: timeout } }; - process.send!(timeoutMsg); - return this; - }, - retries() { return this; }, - slow() { return this; }, - }; - namestack.push(name); - if (beforeEachFunc) { - try { - beforeEachFunc(); + /** + * Shims a 'bdd'-style test interface to support parallel test execution in a worker. + * @param rootSuite The root suite. + * @param context The test context (usually the NodeJS `global` object). + */ + function shimTestInterface(rootSuite: Mocha.Suite, context: Mocha.MochaGlobals) { + // tslint:disable-next-line:variable-name + const suites = [rootSuite]; + context.before = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].beforeAll(title as string, fn); }; + context.after = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].afterAll(title as string, fn); }; + context.beforeEach = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].beforeEach(title as string, fn); }; + context.afterEach = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => { suites[0].afterEach(title as string, fn); }; + context.describe = context.context = ((title: string, fn: (this: Mocha.Suite) => void) => addSuite(title, fn)) as Mocha.SuiteFunction; + context.describe.skip = context.xdescribe = context.xcontext = (title: string) => addSuite(title, /*fn*/ undefined); + context.describe.only = (title: string, fn?: (this: Mocha.Suite) => void) => addSuite(title, fn); + context.it = context.specify = ((title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => addTest(title, fn)) as Mocha.TestFunction; + context.it.skip = context.xit = context.xspecify = (title: string | Mocha.Func | Mocha.AsyncFunc) => addTest(typeof title === "function" ? title.name : title, /*fn*/ undefined); + context.it.only = (title: string | Mocha.Func | Mocha.AsyncFunc, fn?: Mocha.Func | Mocha.AsyncFunc) => addTest(title, fn); + + function addSuite(title: string, fn: ((this: Mocha.Suite) => void) | undefined): Mocha.Suite { + const suite = new Suite(title, suites[0].ctx); + suites[0].addSuite(suite); + suite.pending = !fn; + suites.unshift(suite); + if (fn) { + fn.call(suite); + } + suites.shift(); + return suite; } - catch (error) { - errors.push({ error: error.message, stack: error.stack, name: [...namestack] }); - namestack.pop(); - return; + + function addTest(title: string | Mocha.Func | Mocha.AsyncFunc, fn: Mocha.Func | Mocha.AsyncFunc | undefined): Mocha.Test { + if (typeof title === "function") fn = title, title = fn.name; + const test = new Test(title, suites[0].pending ? undefined : fn); + suites[0].addTest(test); + return test; } } - if (callback.length === 0) { - try { - // TODO: If we ever start using async test completions, polyfill promise return handling - callback.call(fakeContext); - passes.push({ name: [...namestack] }); + + /** + * Run the tests in the requested task. + */ + function runTests(task: Task, fn: (payload: TaskResult) => void) { + if (task.runner === "unittest") { + return runUnitTests(task, fn); } - catch (error) { - errors.push({ error: error.message, stack: error.stack, name: [...namestack] }); - return; + else { + return runFileTests(task, fn); } - finally { - namestack.pop(); - if (timeout !== undefined) { - const timeoutMsg: ParallelTimeoutChangeMessage = { type: "timeout", payload: { duration: "reset" } }; - process.send!(timeoutMsg); + } + + function runUnitTests(task: UnitTestTask, fn: (payload: TaskResult) => void) { + if (!unitTestSuiteMap && unitTestSuite.suites.length) { + unitTestSuiteMap = ts.createMap(); + for (const suite of unitTestSuite.suites) { + unitTestSuiteMap.set(suite.title, suite); } } - passing++; + + if (!unitTestSuiteMap) { + throw new Error(`Asked to run unit test ${task.file}, but no unit tests were discovered!`); + } + + const suite = unitTestSuiteMap.get(task.file); + if (!suite) { + throw new Error(`Unit test with name "${task.file}" was asked to be run, but such a test does not exist!`); + } + + const root = new Suite("", new Mocha.Context()); + root.timeout(globalTimeout || 40_000); + root.addSuite(suite); + Object.setPrototypeOf(suite.ctx, root.ctx); + + runSuite(task, suite, payload => { + suite.parent = unitTestSuite; + Object.setPrototypeOf(suite.ctx, unitTestSuite.ctx); + fn(payload); + }); } - else { - // Uses `done` callback - let completed = false; - try { - callback.call(fakeContext, (err: any) => { - if (completed) { - throw new Error(`done() callback called multiple times; ensure it is only called once.`); - } - if (err) { - errors.push({ error: err.toString(), stack: "", name: [...namestack] }); - } - else { - passes.push({ name: [...namestack] }); - passing++; - } - completed = true; + + function runFileTests(task: RunnerTask, fn: (result: TaskResult) => void) { + let instance = runners.get(task.runner); + if (!instance) runners.set(task.runner, instance = createRunner(task.runner)); + instance.tests = [task.file]; + + const suite = new Suite("", new Mocha.Context()); + suite.timeout(globalTimeout || 40_000); + + shimTestInterface(suite, global); + instance.initializeTests(); + + runSuite(task, suite, fn); + } + + function runSuite(task: Task, suite: Mocha.Suite, fn: (result: TaskResult) => void) { + const errors: ErrorInfo[] = []; + const passes: TestInfo[] = []; + const start = +new Date(); + const runner = new Mocha.Runner(suite, /*delay*/ false); + const uncaught = (err: any) => runner.uncaught(err); + + runner + .on("start", () => { + unhookUncaughtExceptions(); // turn off global uncaught handling + process.on("unhandledRejection", uncaught); // turn on unhandled rejection handling (not currently handled in mocha) + }) + .on("pass", (test: Mocha.Test) => { + passes.push({ name: test.titlePath() }); + }) + .on("fail", (test: Mocha.Test | Mocha.Hook, err: any) => { + errors.push({ name: test.titlePath(), error: err.message, stack: err.stack }); + }) + .on("end", () => { + process.removeListener("unhandledRejection", uncaught); + hookUncaughtExceptions(); + }) + .run(() => { + fn({ task, errors, passes, passing: passes.length, duration: +new Date() - start }); }); + } + + /** + * Validates a message received from the host is well-formed. + */ + function validateHostMessage(message: ParallelHostMessage) { + switch (message.type) { + case "test": return validateTest(message.payload); + case "batch": return validateBatch(message.payload); + case "close": return true; + default: return false; } - catch (error) { - errors.push({ error: error.message, stack: error.stack, name: [...namestack] }); + } + + /** + * Validates a test task is well formed. + */ + function validateTest(task: Task) { + return !!task && !!task.runner && !!task.file; + } + + /** + * Validates a batch of test tasks are well formed. + */ + function validateBatch(tasks: Task[]) { + return !!tasks && Array.isArray(tasks) && tasks.length > 0 && tasks.every(validateTest); + } + + function processHostMessage(message: ParallelHostMessage) { + if (!validateHostMessage(message)) { + console.log("Invalid message:", message); return; } - finally { - namestack.pop(); - if (timeout !== undefined) { - const timeoutMsg: ParallelTimeoutChangeMessage = { type: "timeout", payload: { duration: "reset" } }; - process.send!(timeoutMsg); - } - } - if (!completed) { - errors.push({ error: "Test completes asynchronously, which is unsupported by the parallel harness", stack: "", name: [...namestack] }); + + switch (message.type) { + case "test": return processTest(message.payload, /*last*/ true); + case "batch": return processBatch(message.payload); + case "close": return process.exit(0); } } - } - export function start() { - let initialized = false; - const runners = ts.createMap(); - process.on("message", (data: ParallelHostMessage) => { - if (!initialized) { - initialized = true; - shimMochaHarness(); - } - switch (data.type) { - case "test": - const { runner, file } = data.payload; - if (!runner) { - console.error(data); - } - const message: ParallelResultMessage = { type: "result", payload: handleTest(runner, file) }; - process.send!(message); - break; - case "close": - process.exit(0); - break; - case "batch": { - const items = data.payload; - for (let i = 0; i < items.length; i++) { - const { runner, file } = items[i]; - if (!runner) { - console.error(data); - } - let message: ParallelBatchProgressMessage | ParallelResultMessage; - const payload = handleTest(runner, file); - if (i === (items.length - 1)) { - message = { type: "result", payload }; - } - else { - message = { type: "progress", payload }; - } - process.send!(message); - } - break; - } - } - }); - process.on("uncaughtException", error => { - const message: ParallelErrorMessage = { type: "error", payload: { error: error.message, stack: error.stack!, name: [...namestack] } }; - try { - process.send!(message); - } - catch (e) { - console.error(error); - throw error; - } - }); - if (!runUnitTests) { - // ensure unit tests do not get run - (global as any).describe = ts.noop; + function processTest(task: Task, last: boolean, fn?: () => void) { + runTests(task, payload => { + sendMessage(last ? { type: "result", payload } : { type: "progress", payload }); + if (fn) fn(); + }); } - else { - initialized = true; - shimMochaHarness(); + + function processBatch(tasks: Task[], fn?: () => void) { + const next = () => { + const task = tasks.shift(); + if (task) return processTest(task, tasks.length === 0, next); + if (fn) fn(); + }; + next(); } - function handleTest(runner: TestRunnerKind | "unittest", file: string) { - collectUnitTestsIfNeeded(); - if (runner === unittest) { - return executeUnitTest(file); - } - else { - if (!runners.has(runner)) { - runners.set(runner, createRunner(runner)); - } - const instance = runners.get(runner)!; - instance.tests = [file]; - return { ...resetShimHarnessAndExecute(instance), runner, file }; - } + function handleUncaughtException(err: any) { + const error = err instanceof Error ? err : new Error("" + err); + sendMessage({ type: "error", payload: { error: error.message, stack: error.stack! } }); } - } - const unittest: "unittest" = "unittest"; - let unitTests: {[name: string]: MochaCallback}; - function collectUnitTestsIfNeeded() { - if (!unitTests && testList.length) { - unitTests = {}; - for (const test of testList) { - unitTests[test.name] = test.callback; - } - testList.length = 0; + function sendMessage(message: ParallelClientMessage) { + process.send!(message); } - } - function executeUnitTest(name: string) { - if (!unitTests) { - throw new Error(`Asked to run unit test ${name}, but no unit tests were discovered!`); + // A cache of test harness Runner instances. + const runners = ts.createMap(); + + // The root suite for all unit tests. + let unitTestSuite: Suite; + let unitTestSuiteMap: ts.Map; + + if (runUnitTests) { + unitTestSuite = new Suite("", new Mocha.Context()); + unitTestSuite.timeout(globalTimeout || 40_000); + shimTestInterface(unitTestSuite, global); } - if (unitTests[name]) { - errors = []; - passes = []; - passing = 0; - const start = +(new Date()); - executeSuiteCallback(name, unitTests[name]); - delete unitTests[name]; - return { file: name, runner: unittest, errors, passes, passing, duration: +(new Date()) - start }; + else { + // ensure unit tests do not get run + shimNoopTestInterface(global); } - throw new Error(`Unit test with name "${name}" was asked to be run, but such a test does not exist!`); + + process.on("message", processHostMessage); } } \ No newline at end of file