Skip to content

Commit d0168af

Browse files
authored
Functioning parallel unittests (#18956)
1 parent 856961b commit d0168af

File tree

3 files changed

+80
-32
lines changed

3 files changed

+80
-32
lines changed

src/harness/parallel/host.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,45 @@ namespace Harness.Parallel.Host {
3838
return undefined;
3939
}
4040

41-
function hashName(runner: TestRunnerKind, test: string) {
41+
function hashName(runner: TestRunnerKind | "unittest", test: string) {
4242
return `tsrunner-${runner}://${test}`;
4343
}
4444

45+
let tasks: { runner: TestRunnerKind | "unittest", file: string, size: number }[] = [];
46+
const newTasks: { runner: TestRunnerKind | "unittest", file: string, size: number }[] = [];
47+
let unknownValue: string | undefined;
4548
export function start() {
49+
const perfData = readSavedPerfData(configOption);
50+
let totalCost = 0;
51+
if (runUnitTests) {
52+
(global as any).describe = (suiteName: string) => {
53+
// Note, sub-suites are not indexed (we assume such granularity is not required)
54+
let size = 0;
55+
if (perfData) {
56+
size = perfData[hashName("unittest", suiteName)];
57+
if (size === undefined) {
58+
newTasks.push({ runner: "unittest", file: suiteName, size: 0 });
59+
unknownValue = suiteName;
60+
return;
61+
}
62+
}
63+
tasks.push({ runner: "unittest", file: suiteName, size });
64+
totalCost += size;
65+
};
66+
}
67+
else {
68+
(global as any).describe = ts.noop;
69+
}
70+
71+
setTimeout(() => startDelayed(perfData, totalCost), 0); // Do real startup on next tick, so all unit tests have been collected
72+
}
73+
74+
function startDelayed(perfData: {[testHash: string]: number}, totalCost: number) {
4675
initializeProgressBarsDependencies();
47-
console.log("Discovering tests...");
76+
console.log(`Discovered ${tasks.length} unittest suites` + (newTasks.length ? ` and ${newTasks.length} new suites.` : "."));
77+
console.log("Discovering runner-based tests...");
4878
const discoverStart = +(new Date());
4979
const { statSync }: { statSync(path: string): { size: number }; } = require("fs");
50-
let tasks: { runner: TestRunnerKind, file: string, size: number }[] = [];
51-
const newTasks: { runner: TestRunnerKind, file: string, size: number }[] = [];
52-
const perfData = readSavedPerfData(configOption);
53-
let totalCost = 0;
54-
let unknownValue: string | undefined;
5580
for (const runner of runners) {
5681
const files = runner.enumerateTestFiles();
5782
for (const file of files) {
@@ -87,8 +112,7 @@ namespace Harness.Parallel.Host {
87112
}
88113
tasks.sort((a, b) => a.size - b.size);
89114
tasks = tasks.concat(newTasks);
90-
// 1 fewer batches than threads to account for unittests running on the final thread
91-
const batchCount = runners.length === 1 ? workerCount : workerCount - 1;
115+
const batchCount = workerCount;
92116
const packfraction = 0.9;
93117
const chunkSize = 1000; // ~1KB or 1s for sending batches near the end of a test
94118
const batchSize = (totalCost / workerCount) * packfraction; // Keep spare tests for unittest thread in reserve
@@ -113,7 +137,7 @@ namespace Harness.Parallel.Host {
113137
let closedWorkers = 0;
114138
for (let i = 0; i < workerCount; i++) {
115139
// TODO: Just send the config over the IPC channel or in the command line arguments
116-
const config: TestConfig = { light: Harness.lightMode, listenForWork: true, runUnitTests: runners.length === 1 ? false : i === workerCount - 1 };
140+
const config: TestConfig = { light: Harness.lightMode, listenForWork: true, runUnitTests: runners.length !== 1 };
117141
const configPath = ts.combinePaths(taskConfigsFolder, `task-config${i}.json`);
118142
Harness.IO.writeFile(configPath, JSON.stringify(config));
119143
const child = fork(__filename, [`--config="${configPath}"`]);
@@ -187,7 +211,7 @@ namespace Harness.Parallel.Host {
187211
// It's only really worth doing an initial batching if there are a ton of files to go through
188212
if (totalFiles > 1000) {
189213
console.log("Batching initial test lists...");
190-
const batches: { runner: TestRunnerKind, file: string, size: number }[][] = new Array(batchCount);
214+
const batches: { runner: TestRunnerKind | "unittest", file: string, size: number }[][] = new Array(batchCount);
191215
const doneBatching = new Array(batchCount);
192216
let scheduledTotal = 0;
193217
batcher: while (true) {
@@ -230,7 +254,7 @@ namespace Harness.Parallel.Host {
230254
if (payload) {
231255
worker.send({ type: "batch", payload });
232256
}
233-
else { // Unittest thread - send off just one test
257+
else { // Out of batches, send off just one test
234258
const payload = tasks.pop();
235259
ts.Debug.assert(!!payload); // The reserve kept above should ensure there is always an initial task available, even in suboptimal scenarios
236260
worker.send({ type: "test", payload });

src/harness/parallel/shared.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/// <reference path="./host.ts" />
22
/// <reference path="./worker.ts" />
33
namespace Harness.Parallel {
4-
export type ParallelTestMessage = { type: "test", payload: { runner: TestRunnerKind, file: string } } | never;
4+
export type ParallelTestMessage = { type: "test", payload: { runner: TestRunnerKind | "unittest", file: string } } | never;
55
export type ParallelBatchMessage = { type: "batch", payload: ParallelTestMessage["payload"][] } | never;
66
export type ParallelCloseMessage = { type: "close" } | never;
77
export type ParallelHostMessage = ParallelTestMessage | ParallelCloseMessage | ParallelBatchMessage;
88

99
export type ParallelErrorMessage = { type: "error", payload: { error: string, stack: string, name?: string[] } } | never;
1010
export type ErrorInfo = ParallelErrorMessage["payload"] & { name: string[] };
11-
export type ParallelResultMessage = { type: "result", payload: { passing: number, errors: ErrorInfo[], duration: number, runner: TestRunnerKind, file: string } } | never;
11+
export type ParallelResultMessage = { type: "result", payload: { passing: number, errors: ErrorInfo[], duration: number, runner: TestRunnerKind | "unittest", file: string } } | never;
1212
export type ParallelBatchProgressMessage = { type: "progress", payload: ParallelResultMessage["payload"] } | never;
1313
export type ParallelClientMessage = ParallelErrorMessage | ParallelResultMessage | ParallelBatchProgressMessage;
1414
}

src/harness/parallel/worker.ts

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,13 @@
11
namespace Harness.Parallel.Worker {
22
let errors: ErrorInfo[] = [];
33
let passing = 0;
4-
let reportedUnitTests = false;
54

65
type Executor = {name: string, callback: Function, kind: "suite" | "test"} | never;
76

87
function resetShimHarnessAndExecute(runner: RunnerBase) {
9-
if (reportedUnitTests) {
10-
errors = [];
11-
passing = 0;
12-
testList.length = 0;
13-
}
14-
reportedUnitTests = true;
15-
if (testList.length) {
16-
// Execute unit tests
17-
testList.forEach(({ name, callback, kind }) => executeCallback(name, callback, kind));
18-
testList.length = 0;
19-
}
8+
errors = [];
9+
passing = 0;
10+
testList.length = 0;
2011
const start = +(new Date());
2112
runner.initializeTests();
2213
testList.forEach(({ name, callback, kind }) => executeCallback(name, callback, kind));
@@ -226,13 +217,46 @@ namespace Harness.Parallel.Worker {
226217
shimMochaHarness();
227218
}
228219

229-
function handleTest(runner: TestRunnerKind, file: string) {
230-
if (!runners.has(runner)) {
231-
runners.set(runner, createRunner(runner));
220+
function handleTest(runner: TestRunnerKind | "unittest", file: string) {
221+
collectUnitTestsIfNeeded();
222+
if (runner === unittest) {
223+
return executeUnitTest(file);
224+
}
225+
else {
226+
if (!runners.has(runner)) {
227+
runners.set(runner, createRunner(runner));
228+
}
229+
const instance = runners.get(runner);
230+
instance.tests = [file];
231+
return { ...resetShimHarnessAndExecute(instance), runner, file };
232232
}
233-
const instance = runners.get(runner);
234-
instance.tests = [file];
235-
return { ...resetShimHarnessAndExecute(instance), runner, file };
236233
}
237234
}
235+
236+
const unittest: "unittest" = "unittest";
237+
let unitTests: {[name: string]: Function};
238+
function collectUnitTestsIfNeeded() {
239+
if (!unitTests && testList.length) {
240+
unitTests = {};
241+
for (const test of testList) {
242+
unitTests[test.name] = test.callback;
243+
}
244+
testList.length = 0;
245+
}
246+
}
247+
248+
function executeUnitTest(name: string) {
249+
if (!unitTests) {
250+
throw new Error(`Asked to run unit test ${name}, but no unit tests were discovered!`);
251+
}
252+
if (unitTests[name]) {
253+
errors = [];
254+
passing = 0;
255+
const start = +(new Date());
256+
executeSuiteCallback(name, unitTests[name]);
257+
delete unitTests[name];
258+
return { file: name, runner: unittest, errors, passing, duration: +(new Date()) - start };
259+
}
260+
throw new Error(`Unit test with name "${name}" was asked to be run, but such a test does not exist!`);
261+
}
238262
}

0 commit comments

Comments
 (0)