chore(test runner): reuse TestGroup instead of DispatcherEntry (#7924)

This commit is contained in:
Dmitry Gozman 2021-07-29 21:41:06 -07:00 committed by GitHub
parent 4163cec93b
commit 2e387b3a3a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23

View file

@ -22,14 +22,6 @@ import type { TestResult, Reporter } from '../../types/testReporter';
import { TestCase } from './test'; import { TestCase } from './test';
import { Loader } from './loader'; import { Loader } from './loader';
// TODO: use TestGroup instead of DispatcherEntry
type DispatcherEntry = {
runPayload: RunPayload;
hash: string;
repeatEachIndex: number;
projectIndex: number;
};
export type TestGroup = { export type TestGroup = {
workerHash: string; workerHash: string;
requireFile: string; requireFile: string;
@ -44,7 +36,7 @@ export class Dispatcher {
private _workerClaimers: (() => void)[] = []; private _workerClaimers: (() => void)[] = [];
private _testById = new Map<string, { test: TestCase, result: TestResult }>(); private _testById = new Map<string, { test: TestCase, result: TestResult }>();
private _queue: DispatcherEntry[] = []; private _queue: TestGroup[] = [];
private _stopCallback = () => {}; private _stopCallback = () => {};
readonly _loader: Loader; readonly _loader: Loader;
private _reporter: Reporter; private _reporter: Reporter;
@ -55,27 +47,12 @@ export class Dispatcher {
constructor(loader: Loader, testGroups: TestGroup[], reporter: Reporter) { constructor(loader: Loader, testGroups: TestGroup[], reporter: Reporter) {
this._loader = loader; this._loader = loader;
this._reporter = reporter; this._reporter = reporter;
this._queue = testGroups;
this._queue = [];
for (const group of testGroups) { for (const group of testGroups) {
const entry: DispatcherEntry = {
runPayload: {
file: group.requireFile,
entries: []
},
hash: group.workerHash,
repeatEachIndex: group.repeatEachIndex,
projectIndex: group.projectIndex,
};
for (const test of group.tests) { for (const test of group.tests) {
const result = test._appendTestResult(); const result = test._appendTestResult();
this._testById.set(test._id, { test, result }); this._testById.set(test._id, { test, result });
entry.runPayload.entries.push({
retry: result.retry,
testId: test._id,
});
} }
this._queue.push(entry);
} }
} }
@ -90,22 +67,23 @@ export class Dispatcher {
while (this._queue.length) { while (this._queue.length) {
if (this._isStopped) if (this._isStopped)
break; break;
const entry = this._queue.shift()!; const testGroup = this._queue.shift()!;
const requiredHash = entry.hash; const requiredHash = testGroup.workerHash;
let worker = await this._obtainWorker(entry); let worker = await this._obtainWorker(testGroup);
while (!this._isStopped && worker.hash && worker.hash !== requiredHash) { while (!this._isStopped && worker.hash && worker.hash !== requiredHash) {
worker.stop(); worker.stop();
worker = await this._obtainWorker(entry); worker = await this._obtainWorker(testGroup);
} }
if (this._isStopped) if (this._isStopped)
break; break;
jobs.push(this._runJob(worker, entry)); jobs.push(this._runJob(worker, testGroup));
} }
await Promise.all(jobs); await Promise.all(jobs);
} }
async _runJob(worker: Worker, entry: DispatcherEntry) { async _runJob(worker: Worker, testGroup: TestGroup) {
worker.run(entry.runPayload); worker.run(testGroup);
let doneCallback = () => {}; let doneCallback = () => {};
const result = new Promise<void>(f => doneCallback = f); const result = new Promise<void>(f => doneCallback = f);
const doneWithJob = () => { const doneWithJob = () => {
@ -116,7 +94,7 @@ export class Dispatcher {
doneCallback(); doneCallback();
}; };
const remainingByTestId = new Map(entry.runPayload.entries.map(e => [ e.testId, e ])); const remainingByTestId = new Map(testGroup.tests.map(e => [ e._id, e ]));
let lastStartedTestId: string | undefined; let lastStartedTestId: string | undefined;
const onTestBegin = (params: TestBeginPayload) => { const onTestBegin = (params: TestBeginPayload) => {
@ -152,17 +130,17 @@ export class Dispatcher {
// and all others as skipped. // and all others as skipped.
if (params.fatalError) { if (params.fatalError) {
let first = true; let first = true;
for (const { testId } of remaining) { for (const test of remaining) {
const { test, result } = this._testById.get(testId)!; const { result } = this._testById.get(test._id)!;
if (this._hasReachedMaxFailures()) if (this._hasReachedMaxFailures())
break; break;
// There might be a single test that has started but has not finished yet. // There might be a single test that has started but has not finished yet.
if (testId !== lastStartedTestId) if (test._id !== lastStartedTestId)
this._reporter.onTestBegin?.(test); this._reporter.onTestBegin?.(test);
result.error = params.fatalError; result.error = params.fatalError;
result.status = first ? 'failed' : 'skipped'; result.status = first ? 'failed' : 'skipped';
this._reportTestEnd(test, result); this._reportTestEnd(test, result);
failedTestIds.add(testId); failedTestIds.add(test._id);
first = false; first = false;
} }
// Since we pretend that all remaining tests failed, there is nothing else to run, // Since we pretend that all remaining tests failed, there is nothing else to run,
@ -177,15 +155,12 @@ export class Dispatcher {
const pair = this._testById.get(testId)!; const pair = this._testById.get(testId)!;
if (!this._isStopped && pair.test.expectedStatus === 'passed' && pair.test.results.length < pair.test.retries + 1) { if (!this._isStopped && pair.test.expectedStatus === 'passed' && pair.test.results.length < pair.test.retries + 1) {
pair.result = pair.test._appendTestResult(); pair.result = pair.test._appendTestResult();
remaining.unshift({ remaining.unshift(pair.test);
retry: pair.result.retry,
testId: pair.test._id,
});
} }
} }
if (remaining.length) if (remaining.length)
this._queue.unshift({ ...entry, runPayload: { ...entry.runPayload, entries: remaining } }); this._queue.unshift({ ...testGroup, tests: remaining });
// This job is over, we just scheduled another one. // This job is over, we just scheduled another one.
doneWithJob(); doneWithJob();
@ -203,14 +178,14 @@ export class Dispatcher {
return result; return result;
} }
async _obtainWorker(entry: DispatcherEntry) { async _obtainWorker(testGroup: TestGroup) {
const claimWorker = (): Promise<Worker> | null => { const claimWorker = (): Promise<Worker> | null => {
// Use available worker. // Use available worker.
if (this._freeWorkers.length) if (this._freeWorkers.length)
return Promise.resolve(this._freeWorkers.pop()!); return Promise.resolve(this._freeWorkers.pop()!);
// Create a new worker. // Create a new worker.
if (this._workers.size < this._loader.fullConfig().workers) if (this._workers.size < this._loader.fullConfig().workers)
return this._createWorker(entry); return this._createWorker(testGroup);
return null; return null;
}; };
@ -232,7 +207,7 @@ export class Dispatcher {
callback(); callback();
} }
_createWorker(entry: DispatcherEntry) { _createWorker(testGroup: TestGroup) {
const worker = new Worker(this); const worker = new Worker(this);
worker.on('testBegin', (params: TestBeginPayload) => { worker.on('testBegin', (params: TestBeginPayload) => {
if (this._hasReachedMaxFailures()) if (this._hasReachedMaxFailures())
@ -285,7 +260,7 @@ export class Dispatcher {
this._stopCallback(); this._stopCallback();
}); });
this._workers.add(worker); this._workers.add(worker);
return worker.init(entry).then(() => worker); return worker.init(testGroup).then(() => worker);
} }
async stop() { async stop() {
@ -350,19 +325,25 @@ class Worker extends EventEmitter {
}); });
} }
async init(entry: DispatcherEntry) { async init(testGroup: TestGroup) {
this.hash = entry.hash; this.hash = testGroup.workerHash;
const params: WorkerInitParams = { const params: WorkerInitParams = {
workerIndex: this.index, workerIndex: this.index,
repeatEachIndex: entry.repeatEachIndex, repeatEachIndex: testGroup.repeatEachIndex,
projectIndex: entry.projectIndex, projectIndex: testGroup.projectIndex,
loader: this.runner._loader.serialize(), loader: this.runner._loader.serialize(),
}; };
this.process.send({ method: 'init', params }); this.process.send({ method: 'init', params });
await new Promise(f => this.process.once('message', f)); // Ready ack await new Promise(f => this.process.once('message', f)); // Ready ack
} }
run(runPayload: RunPayload) { run(testGroup: TestGroup) {
const runPayload: RunPayload = {
file: testGroup.requireFile,
entries: testGroup.tests.map(test => {
return { testId: test._id, retry: test.results.length - 1 };
}),
};
this.process.send({ method: 'run', params: runPayload }); this.process.send({ method: 'run', params: runPayload });
} }