Merge pull request #24868 from Microsoft/parallelAsyncTests

Support async tests in runtests-parallel
This commit is contained in:
Ron Buckton 2018-06-15 14:02:01 -07:00 коммит произвёл GitHub
Родитель e7e69ad4a2 23c7571e27
Коммит 6c8ecc7386
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
10 изменённых файлов: 4056 добавлений и 930 удалений

133
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",

Просмотреть файл

@ -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",

Просмотреть файл

@ -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

2856
scripts/types/mocha/index.d.ts поставляемый Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

109
scripts/types/mocha/lib/interfaces/common.d.ts поставляемый Normal file
Просмотреть файл

@ -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;
}
}

17
scripts/types/mocha/lib/ms.d.ts поставляемый Normal file
Просмотреть файл

@ -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;

Просмотреть файл

@ -0,0 +1,5 @@
{
"name": "@types/mocha",
"private": true,
"version": "5.2.1"
}

Разница между файлами не показана из-за своего большого размера Загрузить разницу

Просмотреть файл

@ -1,16 +1,90 @@
/// <reference path="./host.ts" />
/// <reference path="./worker.ts" />
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;
}
}

Просмотреть файл

@ -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");
}
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();
}
}
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));
});
try {
if (afterFunc) {
afterFunc();
}
}
catch (e) {
errors.push({ error: `Error executing after function: ${e.message}`, stack: e.stack, name: [...namestack] });
}
finally {
afterFunc = undefined;
cleanup();
}
function cleanup() {
testList.length = 0;
testList = savedTestList;
beforeEachFunc = savedBeforeEach;
namestack.pop();
}
}
function executeCallback(name: string, callback: MochaCallback, kind: "suite" | "test") {
if (kind === "suite") {
executeSuiteCallback(name, callback);
}
else {
executeTestCallback(name, callback);
}
}
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();
}
catch (error) {
errors.push({ error: error.message, stack: error.stack, name: [...namestack] });
namestack.pop();
return;
}
}
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] });
}
catch (error) {
errors.push({ error: error.message, stack: error.stack, name: [...namestack] });
return;
}
finally {
namestack.pop();
if (timeout !== undefined) {
const timeoutMsg: ParallelTimeoutChangeMessage = { type: "timeout", payload: { duration: "reset" } };
process.send!(timeoutMsg);
}
}
passing++;
}
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;
});
}
catch (error) {
errors.push({ error: error.message, stack: error.stack, name: [...namestack] });
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] });
}
}
}
export function start() {
let initialized = false;
const runners = ts.createMap<RunnerBase>();
process.on("message", (data: ParallelHostMessage) => {
if (!initialized) {
initialized = true;
shimMochaHarness();
function hookUncaughtExceptions() {
if (!exceptionsHooked) {
process.on("uncaughtException", handleUncaughtException);
process.on("unhandledRejection", handleUncaughtException);
exceptionsHooked = true;
}
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;
}
else {
initialized = true;
shimMochaHarness();
}
function handleTest(runner: TestRunnerKind | "unittest", file: string) {
collectUnitTestsIfNeeded();
if (runner === unittest) {
return executeUnitTest(file);
function unhookUncaughtExceptions() {
if (exceptionsHooked) {
process.removeListener("uncaughtException", handleUncaughtException);
process.removeListener("unhandledRejection", handleUncaughtException);
exceptionsHooked = false;
}
}
let exceptionsHooked = false;
hookUncaughtExceptions();
// 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<T extends new (...args: any[]) => any>(base: T, ...mixins: ((klass: T) => T)[]) {
for (const mixin of mixins) {
base = mixin(base);
}
return base;
}
/**
* Mixes in overrides for `resetTimeout` and `clearTimeout` to support parallel test execution in a worker.
*/
function Timeout<T extends typeof Mocha.Runnable>(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;
}
/**
* Mixes in an override for `clone` to support parallel test execution in a worker.
*/
function Clone<T extends typeof Mocha.Suite | typeof Mocha.Test>(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;
}
/**
* 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;
}
}
/**
* A `Mocha.Hook` subclass to support parallel test execution in a worker.
*/
class Hook extends mixin(Mocha.Hook, Timeout) {
}
/**
* A `Mocha.Test` subclass to support parallel test execution in a worker.
*/
class Test extends mixin(Mocha.Test, Timeout, Clone) {
}
/**
* 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;
}
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;
}
}
/**
* Run the tests in the requested task.
*/
function runTests(task: Task, fn: (payload: TaskResult) => void) {
if (task.runner === "unittest") {
return runUnitTests(task, fn);
}
else {
if (!runners.has(runner)) {
runners.set(runner, createRunner(runner));
return runFileTests(task, fn);
}
}
function runUnitTests(task: UnitTestTask, fn: (payload: TaskResult) => void) {
if (!unitTestSuiteMap && unitTestSuite.suites.length) {
unitTestSuiteMap = ts.createMap<Mocha.Suite>();
for (const suite of unitTestSuite.suites) {
unitTestSuiteMap.set(suite.title, suite);
}
const instance = runners.get(runner)!;
instance.tests = [file];
return { ...resetShimHarnessAndExecute(instance), runner, file };
}
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);
});
}
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;
}
}
}
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;
/**
* 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;
}
testList.length = 0;
}
}
function executeUnitTest(name: string) {
if (!unitTests) {
throw new Error(`Asked to run unit test ${name}, but no unit tests were discovered!`);
switch (message.type) {
case "test": return processTest(message.payload, /*last*/ true);
case "batch": return processBatch(message.payload);
case "close": return process.exit(0);
}
}
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 };
function processTest(task: Task, last: boolean, fn?: () => void) {
runTests(task, payload => {
sendMessage(last ? { type: "result", payload } : { type: "progress", payload });
if (fn) fn();
});
}
throw new Error(`Unit test with name "${name}" was asked to be run, but such a test does not exist!`);
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 handleUncaughtException(err: any) {
const error = err instanceof Error ? err : new Error("" + err);
sendMessage({ type: "error", payload: { error: error.message, stack: error.stack! } });
}
function sendMessage(message: ParallelClientMessage) {
process.send!(message);
}
// A cache of test harness Runner instances.
const runners = ts.createMap<RunnerBase>();
// The root suite for all unit tests.
let unitTestSuite: Suite;
let unitTestSuiteMap: ts.Map<Mocha.Suite>;
if (runUnitTests) {
unitTestSuite = new Suite("", new Mocha.Context());
unitTestSuite.timeout(globalTimeout || 40_000);
shimTestInterface(unitTestSuite, global);
}
else {
// ensure unit tests do not get run
shimNoopTestInterface(global);
}
process.on("message", processHostMessage);
}
}