mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-10 01:43:59 -05:00
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:
21
Gulpfile.ts
21
Gulpfile.ts
@@ -680,14 +680,14 @@ function runConsoleTests(defaultReporter: string, runInParallel: boolean, done:
|
||||
workerCount = cmdLineOptions.workers;
|
||||
}
|
||||
|
||||
if (tests || runners || light || taskConfigsFolder) {
|
||||
writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit);
|
||||
}
|
||||
|
||||
if (tests && tests.toLocaleLowerCase() === "rwc") {
|
||||
testTimeout = 400000;
|
||||
}
|
||||
|
||||
if (tests || runners || light || testTimeout || taskConfigsFolder) {
|
||||
writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, testTimeout);
|
||||
}
|
||||
|
||||
const colors = cmdLineOptions.colors;
|
||||
const reporter = cmdLineOptions.reporter || defaultReporter;
|
||||
|
||||
@@ -872,8 +872,17 @@ function cleanTestDirs(done: (e?: any) => void) {
|
||||
}
|
||||
|
||||
// used to pass data from jake command line directly to run.js
|
||||
function writeTestConfigFile(tests: string, runners: string, light: boolean, taskConfigsFolder?: string, workerCount?: number, stackTraceLimit?: string) {
|
||||
const testConfigContents = JSON.stringify({ test: tests ? [tests] : undefined, runner: runners ? runners.split(",") : undefined, light, workerCount, stackTraceLimit, taskConfigsFolder, noColor: !cmdLineOptions.colors });
|
||||
function writeTestConfigFile(tests: string, runners: string, light: boolean, taskConfigsFolder?: string, workerCount?: number, stackTraceLimit?: string, timeout?: number) {
|
||||
const testConfigContents = JSON.stringify({
|
||||
test: tests ? [tests] : undefined,
|
||||
runner: runners ? runners.split(",") : undefined,
|
||||
light,
|
||||
workerCount,
|
||||
stackTraceLimit,
|
||||
taskConfigsFolder,
|
||||
noColor: !cmdLineOptions.colors,
|
||||
timeout,
|
||||
});
|
||||
console.log("Running tests with config: " + testConfigContents);
|
||||
fs.writeFileSync("test.config", testConfigContents);
|
||||
}
|
||||
|
||||
13
Jakefile.js
13
Jakefile.js
@@ -858,7 +858,7 @@ function cleanTestDirs() {
|
||||
}
|
||||
|
||||
// used to pass data from jake command line directly to run.js
|
||||
function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, colors) {
|
||||
function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, colors, testTimeout) {
|
||||
var testConfigContents = JSON.stringify({
|
||||
runners: runners ? runners.split(",") : undefined,
|
||||
test: tests ? [tests] : undefined,
|
||||
@@ -866,7 +866,8 @@ function writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCou
|
||||
workerCount: workerCount,
|
||||
taskConfigsFolder: taskConfigsFolder,
|
||||
stackTraceLimit: stackTraceLimit,
|
||||
noColor: !colors
|
||||
noColor: !colors,
|
||||
timeout: testTimeout
|
||||
});
|
||||
fs.writeFileSync('test.config', testConfigContents);
|
||||
}
|
||||
@@ -908,14 +909,14 @@ function runConsoleTests(defaultReporter, runInParallel) {
|
||||
workerCount = process.env.workerCount || process.env.p || os.cpus().length;
|
||||
}
|
||||
|
||||
if (tests || runners || light || taskConfigsFolder) {
|
||||
writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, colors);
|
||||
}
|
||||
|
||||
if (tests && tests.toLocaleLowerCase() === "rwc") {
|
||||
testTimeout = 800000;
|
||||
}
|
||||
|
||||
if (tests || runners || light || testTimeout || taskConfigsFolder) {
|
||||
writeTestConfigFile(tests, runners, light, taskConfigsFolder, workerCount, stackTraceLimit, colors, testTimeout);
|
||||
}
|
||||
|
||||
var colorsFlag = process.env.color || process.env.colors;
|
||||
var colors = colorsFlag !== "false" && colorsFlag !== "0";
|
||||
var reporter = process.env.reporter || process.env.r || defaultReporter;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user