[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat!(runner): support concurrent suites (#5491)

authored by

Hiroshi Ogawa and committed by
GitHub
(May 14, 2024, 7:14 PM +0200) 222ce441 e20538a3

+284 -7
+3
pnpm-lock.yaml
··· 1091 1091 '@vitest/runner': 1092 1092 specifier: workspace:^ 1093 1093 version: link:../../packages/runner 1094 + '@vitest/utils': 1095 + specifier: workspace:* 1096 + version: link:../../packages/utils 1094 1097 debug: 1095 1098 specifier: ^4.3.4 1096 1099 version: 4.3.4(supports-color@8.1.1)
+6 -3
docs/api/index.md
··· 684 684 685 685 - **Alias:** `suite.concurrent` 686 686 687 - `describe.concurrent` in a suite marks every tests as concurrent 687 + `describe.concurrent` runs all inner suites and tests in parallel 688 688 689 689 ```ts twoslash 690 690 import { describe, test } from 'vitest' 691 691 // ---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 693 693 describe.concurrent('suite', () => { 694 694 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 + }) 696 699 test.concurrent('concurrent test 3', async () => { /* ... */ }) 697 700 }) 698 701 ```
+1
test/cli/package.json
··· 10 10 "@types/ws": "^8.5.9", 11 11 "@vitejs/plugin-basic-ssl": "^1.0.2", 12 12 "@vitest/runner": "workspace:^", 13 + "@vitest/utils": "workspace:*", 13 14 "debug": "^4.3.4", 14 15 "execa": "^8.0.1", 15 16 "unplugin-swc": "^1.4.4",
+6 -3
packages/runner/src/run.ts
··· 332 332 else { 333 333 for (let tasksGroup of partitionSuiteChildren(suite)) { 334 334 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))) 337 336 } 338 337 else { 339 338 const { sequence } = runner.config ··· 386 385 } 387 386 } 388 387 388 + let limitMaxConcurrency: ReturnType<typeof limit> 389 + 389 390 async function runSuiteChild(c: Task, runner: VitestRunner) { 390 391 if (c.type === 'test' || c.type === 'custom') 391 - return runTest(c, runner) 392 + return limitMaxConcurrency(() => runTest(c, runner)) 392 393 393 394 else if (c.type === 'suite') 394 395 return runSuite(c, runner) 395 396 } 396 397 397 398 export async function runFiles(files: File[], runner: VitestRunner) { 399 + limitMaxConcurrency ??= limit(runner.config.maxConcurrency) 400 + 398 401 for (const file of files) { 399 402 if (!file.tasks.length && !runner.config.passWithNoTests) { 400 403 if (!file.result?.errors?.length) {
+1
packages/runner/src/suite.ts
··· 222 222 shuffle, 223 223 tasks: [], 224 224 meta: Object.create(null), 225 + concurrent: suiteOptions?.concurrent, 225 226 } 226 227 227 228 if (runner && includeLocation && runner.config.includeTaskLocation) {
+184
test/core/test/concurrent-suite.test.ts
··· 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 + }
+1 -1
packages/runner/src/types/tasks.ts
··· 180 180 */ 181 181 repeats?: number 182 182 /** 183 - * Whether tests run concurrently. 183 + * Whether suites and tests run concurrently. 184 184 * Tests inherit `concurrent` from `describe()` and nested `describe()` will inherit from parent's `concurrent`. 185 185 */ 186 186 concurrent?: boolean
+41
test/cli/fixtures/fails/concurrent-suite-deadlock.test.ts
··· 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 + })
+37
test/cli/fixtures/fails/concurrent-test-deadlock.test.ts
··· 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 + })
+4
test/cli/test/__snapshots__/fails.test.ts.snap
··· 2 2 3 3 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"`; 4 4 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 + 5 9 exports[`should fail each-timeout.test.ts > each-timeout.test.ts 1`] = `"Error: Test timed out in 10ms."`; 6 10 7 11 exports[`should fail empty.test.ts > empty.test.ts 1`] = `"Error: No test suite found in file <rootDir>/empty.test.ts"`;