Support timeouts in the parallel runner (#20631)

* Support timeouts in the parallel runner

* Apply PR feedback: unify code paths, use string as sentinel
This commit is contained in:
Wesley Wigham
2018-01-08 12:28:04 -08:00
committed by GitHub
parent b5fda4970d
commit 84e3681b79
6 changed files with 106 additions and 29 deletions

View File

@@ -9,6 +9,8 @@ namespace Harness.Parallel.Host {
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
}
interface ProgressBarsOptions {
@@ -134,6 +136,11 @@ namespace Harness.Parallel.Host {
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
@@ -141,6 +148,14 @@ namespace Harness.Parallel.Host {
const configPath = ts.combinePaths(taskConfigsFolder, `task-config${i}.json`);
Harness.IO.writeFile(configPath, JSON.stringify(config));
const child = fork(__filename, [`--config="${configPath}"`]);
let currentTimeout = defaultTimeout;
const killChild = () => {
child.kill();
console.error(`Worker exceeded 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);
@@ -160,8 +175,23 @@ namespace Harness.Parallel.Host {
Stack: ${data.payload.stack}`);
return process.exit(2);
}
case "timeout": {
if (data.payload.duration === "reset") {
currentTimeout = timeoutStack.pop() || defaultTimeout;
}
else {
timeoutStack.push(currentTimeout);
currentTimeout = data.payload.duration;
}
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);
@@ -195,6 +225,7 @@ namespace Harness.Parallel.Host {
while (tasks.length && taskList.reduce((p, c) => p + c.size, 0) < chunkSize) {
taskList.push(tasks.pop());
}
child.currentTasks = taskList;
if (taskList.length === 1) {
child.send({ type: "test", payload: taskList[0] });
}
@@ -252,18 +283,22 @@ namespace Harness.Parallel.Host {
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 });
}
}
}
else {
for (let i = 0; i < workerCount; i++) {
workers[i].send({ type: "test", payload: tasks.pop() });
const task = tasks.pop();
workers[i].currentTasks = [task];
workers[i].send({ type: "test", payload: task });
}
}

View File

@@ -10,5 +10,6 @@ namespace Harness.Parallel {
export type ErrorInfo = ParallelErrorMessage["payload"] & { name: string[] };
export type ParallelResultMessage = { type: "result", payload: { passing: number, errors: ErrorInfo[], duration: number, runner: TestRunnerKind | "unittest", file: string } } | never;
export type ParallelBatchProgressMessage = { type: "progress", payload: ParallelResultMessage["payload"] } | never;
export type ParallelClientMessage = ParallelErrorMessage | ParallelResultMessage | ParallelBatchProgressMessage;
export type ParallelTimeoutChangeMessage = { type: "timeout", payload: { duration: number | "reset" } } | never;
export type ParallelClientMessage = ParallelErrorMessage | ParallelResultMessage | ParallelBatchProgressMessage | ParallelTimeoutChangeMessage;
}

View File

@@ -36,11 +36,28 @@ namespace Harness.Parallel.Worker {
}) as Mocha.ITestDefinition;
}
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;
const fakeContext: Mocha.ISuiteCallbackContext = {
retries() { return this; },
slow() { return this; },
timeout() { return this; },
timeout(n) {
timeout = n;
return this;
},
};
namestack.push(name);
let beforeFunc: Callable;
@@ -71,7 +88,10 @@ namespace Harness.Parallel.Worker {
finally {
beforeFunc = undefined;
}
testList.forEach(({ name, callback, kind }) => executeCallback(name, callback, kind));
setTimeoutAndExecute(timeout, () => {
testList.forEach(({ name, callback, kind }) => executeCallback(name, callback, kind));
});
try {
if (afterFunc) {
@@ -103,9 +123,13 @@ namespace Harness.Parallel.Worker {
}
function executeTestCallback(name: string, callback: MochaCallback) {
let timeout: number;
const fakeContext: Mocha.ITestCallbackContext = {
skip() { return this; },
timeout() { return this; },
timeout(n) {
timeout = n;
return this;
},
retries() { return this; },
slow() { return this; },
};
@@ -121,18 +145,20 @@ namespace Harness.Parallel.Worker {
}
}
if (callback.length === 0) {
try {
// TODO: If we ever start using async test completions, polyfill promise return handling
callback.call(fakeContext);
}
catch (error) {
errors.push({ error: error.message, stack: error.stack, name: [...namestack] });
return;
}
finally {
namestack.pop();
}
passing++;
setTimeoutAndExecute(timeout, () => {
try {
// TODO: If we ever start using async test completions, polyfill promise return handling
callback.call(fakeContext);
}
catch (error) {
errors.push({ error: error.message, stack: error.stack, name: [...namestack] });
return;
}
finally {
namestack.pop();
}
passing++;
});
}
else {
// Uses `done` callback

View File

@@ -100,6 +100,7 @@ interface TestConfig {
runners?: string[];
runUnitTests?: boolean;
noColors?: boolean;
timeout?: number;
}
interface TaskSet {
@@ -108,12 +109,16 @@ interface TaskSet {
}
let configOption: string;
let globalTimeout: number;
function handleTestConfig() {
if (testConfigContent !== "") {
const testConfig = <TestConfig>JSON.parse(testConfigContent);
if (testConfig.light) {
Harness.lightMode = true;
}
if (testConfig.timeout) {
globalTimeout = testConfig.timeout;
}
runUnitTests = testConfig.runUnitTests;
if (testConfig.workerCount) {
workerCount = +testConfig.workerCount;