From d0168af142dfcaa0310a6825cfd67e5e0e51da1c Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 10 Oct 2017 17:59:43 -0700 Subject: [PATCH] Functioning parallel unittests (#18956) --- src/harness/parallel/host.ts | 50 ++++++++++++++++++++-------- src/harness/parallel/shared.ts | 4 +-- src/harness/parallel/worker.ts | 60 ++++++++++++++++++++++++---------- 3 files changed, 81 insertions(+), 33 deletions(-) diff --git a/src/harness/parallel/host.ts b/src/harness/parallel/host.ts index 2aaa4f78728..d7bba70408e 100644 --- a/src/harness/parallel/host.ts +++ b/src/harness/parallel/host.ts @@ -38,20 +38,45 @@ namespace Harness.Parallel.Host { return undefined; } - function hashName(runner: TestRunnerKind, test: string) { + function hashName(runner: TestRunnerKind | "unittest", test: string) { return `tsrunner-${runner}://${test}`; } + 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() { - initializeProgressBarsDependencies(); - console.log("Discovering tests..."); - const discoverStart = +(new Date()); - const { statSync }: { statSync(path: string): { size: number }; } = require("fs"); - let tasks: { runner: TestRunnerKind, file: string, size: number }[] = []; - const newTasks: { runner: TestRunnerKind, file: string, size: number }[] = []; const perfData = readSavedPerfData(configOption); let totalCost = 0; - let unknownValue: string | undefined; + 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; + } + } + tasks.push({ runner: "unittest", file: suiteName, size }); + totalCost += size; + }; + } + else { + (global as any).describe = ts.noop; + } + + setTimeout(() => startDelayed(perfData, totalCost), 0); // Do real startup on next tick, so all unit tests have been collected + } + + function startDelayed(perfData: {[testHash: string]: number}, 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"); for (const runner of runners) { const files = runner.enumerateTestFiles(); for (const file of files) { @@ -87,8 +112,7 @@ namespace Harness.Parallel.Host { } tasks.sort((a, b) => a.size - b.size); tasks = tasks.concat(newTasks); - // 1 fewer batches than threads to account for unittests running on the final thread - const batchCount = runners.length === 1 ? workerCount : workerCount - 1; + 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 @@ -113,7 +137,7 @@ namespace Harness.Parallel.Host { 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: Harness.lightMode, listenForWork: true, runUnitTests: runners.length === 1 ? false : i === workerCount - 1 }; + const config: TestConfig = { light: Harness.lightMode, listenForWork: true, runUnitTests: runners.length !== 1 }; const configPath = ts.combinePaths(taskConfigsFolder, `task-config${i}.json`); Harness.IO.writeFile(configPath, JSON.stringify(config)); const child = fork(__filename, [`--config="${configPath}"`]); @@ -187,7 +211,7 @@ namespace Harness.Parallel.Host { // It's only really worth doing an initial batching if there are a ton of files to go through if (totalFiles > 1000) { console.log("Batching initial test lists..."); - const batches: { runner: TestRunnerKind, file: string, size: number }[][] = new Array(batchCount); + const batches: { runner: TestRunnerKind | "unittest", file: string, size: number }[][] = new Array(batchCount); const doneBatching = new Array(batchCount); let scheduledTotal = 0; batcher: while (true) { @@ -230,7 +254,7 @@ namespace Harness.Parallel.Host { if (payload) { worker.send({ type: "batch", payload }); } - else { // Unittest thread - send off just one test + 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.send({ type: "test", payload }); diff --git a/src/harness/parallel/shared.ts b/src/harness/parallel/shared.ts index 85d885c14a1..2eb7777f828 100644 --- a/src/harness/parallel/shared.ts +++ b/src/harness/parallel/shared.ts @@ -1,14 +1,14 @@ /// /// namespace Harness.Parallel { - export type ParallelTestMessage = { type: "test", payload: { runner: TestRunnerKind, file: string } } | never; + 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 type ParallelHostMessage = ParallelTestMessage | ParallelCloseMessage | ParallelBatchMessage; export type ParallelErrorMessage = { type: "error", payload: { error: string, stack: string, name?: string[] } } | never; export type ErrorInfo = ParallelErrorMessage["payload"] & { name: string[] }; - export type ParallelResultMessage = { type: "result", payload: { passing: number, errors: ErrorInfo[], duration: number, runner: TestRunnerKind, file: string } } | never; + 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; } \ No newline at end of file diff --git a/src/harness/parallel/worker.ts b/src/harness/parallel/worker.ts index 7e95831535a..c32b9660a39 100644 --- a/src/harness/parallel/worker.ts +++ b/src/harness/parallel/worker.ts @@ -1,22 +1,13 @@ namespace Harness.Parallel.Worker { let errors: ErrorInfo[] = []; let passing = 0; - let reportedUnitTests = false; type Executor = {name: string, callback: Function, kind: "suite" | "test"} | never; function resetShimHarnessAndExecute(runner: RunnerBase) { - if (reportedUnitTests) { - errors = []; - passing = 0; - testList.length = 0; - } - reportedUnitTests = true; - if (testList.length) { - // Execute unit tests - testList.forEach(({ name, callback, kind }) => executeCallback(name, callback, kind)); - testList.length = 0; - } + errors = []; + passing = 0; + testList.length = 0; const start = +(new Date()); runner.initializeTests(); testList.forEach(({ name, callback, kind }) => executeCallback(name, callback, kind)); @@ -226,13 +217,46 @@ namespace Harness.Parallel.Worker { shimMochaHarness(); } - function handleTest(runner: TestRunnerKind, file: string) { - if (!runners.has(runner)) { - runners.set(runner, createRunner(runner)); + 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 }; } - const instance = runners.get(runner); - instance.tests = [file]; - return { ...resetShimHarnessAndExecute(instance), runner, file }; } } + + const unittest: "unittest" = "unittest"; + let unitTests: {[name: string]: Function}; + function collectUnitTestsIfNeeded() { + if (!unitTests && testList.length) { + unitTests = {}; + for (const test of testList) { + unitTests[test.name] = test.callback; + } + testList.length = 0; + } + } + + function executeUnitTest(name: string) { + if (!unitTests) { + throw new Error(`Asked to run unit test ${name}, but no unit tests were discovered!`); + } + if (unitTests[name]) { + errors = []; + passing = 0; + const start = +(new Date()); + executeSuiteCallback(name, unitTests[name]); + delete unitTests[name]; + return { file: name, runner: unittest, errors, passing, duration: +(new Date()) - start }; + } + throw new Error(`Unit test with name "${name}" was asked to be run, but such a test does not exist!`); + } } \ No newline at end of file