Skip to content

Commit 7b48995

Browse files
fix: distribute test files to shards more evenly (#8288)
Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>
1 parent 7c16c9a commit 7b48995

File tree

8 files changed

+116
-6
lines changed

8 files changed

+116
-6
lines changed

packages/vitest/src/node/sequencers/BaseSequencer.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,7 @@ export class BaseSequencer implements TestSequencer {
1616
public async shard(files: TestSpecification[]): Promise<TestSpecification[]> {
1717
const { config } = this.ctx
1818
const { index, count } = config.shard!
19-
const shardSize = Math.ceil(files.length / count)
20-
const shardStart = shardSize * (index - 1)
21-
const shardEnd = shardSize * index
19+
const [shardStart, shardEnd] = this.calculateShardRange(files.length, index, count)
2220
return [...files]
2321
.map((spec) => {
2422
const fullPath = resolve(slash(config.root), slash(spec.moduleId))
@@ -68,4 +66,20 @@ export class BaseSequencer implements TestSequencer {
6866
return bState.duration - aState.duration
6967
})
7068
}
69+
70+
// Calculate distributed shard range [start, end] distributed equally
71+
private calculateShardRange(filesCount: number, index: number, count: number): [number, number] {
72+
const baseShardSize = Math.floor(filesCount / count)
73+
const remainderTestFilesCount = filesCount % count
74+
if (remainderTestFilesCount >= index) {
75+
const shardSize = baseShardSize + 1
76+
const shardStart = shardSize * (index - 1)
77+
const shardEnd = shardSize * index
78+
return [shardStart, shardEnd]
79+
}
80+
81+
const shardStart = remainderTestFilesCount * (baseShardSize + 1) + (index - remainderTestFilesCount - 1) * baseShardSize
82+
const shardEnd = shardStart + baseShardSize
83+
return [shardStart, shardEnd]
84+
}
7185
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test, expect } from 'vitest'
2+
3+
test('test file 1', () => {
4+
expect(1).toBe(1)
5+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test, expect } from 'vitest'
2+
3+
test('test file 2', () => {
4+
expect(2).toBe(2)
5+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test, expect } from 'vitest'
2+
3+
test('test file 3', () => {
4+
expect(3).toBe(3)
5+
})
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { test, expect } from 'vitest'
2+
3+
test('test file 4', () => {
4+
expect(4).toBe(4)
5+
})
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from 'vitest/config'
2+
3+
export default defineConfig({
4+
test: {
5+
include: ['test/**/*.test.js'],
6+
},
7+
})

test/config/test/shard.test.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import { basename } from 'pathe'
44
import { expect, test } from 'vitest'
55
import * as testUtils from '../../test-utils'
66

7-
function runVitest(config: TestUserConfig) {
8-
return testUtils.runVitest({ ...config, root: './fixtures/shard' })
7+
function runVitest(config: TestUserConfig, root = './fixtures/shard') {
8+
return testUtils.runVitest({ ...config, root })
99
}
1010

1111
function parsePaths(stdout: string) {
@@ -40,6 +40,52 @@ test('--shard=2/2', async () => {
4040
expect(paths).toEqual(['3.test.js'])
4141
})
4242

43+
test('--shard=1/3 should distribute files evenly', async () => {
44+
const { stdout } = await runVitest({ shard: '1/3' })
45+
46+
const paths = parsePaths(stdout)
47+
48+
// With 3 files and 3 shards, should get 1 file per shard
49+
expect(paths).toEqual(['1.test.js'])
50+
})
51+
52+
test('--shard=2/3 should distribute files evenly', async () => {
53+
const { stdout } = await runVitest({ shard: '2/3' })
54+
55+
const paths = parsePaths(stdout)
56+
57+
// With 3 files and 3 shards, should get 1 file per shard
58+
expect(paths).toEqual(['2.test.js'])
59+
})
60+
61+
test('--shard=3/3 should distribute files evenly', async () => {
62+
const { stdout } = await runVitest({ shard: '3/3' })
63+
64+
const paths = parsePaths(stdout)
65+
66+
// With 3 files and 3 shards, should get 1 file per shard
67+
expect(paths).toEqual(['3.test.js'])
68+
})
69+
70+
test('4 files with 3 shards should distribute evenly', async () => {
71+
const { stdout: stdout1 } = await runVitest({ shard: '1/3' }, './fixtures/shard-4-files')
72+
const { stdout: stdout2 } = await runVitest({ shard: '2/3' }, './fixtures/shard-4-files')
73+
const { stdout: stdout3 } = await runVitest({ shard: '3/3' }, './fixtures/shard-4-files')
74+
75+
const paths1 = parsePaths(stdout1)
76+
const paths2 = parsePaths(stdout2)
77+
const paths3 = parsePaths(stdout3)
78+
79+
// Should distribute files more evenly: [2,1,1] instead of [2,2,0]
80+
expect(paths1.length).toBe(2)
81+
expect(paths2.length).toBe(1)
82+
expect(paths3.length).toBe(1)
83+
84+
// All files should be covered exactly once
85+
const allFiles = [...paths1, ...paths2, ...paths3].sort()
86+
expect(allFiles).toEqual(['1.test.js', '2.test.js', '3.test.js', '4.test.js'])
87+
})
88+
4389
test('--shard=4/4', async () => {
4490
const { stdout } = await runVitest({ shard: '4/4' })
4591

test/core/test/sequencers.test.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ import { BaseSequencer } from '../../../packages/vitest/src/node/sequencers/Base
55
import { RandomSequencer } from '../../../packages/vitest/src/node/sequencers/RandomSequencer'
66
import { TestSpecification } from '../../../packages/vitest/src/node/spec'
77

8-
function buildCtx() {
8+
function buildCtx(config?: Partial<Vitest['config']>) {
99
return {
1010
config: {
1111
sequence: {},
12+
...config,
1213
},
1314
cache: {
1415
getFileTestResults: vi.fn(),
@@ -128,6 +129,28 @@ describe('base sequencer', () => {
128129
const sorted = await sequencer.sort(files)
129130
expect(sorted).toStrictEqual(workspaced(['c', 'b', 'a']))
130131
})
132+
133+
test.each([
134+
{ files: 4, count: 3, expected: [2, 1, 1] },
135+
{ files: 5, count: 4, expected: [2, 1, 1, 1] },
136+
{ files: 9, count: 4, expected: [3, 2, 2, 2] },
137+
])('shard x/$count distributes $files files as $expected', async ({ count, files, expected }) => {
138+
const specs = Array.from({ length: files }, (_, id) => ({ moduleId: `file-${id}.test.ts` } as TestSpecification))
139+
const slices = []
140+
141+
for (const index of Array.from({ length: count }).keys()) {
142+
const ctx = buildCtx({ root: '/example/root', shard: { index: 1 + index, count } })
143+
const sequencer = new BaseSequencer(ctx)
144+
const shard = await sequencer.shard(specs)
145+
146+
slices.push(shard.length)
147+
}
148+
149+
expect(slices).toEqual(expected)
150+
151+
const sum = slices.reduce((total, current) => total + current, 0)
152+
expect(sum).toBe(files)
153+
})
131154
})
132155

133156
describe('random sequencer', () => {

0 commit comments

Comments
 (0)