Skip to content

Commit 528d1c0

Browse files
authored
fix(test runner): project.workers is sometimes exceeded (#39539)
1 parent e38f9aa commit 528d1c0

2 files changed

Lines changed: 48 additions & 8 deletions

File tree

packages/playwright/src/runner/dispatcher.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ import type { RegisteredListener } from 'playwright-core/lib/utils';
4040
export type EnvByProjectId = Map<string, Record<string, string | undefined>>;
4141

4242
export class Dispatcher {
43-
private _workerSlots: { busy: boolean, worker?: WorkerHost, jobDispatcher?: JobDispatcher }[] = [];
43+
// Worker slot is claimed when it has jobDispatcher assigned.
44+
private _workerSlots: { worker?: WorkerHost, jobDispatcher?: JobDispatcher }[] = [];
4445
private _queue: TestGroup[] = [];
4546
private _workerLimitPerProjectId = new Map<string, number>();
4647
private _queuedOrRunningHashCount = new Map<string, number>();
@@ -71,7 +72,7 @@ export class Dispatcher {
7172
const projectIdWorkerLimit = this._workerLimitPerProjectId.get(job.projectId);
7273
if (!projectIdWorkerLimit)
7374
return index;
74-
const runningWorkersWithSameProjectId = this._workerSlots.filter(w => w.busy && w.worker && w.worker.projectId() === job.projectId).length;
75+
const runningWorkersWithSameProjectId = this._workerSlots.filter(w => w.jobDispatcher?.job.projectId === job.projectId).length;
7576
if (runningWorkersWithSameProjectId < projectIdWorkerLimit)
7677
return index;
7778
}
@@ -92,9 +93,9 @@ export class Dispatcher {
9293
const job = this._queue[jobIndex];
9394

9495
// 2. Find a worker with the same hash, or just some free worker.
95-
let workerIndex = this._workerSlots.findIndex(w => !w.busy && w.worker && w.worker.hash() === job.workerHash && !w.worker.didSendStop());
96+
let workerIndex = this._workerSlots.findIndex(w => !w.jobDispatcher && w.worker && w.worker.hash() === job.workerHash && !w.worker.didSendStop());
9697
if (workerIndex === -1)
97-
workerIndex = this._workerSlots.findIndex(w => !w.busy);
98+
workerIndex = this._workerSlots.findIndex(w => !w.jobDispatcher);
9899
if (workerIndex === -1) {
99100
// No workers available, bail out.
100101
return;
@@ -103,15 +104,13 @@ export class Dispatcher {
103104
// 3. Claim both the job and the worker slot.
104105
this._queue.splice(jobIndex, 1);
105106
const jobDispatcher = new JobDispatcher(job, this._config, this._reporter, this._failureTracker, () => this.stop().catch(() => {}));
106-
this._workerSlots[workerIndex].busy = true;
107107
this._workerSlots[workerIndex].jobDispatcher = jobDispatcher;
108108

109109
// 4. Run the job. This is the only async operation.
110110
void this._runJobInWorker(workerIndex, jobDispatcher).then(() => {
111111

112112
// 5. Release the worker slot.
113113
this._workerSlots[workerIndex].jobDispatcher = undefined;
114-
this._workerSlots[workerIndex].busy = false;
115114

116115
// 6. Check whether we are done or should schedule another job.
117116
this._checkFinished();
@@ -178,7 +177,7 @@ export class Dispatcher {
178177
return;
179178

180179
// Make sure all workers have finished the current job.
181-
if (this._workerSlots.some(w => w.busy))
180+
if (this._workerSlots.some(w => !!w.jobDispatcher))
182181
return;
183182

184183
this._finished.resolve();
@@ -209,7 +208,7 @@ export class Dispatcher {
209208
void this.stop();
210209
// 1. Allocate workers.
211210
for (let i = 0; i < this._config.config.workers; i++)
212-
this._workerSlots.push({ busy: false });
211+
this._workerSlots.push({});
213212
// 2. Schedule enough jobs.
214213
for (let i = 0; i < this._workerSlots.length; i++)
215214
this._scheduleJob();

tests/playwright-test/worker-index.spec.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,47 @@ test('should respect project.workers=1', async ({ runInlineTest }) => {
280280
]);
281281
});
282282

283+
test('should respect project.workers=1 on startup', async ({ runInlineTest }) => {
284+
const result = await runInlineTest({
285+
'playwright.config.ts': `
286+
export default {
287+
workers: 2,
288+
projects: [
289+
{ name: 'project1' },
290+
{ name: 'project2', workers: 1 },
291+
],
292+
};
293+
`,
294+
'a.test.js': `
295+
import { test, expect } from '@playwright/test';
296+
test('test1', async ({}, testInfo) => {
297+
console.log('%%test1-begin:' + testInfo.project.name);
298+
await new Promise(f => setTimeout(f, 2000));
299+
console.log('%%test1-end:' + testInfo.project.name);
300+
});
301+
`,
302+
'b.test.js': `
303+
import { test, expect } from '@playwright/test';
304+
test('test2', async ({}, testInfo) => {
305+
console.log('%%test2-begin:' + testInfo.project.name);
306+
await new Promise(f => setTimeout(f, 2000));
307+
console.log('%%test2-end:' + testInfo.project.name);
308+
});
309+
`,
310+
}, { workers: 2 });
311+
expect(result.passed).toBe(4);
312+
expect(result.exitCode).toBe(0);
313+
314+
// Once both tests from the first project finish apporximately at the same time,
315+
// tests from the second project should run sequentially.
316+
expect(result.outputLines.slice(4, 8)).toEqual([
317+
'test1-begin:project2',
318+
'test1-end:project2',
319+
'test2-begin:project2',
320+
'test2-end:project2',
321+
]);
322+
});
323+
283324
test('should respect project.workers>1', async ({ runInlineTest }) => {
284325
const result = await runInlineTest({
285326
'playwright.config.ts': `

0 commit comments

Comments
 (0)