Skip to content

Commit 222ce44

Browse files
authored
feat!(runner): support concurrent suites (#5491)
1 parent e20538a commit 222ce44

File tree

10 files changed

+284
-7
lines changed

10 files changed

+284
-7
lines changed

docs/api/index.md

+6-3
Original file line numberDiff line numberDiff line change
@@ -684,15 +684,18 @@ In order to do that run `vitest` with specific file containing the tests in ques
684684

685685
- **Alias:** `suite.concurrent`
686686

687-
`describe.concurrent` in a suite marks every tests as concurrent
687+
`describe.concurrent` runs all inner suites and tests in parallel
688688

689689
```ts twoslash
690690
import { describe, test } from 'vitest'
691691
// ---cut---
692-
// All tests within this suite will be run in parallel
692+
// All suites and tests within this suite will be run in parallel
693693
describe.concurrent('suite', () => {
694694
test('concurrent test 1', async () => { /* ... */ })
695-
test('concurrent test 2', async () => { /* ... */ })
695+
describe('concurrent suite 2', async () => {
696+
test('concurrent test inner 1', async () => { /* ... */ })
697+
test('concurrent test inner 2', async () => { /* ... */ })
698+
})
696699
test.concurrent('concurrent test 3', async () => { /* ... */ })
697700
})
698701
```

packages/runner/src/run.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,7 @@ export async function runSuite(suite: Suite, runner: VitestRunner) {
332332
else {
333333
for (let tasksGroup of partitionSuiteChildren(suite)) {
334334
if (tasksGroup[0].concurrent === true) {
335-
const mutex = limit(runner.config.maxConcurrency)
336-
await Promise.all(tasksGroup.map(c => mutex(() => runSuiteChild(c, runner))))
335+
await Promise.all(tasksGroup.map(c => runSuiteChild(c, runner)))
337336
}
338337
else {
339338
const { sequence } = runner.config
@@ -386,15 +385,19 @@ export async function runSuite(suite: Suite, runner: VitestRunner) {
386385
}
387386
}
388387

388+
let limitMaxConcurrency: ReturnType<typeof limit>
389+
389390
async function runSuiteChild(c: Task, runner: VitestRunner) {
390391
if (c.type === 'test' || c.type === 'custom')
391-
return runTest(c, runner)
392+
return limitMaxConcurrency(() => runTest(c, runner))
392393

393394
else if (c.type === 'suite')
394395
return runSuite(c, runner)
395396
}
396397

397398
export async function runFiles(files: File[], runner: VitestRunner) {
399+
limitMaxConcurrency ??= limit(runner.config.maxConcurrency)
400+
398401
for (const file of files) {
399402
if (!file.tasks.length && !runner.config.passWithNoTests) {
400403
if (!file.result?.errors?.length) {

packages/runner/src/suite.ts

+1
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ function createSuiteCollector(name: string, factory: SuiteFactory = () => { }, m
222222
shuffle,
223223
tasks: [],
224224
meta: Object.create(null),
225+
concurrent: suiteOptions?.concurrent,
225226
}
226227

227228
if (runner && includeLocation && runner.config.includeTaskLocation) {

packages/runner/src/types/tasks.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export interface TestOptions {
180180
*/
181181
repeats?: number
182182
/**
183-
* Whether tests run concurrently.
183+
* Whether suites and tests run concurrently.
184184
* Tests inherit `concurrent` from `describe()` and nested `describe()` will inherit from parent's `concurrent`.
185185
*/
186186
concurrent?: boolean

pnpm-lock.yaml

+3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { createDefer } from '@vitest/utils'
2+
import { describe, test, vi } from 'vitest'
3+
4+
// 3 tests depend on each other,
5+
// so they will deadlock when maxConcurrency < 3
6+
//
7+
// [a] [b] [c]
8+
// * ->
9+
// * ->
10+
// <- *
11+
// <------
12+
13+
vi.setConfig({ maxConcurrency: 2 })
14+
15+
describe('wrapper', { concurrent: true, timeout: 500 }, () => {
16+
const defers = [
17+
createDefer<void>(),
18+
createDefer<void>(),
19+
createDefer<void>(),
20+
]
21+
22+
describe('1st suite', () => {
23+
test('a', async () => {
24+
defers[0].resolve()
25+
await defers[2]
26+
})
27+
28+
test('b', async () => {
29+
await defers[0]
30+
defers[1].resolve()
31+
await defers[2]
32+
})
33+
})
34+
35+
describe('2nd suite', () => {
36+
test('c', async () => {
37+
await defers[1]
38+
defers[2].resolve()
39+
})
40+
})
41+
})
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { describe, test, vi } from 'vitest'
2+
import { createDefer } from '@vitest/utils'
3+
4+
// 3 tests depend on each other,
5+
// so they will deadlock when maxConcurrency < 3
6+
//
7+
// [a] [b] [c]
8+
// * ->
9+
// * ->
10+
// <- *
11+
// <------
12+
13+
vi.setConfig({ maxConcurrency: 2 })
14+
15+
describe('wrapper', { concurrent: true, timeout: 500 }, () => {
16+
const defers = [
17+
createDefer<void>(),
18+
createDefer<void>(),
19+
createDefer<void>(),
20+
]
21+
22+
test('a', async () => {
23+
defers[0].resolve()
24+
await defers[2]
25+
})
26+
27+
test('b', async () => {
28+
await defers[0]
29+
defers[1].resolve()
30+
await defers[2]
31+
})
32+
33+
test('c', async () => {
34+
await defers[1]
35+
defers[2].resolve()
36+
})
37+
})

test/cli/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"@types/ws": "^8.5.9",
1111
"@vitejs/plugin-basic-ssl": "^1.0.2",
1212
"@vitest/runner": "workspace:^",
13+
"@vitest/utils": "workspace:*",
1314
"debug": "^4.3.4",
1415
"execa": "^8.0.1",
1516
"unplugin-swc": "^1.4.4",

test/cli/test/__snapshots__/fails.test.ts.snap

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
exports[`should fail .dot-folder/dot-test.test.ts > .dot-folder/dot-test.test.ts 1`] = `"AssertionError: expected true to be false // Object.is equality"`;
44

5+
exports[`should fail concurrent-suite-deadlock.test.ts > concurrent-suite-deadlock.test.ts 1`] = `"Error: Test timed out in 500ms."`;
6+
7+
exports[`should fail concurrent-test-deadlock.test.ts > concurrent-test-deadlock.test.ts 1`] = `"Error: Test timed out in 500ms."`;
8+
59
exports[`should fail each-timeout.test.ts > each-timeout.test.ts 1`] = `"Error: Test timed out in 10ms."`;
610

711
exports[`should fail empty.test.ts > empty.test.ts 1`] = `"Error: No test suite found in file <rootDir>/empty.test.ts"`;
+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { createDefer } from '@vitest/utils'
2+
import { afterAll, describe, expect, test } from 'vitest'
3+
4+
describe('basic', () => {
5+
const defers = [
6+
createDefer<void>(),
7+
createDefer<void>(),
8+
createDefer<void>(),
9+
createDefer<void>(),
10+
]
11+
12+
afterAll(async () => {
13+
await defers[3]
14+
})
15+
16+
describe('1st suite', { concurrent: true }, () => {
17+
test('0', async () => {
18+
defers[0].resolve()
19+
})
20+
21+
test('1', async () => {
22+
await defers[2] // this would deadlock if sequential
23+
defers[1].resolve()
24+
})
25+
})
26+
27+
describe('2nd suite', { concurrent: true }, () => {
28+
test('2', async () => {
29+
await defers[0]
30+
defers[2].resolve()
31+
})
32+
test('3', async () => {
33+
await defers[1]
34+
defers[3].resolve()
35+
})
36+
})
37+
})
38+
39+
describe('inherits option', { concurrent: true }, () => {
40+
const defers = [
41+
createDefer<void>(),
42+
createDefer<void>(),
43+
createDefer<void>(),
44+
createDefer<void>(),
45+
]
46+
47+
afterAll(async () => {
48+
await defers[3]
49+
})
50+
51+
describe('1st suite', () => {
52+
test('0', async () => {
53+
defers[0].resolve()
54+
})
55+
56+
test('1', async () => {
57+
await defers[2] // this would deadlock if sequential
58+
defers[1].resolve()
59+
})
60+
})
61+
62+
describe('2nd suite', () => {
63+
test('2', async () => {
64+
await defers[0]
65+
defers[2].resolve()
66+
})
67+
test('3', async () => {
68+
await defers[1]
69+
defers[3].resolve()
70+
})
71+
})
72+
})
73+
74+
describe('works with describe.each', () => {
75+
const defers = [
76+
createDefer<void>(),
77+
createDefer<void>(),
78+
createDefer<void>(),
79+
createDefer<void>(),
80+
]
81+
82+
afterAll(async () => {
83+
await defers[3]
84+
})
85+
86+
describe.each(['1st suite', '2nd suite'])('%s', { concurrent: true }, (s) => {
87+
if (s === '1st suite') {
88+
test('0', async () => {
89+
defers[0].resolve()
90+
})
91+
92+
test('1', async () => {
93+
await defers[2] // this would deadlock if sequential
94+
defers[1].resolve()
95+
})
96+
}
97+
98+
if (s === '2nd suite') {
99+
test('2', async () => {
100+
await defers[0]
101+
defers[2].resolve()
102+
})
103+
test('3', async () => {
104+
await defers[1]
105+
defers[3].resolve()
106+
})
107+
}
108+
})
109+
})
110+
111+
describe('override concurrent', { concurrent: true }, () => {
112+
checkParallelSuites()
113+
114+
describe('s-x', { concurrent: false }, () => {
115+
checkSequentialTests()
116+
})
117+
118+
describe.sequential('s-x-1', () => {
119+
checkSequentialTests()
120+
})
121+
122+
// TODO: not working?
123+
// describe('s-x-2', { sequential: true, }, () => {
124+
// checkSequentialTests()
125+
// })
126+
127+
describe('s-y', () => {
128+
checkParallelTests()
129+
})
130+
})
131+
132+
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
133+
134+
function checkSequentialTests() {
135+
let x = 0
136+
137+
test('t1', async () => {
138+
await sleep(200)
139+
expect(x).toBe(0)
140+
x++
141+
})
142+
143+
test('t2', async () => {
144+
expect(x).toBe(1)
145+
})
146+
}
147+
148+
function checkParallelTests() {
149+
const defers = [
150+
createDefer<void>(),
151+
createDefer<void>(),
152+
]
153+
154+
test('t1', async () => {
155+
defers[0].resolve()
156+
await defers[1]
157+
})
158+
159+
test('t2', async () => {
160+
await defers[0]
161+
defers[1].resolve()
162+
})
163+
}
164+
165+
function checkParallelSuites() {
166+
const defers = [
167+
createDefer<void>(),
168+
createDefer<void>(),
169+
]
170+
171+
describe('s1', () => {
172+
test('t1', async () => {
173+
defers[0].resolve()
174+
await defers[1]
175+
})
176+
})
177+
178+
describe('s2', () => {
179+
test('t1', async () => {
180+
await defers[0]
181+
defers[1].resolve()
182+
})
183+
})
184+
}

0 commit comments

Comments
 (0)