Skip to content

Commit d1a1df0

Browse files
authored
feat: add sequence.groupOrder option (#7852)
1 parent 05b7dd9 commit d1a1df0

File tree

7 files changed

+210
-43
lines changed

7 files changed

+210
-43
lines changed

docs/config/index.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2034,7 +2034,7 @@ export default defineConfig({
20342034

20352035
### sequence
20362036

2037-
- **Type**: `{ sequencer?, shuffle?, seed?, hooks?, setupFiles? }`
2037+
- **Type**: `{ sequencer?, shuffle?, seed?, hooks?, setupFiles?, groupOrder }`
20382038

20392039
Options for how tests should be sorted.
20402040

@@ -2053,6 +2053,71 @@ A custom class that defines methods for sharding and sorting. You can extend `Ba
20532053

20542054
Sharding is happening before sorting, and only if `--shard` option is provided.
20552055

2056+
If [`sequencer.groupOrder`](#groupOrder) is specified, the sequencer will be called once for each group and pool.
2057+
2058+
#### groupOrder <Version>3.2.0</Version> {#groupOrder}
2059+
2060+
- **Type:** `number`
2061+
- **Default:** `0`
2062+
2063+
Controls the order in which this project runs its tests when using multiple [projects](/guide/projects).
2064+
2065+
- Projects with the same group order number will run together, and groups are run from lowest to highest.
2066+
- If you don’t set this option, all projects run in parallel.
2067+
- If several projects use the same group order, they will run at the same time.
2068+
2069+
This setting only affects the order in which projects run, not the order of tests within a project.
2070+
To control test isolation or the order of tests inside a project, use the [`isolate`](#isolate) and [`sequence.sequencer`](#sequence-sequencer) options.
2071+
2072+
::: details Example
2073+
Consider this example:
2074+
2075+
```ts
2076+
import { defineConfig } from 'vitest/config'
2077+
2078+
export default defineConfig({
2079+
test: {
2080+
projects: [
2081+
{
2082+
test: {
2083+
name: 'slow',
2084+
sequence: {
2085+
groupOrder: 0,
2086+
},
2087+
},
2088+
},
2089+
{
2090+
test: {
2091+
name: 'fast',
2092+
sequence: {
2093+
groupOrder: 0,
2094+
},
2095+
},
2096+
},
2097+
{
2098+
test: {
2099+
name: 'flaky',
2100+
sequence: {
2101+
groupOrder: 1,
2102+
},
2103+
},
2104+
},
2105+
],
2106+
},
2107+
})
2108+
```
2109+
2110+
Tests in these projects will run in this order:
2111+
2112+
```
2113+
0. slow |
2114+
|> running together
2115+
0. fast |
2116+
2117+
1. flaky |> runs after slow and fast alone
2118+
```
2119+
:::
2120+
20562121
#### sequence.shuffle
20572122

20582123
- **Type**: `boolean | { files?, tests? }`

docs/guide/cli-generated.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,9 +89,7 @@ Hide logs for skipped tests
8989
- **CLI:** `--reporter <name>`
9090
- **Config:** [reporters](/config/#reporters)
9191

92-
Specify which reporter(s) to use. Available values:
93-
94-
`default`, `basic`, `blob`, `verbose`, `dot`, `json`, `tap`, `tap-flat`, `junit`, `hanging-process`, `github-actions`.
92+
Specify reporters (default, basic, blob, verbose, dot, json, tap, tap-flat, junit, hanging-process, github-actions)
9593

9694
### outputFile
9795

packages/vitest/src/node/cli/cli-config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
549549
'Changes the order in which setup files are executed. Accepted values are: "list" and "parallel". If set to "list", will run setup files in the order they are defined. If set to "parallel", will run setup files in parallel (default: `"parallel"`)',
550550
argument: '<order>',
551551
},
552+
groupOrder: null,
552553
},
553554
},
554555
inspect: {

packages/vitest/src/node/config/resolveConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -772,6 +772,7 @@ export function resolveConfig(
772772
? RandomSequencer
773773
: BaseSequencer
774774
}
775+
resolved.sequence.groupOrder ??= 0
775776
resolved.sequence.hooks ??= 'stack'
776777
if (resolved.sequence.sequencer === RandomSequencer) {
777778
resolved.sequence.seed ??= Date.now()

packages/vitest/src/node/pool.ts

Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ export function createPool(ctx: Vitest): ProcessPool {
148148
}
149149
}
150150

151+
const poolConcurrentPromises = new Map<string, Promise<ProcessPool>>()
151152
const customPools = new Map<string, ProcessPool>()
152153
async function resolveCustomPool(filepath: string) {
153154
if (customPools.has(filepath)) {
@@ -178,26 +179,44 @@ export function createPool(ctx: Vitest): ProcessPool {
178179
return poolInstance as ProcessPool
179180
}
180181

181-
const filesByPool: Record<LocalPool, TestSpecification[]> = {
182-
forks: [],
183-
threads: [],
184-
vmThreads: [],
185-
vmForks: [],
186-
typescript: [],
182+
function getConcurrentPool(pool: string, fn: () => Promise<ProcessPool>) {
183+
if (poolConcurrentPromises.has(pool)) {
184+
return poolConcurrentPromises.get(pool)!
185+
}
186+
const promise = fn().finally(() => {
187+
poolConcurrentPromises.delete(pool)
188+
})
189+
poolConcurrentPromises.set(pool, promise)
190+
return promise
191+
}
192+
193+
function getCustomPool(pool: string) {
194+
return getConcurrentPool(pool, () => resolveCustomPool(pool))
187195
}
188196

197+
function getBrowserPool() {
198+
return getConcurrentPool('browser', async () => {
199+
const { createBrowserPool } = await import('@vitest/browser')
200+
return createBrowserPool(ctx)
201+
})
202+
}
203+
204+
const groupedSpecifications: Record<string, TestSpecification[]> = {}
205+
const groups = new Set<number>()
206+
189207
const factories: Record<LocalPool, () => ProcessPool> = {
190208
vmThreads: () => createVmThreadsPool(ctx, options),
209+
vmForks: () => createVmForksPool(ctx, options),
191210
threads: () => createThreadsPool(ctx, options),
192211
forks: () => createForksPool(ctx, options),
193-
vmForks: () => createVmForksPool(ctx, options),
194212
typescript: () => createTypecheckPool(ctx),
195213
}
196214

197215
for (const spec of files) {
198-
const { pool } = spec[2]
199-
filesByPool[pool] ??= []
200-
filesByPool[pool].push(spec)
216+
const group = spec[0].config.sequence.groupOrder ?? 0
217+
groups.add(group)
218+
groupedSpecifications[group] ??= []
219+
groupedSpecifications[group].push(spec)
201220
}
202221

203222
const Sequencer = ctx.config.sequence.sequencer
@@ -210,35 +229,55 @@ export function createPool(ctx: Vitest): ProcessPool {
210229
return sequencer.sort(specs)
211230
}
212231

213-
await Promise.all(
214-
Object.entries(filesByPool).map(async (entry) => {
215-
const [pool, files] = entry as [Pool, TestSpecification[]]
216-
217-
if (!files.length) {
218-
return null
219-
}
220-
221-
const specs = await sortSpecs(files)
222-
223-
if (pool in factories) {
224-
const factory = factories[pool]
225-
pools[pool] ??= factory()
226-
return pools[pool]![method](specs, invalidate)
227-
}
228-
229-
if (pool === 'browser') {
230-
pools[pool] ??= await (async () => {
231-
const { createBrowserPool } = await import('@vitest/browser')
232-
return createBrowserPool(ctx)
233-
})()
234-
return pools[pool]![method](specs, invalidate)
235-
}
236-
237-
const poolHandler = await resolveCustomPool(pool)
238-
pools[poolHandler.name] ??= poolHandler
239-
return poolHandler[method](specs, invalidate)
240-
}),
241-
)
232+
const sortedGroups = Array.from(groups).sort()
233+
for (const group of sortedGroups) {
234+
const specifications = groupedSpecifications[group]
235+
236+
if (!specifications?.length) {
237+
continue
238+
}
239+
240+
const filesByPool: Record<LocalPool, TestSpecification[]> = {
241+
forks: [],
242+
threads: [],
243+
vmThreads: [],
244+
vmForks: [],
245+
typescript: [],
246+
}
247+
248+
specifications.forEach((specification) => {
249+
const pool = specification[2].pool
250+
filesByPool[pool] ??= []
251+
filesByPool[pool].push(specification)
252+
})
253+
254+
await Promise.all(
255+
Object.entries(filesByPool).map(async (entry) => {
256+
const [pool, files] = entry as [Pool, TestSpecification[]]
257+
258+
if (!files.length) {
259+
return null
260+
}
261+
262+
const specs = await sortSpecs(files)
263+
264+
if (pool in factories) {
265+
const factory = factories[pool]
266+
pools[pool] ??= factory()
267+
return pools[pool]![method](specs, invalidate)
268+
}
269+
270+
if (pool === 'browser') {
271+
pools.browser ??= await getBrowserPool()
272+
return pools.browser[method](specs, invalidate)
273+
}
274+
275+
const poolHandler = await getCustomPool(pool)
276+
pools[poolHandler.name] ??= poolHandler
277+
return poolHandler[method](specs, invalidate)
278+
}),
279+
)
280+
}
242281
}
243282

244283
return {

packages/vitest/src/node/types/config.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ interface SequenceOptions {
6464
* @default BaseSequencer
6565
*/
6666
sequencer?: TestSequencerConstructor
67+
/**
68+
* Controls the order in which this project runs its tests when using multiple [projects](/guide/projects).
69+
*
70+
* - Projects with the same group order number will run together, and groups are run from lowest to highest.
71+
* - If you don’t set this option, all projects run in parallel.
72+
* - If several projects use the same group order, they will run at the same time.
73+
* @default 0
74+
*/
75+
groupOrder?: number
6776
/**
6877
* Should files and tests run in random order.
6978
* @default false
@@ -1066,6 +1075,7 @@ export interface ResolvedConfig
10661075
shuffle?: boolean
10671076
concurrent?: boolean
10681077
seed: number
1078+
groupOrder: number
10691079
}
10701080

10711081
typecheck: Omit<TypecheckConfig, 'enabled'> & {

test/cli/test/group-order.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { expect, test } from 'vitest'
2+
import { runInlineTests } from '../../test-utils'
3+
4+
test('tests run according to the group order', async () => {
5+
const { stdout, stderr } = await runInlineTests({
6+
'example.1.test.ts': `test('1')`,
7+
'example.2.test.ts': `test('2')`,
8+
'example.2-2.test.ts': `test('2-2')`,
9+
'example.3.test.ts': `test('3')`,
10+
}, {
11+
globals: true,
12+
// run projects in the opposite order!
13+
projects: [
14+
{
15+
test: {
16+
name: '3',
17+
include: ['./example.3.test.ts'],
18+
sequence: {
19+
groupOrder: 1,
20+
},
21+
},
22+
},
23+
{
24+
test: {
25+
include: ['./example.2.test.ts', './example.2-2.test.ts'],
26+
name: '2',
27+
sequence: {
28+
groupOrder: 2,
29+
},
30+
},
31+
},
32+
{
33+
test: {
34+
name: '1',
35+
include: ['./example.1.test.ts'],
36+
sequence: {
37+
groupOrder: 3,
38+
},
39+
},
40+
},
41+
],
42+
})
43+
expect(stderr).toBe('')
44+
45+
const tests = stdout.split('\n').filter(c => c.startsWith(' ✓')).join('\n').replace(/\d+ms/g, '<time>')
46+
47+
expect(tests).toMatchInlineSnapshot(`
48+
" ✓ |3| example.3.test.ts > 3 <time>
49+
✓ |2| example.2-2.test.ts > 2-2 <time>
50+
✓ |2| example.2.test.ts > 2 <time>
51+
✓ |1| example.1.test.ts > 1 <time>"
52+
`)
53+
})

0 commit comments

Comments
 (0)