Skip to content

Commit e55a3ff

Browse files
authored
cherry-pick(#13138): fix(parallel): minimize the number of beforeAll/afterAll hooks in parallel mode (#13140)
Previously, we always formed groups consisting of a single test. Now, we group tests that share `beforeAll`/`afterAll` hooks into `config.workers` equally-sized groups.
1 parent 18de7bd commit e55a3ff

2 files changed

Lines changed: 80 additions & 8 deletions

File tree

packages/playwright-test/src/runner.ts

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ export class Runner {
313313
fatalErrors.push(createNoTestsError());
314314

315315
// 8. Compute shards.
316-
let testGroups = createTestGroups(rootSuite);
316+
let testGroups = createTestGroups(rootSuite, config.workers);
317317

318318
const shard = config.shard;
319319
if (shard) {
@@ -619,7 +619,7 @@ function buildItemLocation(rootDir: string, testOrSuite: Suite | TestCase) {
619619
return `${path.relative(rootDir, testOrSuite.location.file)}:${testOrSuite.location.line}`;
620620
}
621621

622-
function createTestGroups(rootSuite: Suite): TestGroup[] {
622+
function createTestGroups(rootSuite: Suite, workers: number): TestGroup[] {
623623
// This function groups tests that can be run together.
624624
// Tests cannot be run together when:
625625
// - They belong to different projects - requires different workers.
@@ -630,7 +630,15 @@ function createTestGroups(rootSuite: Suite): TestGroup[] {
630630

631631
// Using the map "workerHash -> requireFile -> group" makes us preserve the natural order
632632
// of worker hashes and require files for the simple cases.
633-
const groups = new Map<string, Map<string, { general: TestGroup, parallel: TestGroup[] }>>();
633+
const groups = new Map<string, Map<string, {
634+
// Tests that must be run in order are in the same group.
635+
general: TestGroup,
636+
// Tests that may be run independently each has a dedicated group with a single test.
637+
parallel: TestGroup[],
638+
// Tests that are marked as parallel but have beforeAll/afterAll hooks should be grouped
639+
// as much as possible. We split them into equally sized groups, one per worker.
640+
parallelWithHooks: TestGroup,
641+
}>>();
634642

635643
const createGroup = (test: TestCase): TestGroup => {
636644
return {
@@ -654,18 +662,26 @@ function createTestGroups(rootSuite: Suite): TestGroup[] {
654662
withRequireFile = {
655663
general: createGroup(test),
656664
parallel: [],
665+
parallelWithHooks: createGroup(test),
657666
};
658667
withWorkerHash.set(test._requireFile, withRequireFile);
659668
}
660669

661670
let insideParallel = false;
662-
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent)
671+
let hasAllHooks = false;
672+
for (let parent: Suite | undefined = test.parent; parent; parent = parent.parent) {
663673
insideParallel = insideParallel || parent._parallelMode === 'parallel';
674+
hasAllHooks = hasAllHooks || parent.hooks.length > 0;
675+
}
664676

665677
if (insideParallel) {
666-
const group = createGroup(test);
667-
group.tests.push(test);
668-
withRequireFile.parallel.push(group);
678+
if (hasAllHooks) {
679+
withRequireFile.parallelWithHooks.tests.push(test);
680+
} else {
681+
const group = createGroup(test);
682+
group.tests.push(test);
683+
withRequireFile.parallel.push(group);
684+
}
669685
} else {
670686
withRequireFile.general.tests.push(test);
671687
}
@@ -678,6 +694,16 @@ function createTestGroups(rootSuite: Suite): TestGroup[] {
678694
if (withRequireFile.general.tests.length)
679695
result.push(withRequireFile.general);
680696
result.push(...withRequireFile.parallel);
697+
698+
const parallelWithHooksGroupSize = Math.ceil(withRequireFile.parallelWithHooks.tests.length / workers);
699+
let lastGroup: TestGroup | undefined;
700+
for (const test of withRequireFile.parallelWithHooks.tests) {
701+
if (!lastGroup || lastGroup.tests.length >= parallelWithHooksGroupSize) {
702+
lastGroup = createGroup(test);
703+
result.push(lastGroup);
704+
}
705+
lastGroup.tests.push(test);
706+
}
681707
}
682708
}
683709
return result;

tests/playwright-test/test-parallel.spec.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { test, expect } from './playwright-test-fixtures';
17+
import { test, expect, countTimes, stripAnsi } from './playwright-test-fixtures';
1818

1919
test('test.describe.parallel should throw inside test.describe.serial', async ({ runInlineTest }) => {
2020
const result = await runInlineTest({
@@ -176,3 +176,49 @@ test('project.fullyParallel should work', async ({ runInlineTest }) => {
176176
expect(result.output).toContain('%% worker=1');
177177
expect(result.output).toContain('%% worker=2');
178178
});
179+
180+
test('parallel mode should minimize running beforeAll/afterAll hooks', async ({ runInlineTest }) => {
181+
const result = await runInlineTest({
182+
'a.test.ts': `
183+
const { test } = pwt;
184+
test.describe.configure({ mode: 'parallel' });
185+
test.beforeAll(() => {
186+
console.log('\\n%%beforeAll');
187+
});
188+
test.afterAll(() => {
189+
console.log('\\n%%afterAll');
190+
});
191+
test('test1', () => {});
192+
test('test2', () => {});
193+
test('test3', () => {});
194+
test('test4', () => {});
195+
`,
196+
}, { workers: 1 });
197+
expect(result.exitCode).toBe(0);
198+
expect(result.passed).toBe(4);
199+
expect(countTimes(stripAnsi(result.output), '%%beforeAll')).toBe(1);
200+
expect(countTimes(stripAnsi(result.output), '%%afterAll')).toBe(1);
201+
});
202+
203+
test('parallel mode should minimize running beforeAll/afterAll hooks 2', async ({ runInlineTest }) => {
204+
const result = await runInlineTest({
205+
'a.test.ts': `
206+
const { test } = pwt;
207+
test.describe.configure({ mode: 'parallel' });
208+
test.beforeAll(() => {
209+
console.log('\\n%%beforeAll');
210+
});
211+
test.afterAll(() => {
212+
console.log('\\n%%afterAll');
213+
});
214+
test('test1', () => {});
215+
test('test2', () => {});
216+
test('test3', () => {});
217+
test('test4', () => {});
218+
`,
219+
}, { workers: 2 });
220+
expect(result.exitCode).toBe(0);
221+
expect(result.passed).toBe(4);
222+
expect(countTimes(stripAnsi(result.output), '%%beforeAll')).toBe(2);
223+
expect(countTimes(stripAnsi(result.output), '%%afterAll')).toBe(2);
224+
});

0 commit comments

Comments
 (0)