[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.

fix: hooks should respect `maxConcurrency` (#9653)

authored by

Hiroshi Ogawa and committed by
GitHub
(Feb 18, 2026, 9:43 AM +0900) 16d13d98 bb20389f

+1338 -151
+2 -2
docs/config/maxconcurrency.md
··· 9 9 - **Default**: `5` 10 10 - **CLI**: `--max-concurrency=10`, `--maxConcurrency=10` 11 11 12 - A number of tests that are allowed to run at the same time marked with `test.concurrent`. 12 + The maximum number of tests and hooks that can run at the same time when using `test.concurrent` or `describe.concurrent`. 13 13 14 - Test above this limit will be queued to run when available slot appears. 14 + The hook execution order within a single group is also controlled by [`sequence.hooks`](/config/sequence#sequence-hooks). With `sequence.hooks: 'parallel'`, the execution is bounded by the same limit of [`maxConcurrency`](/config/maxconcurrency).
+1 -1
docs/config/sequence.md
··· 145 145 146 146 - `stack` will order "after" hooks in reverse order, "before" hooks will run in the order they were defined 147 147 - `list` will order all hooks in the order they are defined 148 - - `parallel` will run hooks in a single group in parallel (hooks in parent suites will still run before the current suite's hooks) 148 + - `parallel` runs hooks in a single group in parallel (hooks in parent suites still run before the current suite's hooks). The actual number of simultaneously running hooks is limited by [`maxConcurrency`](/config/maxconcurrency). 149 149 150 150 ::: tip 151 151 This option doesn't affect [`onTestFinished`](/api/hooks#ontestfinished). It is always called in reverse order.
+1 -1
docs/guide/cli-generated.md
··· 781 781 - **CLI:** `--maxConcurrency <number>` 782 782 - **Config:** [maxConcurrency](/config/maxconcurrency) 783 783 784 - Maximum number of concurrent tests in a suite (default: `5`) 784 + Maximum number of concurrent tests and suites during test file execution (default: `5`) 785 785 786 786 ### expect.requireAssertions 787 787
+2
docs/guide/parallelism.md
··· 22 22 23 23 Vitest supports the [`concurrent`](/api/test#test-concurrent) option to run tests together. If this option is set, Vitest will group concurrent tests in the same _file_ (the number of simultaneously running tests depends on the [`maxConcurrency`](/config/maxconcurrency) option) and run them with [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all). 24 24 25 + The hook execution order within a single group is also controlled by [`sequence.hooks`](/config/sequence#sequence-hooks). With `sequence.hooks: 'parallel'`, the execution is bounded by the same limit of [`maxConcurrency`](/config/maxconcurrency). 26 + 25 27 Vitest doesn't perform any smart analysis and doesn't create additional workers to run these tests. This means that the performance of your tests will improve only if you rely heavily on asynchronous operations. For example, these tests will still run one after another even though the `concurrent` option is specified. This is because they are synchronous: 26 28 27 29 ```ts
+32 -13
test/test-utils/index.ts
··· 1 1 import type { Options } from 'tinyexec' 2 2 import type { UserConfig as ViteUserConfig } from 'vite' 3 - import type { SerializedConfig, WorkerGlobalState } from 'vitest' 3 + import type { SerializedConfig, TestContext, WorkerGlobalState } from 'vitest' 4 4 import type { TestProjectConfiguration } from 'vitest/config' 5 5 import type { 6 6 TestCase, ··· 8 8 TestCollection, 9 9 TestModule, 10 10 TestSpecification, 11 + TestSuite, 11 12 TestUserConfig, 12 13 Vitest, 13 14 } from 'vitest/node' ··· 423 424 return `export default ${JSON.stringify(content)}` 424 425 } 425 426 426 - export function useFS<T extends TestFsStructure>(root: string, structure: T, ensureConfig = true) { 427 + export function useFS<T extends TestFsStructure>(root: string, structure: T, ensureConfig = true, task?: TestContext['task']) { 427 428 const files = new Set<string>() 428 429 const hasConfig = Object.keys(structure).some(file => file.includes('.config.')) 429 430 if (ensureConfig && !hasConfig) { ··· 436 437 fs.mkdirSync(dirname(filepath), { recursive: true }) 437 438 fs.writeFileSync(filepath, String(content), 'utf-8') 438 439 } 439 - onTestFinished(() => { 440 + (task?.context.onTestFinished ?? onTestFinished)(() => { 440 441 if (process.env.VITEST_FS_CLEANUP !== 'false') { 441 442 fs.rmSync(root, { recursive: true, force: true }) 442 443 } ··· 493 494 structure: TestFsStructure, 494 495 config?: RunVitestConfig, 495 496 options?: VitestRunnerCLIOptions, 497 + task?: TestContext['task'], 496 498 ) { 497 499 const root = resolve(process.cwd(), `vitest-test-${crypto.randomUUID()}`) 498 - const fs = useFS(root, structure) 500 + const fs = useFS(root, structure, undefined, task) 499 501 const vitest = await runVitest({ 500 502 root, 501 503 ...config, ··· 554 556 } 555 557 556 558 export function buildErrorTree(testModules: TestModule[]) { 557 - return buildTestTree(testModules, (testCase) => { 558 - const result = testCase.result() 559 - if (result.state === 'failed') { 560 - return result.errors.map(e => e.message) 561 - } 562 - return result.state 563 - }) 559 + return buildTestTree( 560 + testModules, 561 + (testCase) => { 562 + const result = testCase.result() 563 + if (result.state === 'failed') { 564 + return result.errors.map(e => e.message) 565 + } 566 + return result.state 567 + }, 568 + (testSuite, suiteChildren) => { 569 + const errors = testSuite.errors().map(error => error.message) 570 + if (errors.length > 0) { 571 + return { 572 + ...suiteChildren, 573 + __suite_errors__: errors, 574 + } 575 + } 576 + return suiteChildren 577 + }, 578 + ) 564 579 } 565 580 566 - export function buildTestTree(testModules: TestModule[], onTestCase?: (result: TestCase) => unknown) { 581 + export function buildTestTree( 582 + testModules: TestModule[], 583 + onTestCase?: (result: TestCase) => unknown, 584 + onTestSuite?: (testSuite: TestSuite, suiteChildren: Record<string, any>) => unknown, 585 + ) { 567 586 type TestTree = Record<string, any> 568 587 569 588 function walkCollection(collection: TestCollection): TestTree { ··· 573 592 if (child.type === 'suite') { 574 593 // Recursively walk suite children 575 594 const suiteChildren = walkCollection(child.children) 576 - node[child.name] = suiteChildren 595 + node[child.name] = onTestSuite ? onTestSuite(child, suiteChildren) : suiteChildren 577 596 } 578 597 else if (child.type === 'test') { 579 598 const result = child.result()
+30 -15
packages/runner/src/run.ts
··· 18 18 TestContext, 19 19 WriteableTestContext, 20 20 } from './types/tasks' 21 + import type { ConcurrencyLimiter } from './utils/limit-concurrency' 21 22 import { processError } from '@vitest/utils/error' // TODO: load dynamically 22 23 import { shuffle } from '@vitest/utils/helpers' 23 24 import { getSafeTimers } from '@vitest/utils/timers' ··· 35 36 const now = globalThis.performance ? globalThis.performance.now.bind(globalThis.performance) : Date.now 36 37 const unixNow = Date.now 37 38 const { clearTimeout, setTimeout } = getSafeTimers() 39 + let limitMaxConcurrency: ConcurrencyLimiter 38 40 39 41 /** 40 42 * Normalizes retry configuration to extract individual values. ··· 141 143 142 144 if (sequence === 'parallel') { 143 145 try { 144 - await Promise.all(hooks.map(fn => fn(test.context))) 146 + await Promise.all(hooks.map(fn => limitMaxConcurrency(() => fn(test.context)))) 145 147 } 146 148 catch (e) { 147 149 failTask(test.result!, e, runner.config.diffOptions) ··· 150 152 else { 151 153 for (const fn of hooks) { 152 154 try { 153 - await fn(test.context) 155 + await limitMaxConcurrency(() => fn(test.context)) 154 156 } 155 157 catch (e) { 156 158 failTask(test.result!, e, runner.config.diffOptions) ··· 188 190 } 189 191 190 192 async function runHook(hook: Function) { 191 - return getBeforeHookCleanupCallback( 192 - hook, 193 - await hook(...args), 194 - name === 'beforeEach' ? args[0] as TestContext : undefined, 195 - ) 193 + return limitMaxConcurrency(async () => { 194 + return getBeforeHookCleanupCallback( 195 + hook, 196 + await hook(...args), 197 + name === 'beforeEach' ? args[0] as TestContext : undefined, 198 + ) 199 + }) 196 200 } 197 201 198 202 if (sequence === 'parallel') { ··· 311 315 let useCalled = false 312 316 let setupTimeout: ReturnType<typeof createTimeoutPromise> 313 317 let teardownTimeout: ReturnType<typeof createTimeoutPromise> | undefined 318 + let setupLimitConcurrencyRelease: (() => void) | undefined 319 + let teardownLimitConcurrencyRelease: (() => void) | undefined 314 320 315 321 // Promise that resolves when use() is called (setup phase complete) 316 322 let resolveUseCalled!: () => void ··· 352 358 353 359 // Setup phase completed - clear setup timer 354 360 setupTimeout.clear() 361 + setupLimitConcurrencyRelease?.() 355 362 356 363 // Run inner hooks - don't time this against our teardown timeout 357 364 await runNextHook(index + 1).catch(e => hookErrors.push(e)) 365 + 366 + teardownLimitConcurrencyRelease = await limitMaxConcurrency.acquire() 358 367 359 368 // Start teardown timer after inner hooks complete - only times this hook's teardown code 360 369 teardownTimeout = createTimeoutPromise(timeout, 'teardown', stackTraceError) ··· 362 371 // Signal that use() is returning (teardown phase starting) 363 372 resolveUseReturned() 364 373 } 374 + 375 + setupLimitConcurrencyRelease = await limitMaxConcurrency.acquire() 365 376 366 377 // Start setup timeout 367 378 setupTimeout = createTimeoutPromise(timeout, 'setup', stackTraceError) ··· 381 392 catch (error) { 382 393 rejectHookComplete(error as Error) 383 394 } 395 + finally { 396 + setupLimitConcurrencyRelease?.() 397 + teardownLimitConcurrencyRelease?.() 398 + } 384 399 })() 385 400 386 401 // Wait for either: use() to be called OR hook to complete (error) OR setup timeout ··· 392 407 ]) 393 408 } 394 409 finally { 410 + setupLimitConcurrencyRelease?.() 395 411 setupTimeout.clear() 396 412 } 397 413 ··· 410 426 ]) 411 427 } 412 428 finally { 429 + teardownLimitConcurrencyRelease?.() 413 430 teardownTimeout?.clear() 414 431 } 415 432 } ··· 524 541 if (typeof fn !== 'function') { 525 542 return 526 543 } 527 - await fn() 544 + await limitMaxConcurrency(() => fn()) 528 545 }), 529 546 ) 530 547 } ··· 533 550 if (typeof fn !== 'function') { 534 551 continue 535 552 } 536 - await fn() 553 + await limitMaxConcurrency(() => fn()) 537 554 } 538 555 } 539 556 } ··· 623 640 )) 624 641 625 642 if (runner.runTask) { 626 - await $('test.callback', () => runner.runTask!(test)) 643 + await $('test.callback', () => limitMaxConcurrency(() => runner.runTask!(test))) 627 644 } 628 645 else { 629 646 const fn = getFn(test) ··· 632 649 'Test function is not found. Did you add it using `setFn`?', 633 650 ) 634 651 } 635 - await $('test.callback', () => fn()) 652 + await $('test.callback', () => limitMaxConcurrency(() => fn())) 636 653 } 637 654 638 655 await runner.onAfterTryTask?.(test, { ··· 940 957 } 941 958 } 942 959 943 - let limitMaxConcurrency: ReturnType<typeof limitConcurrency> 944 - 945 960 async function runSuiteChild(c: Task, runner: VitestRunner) { 946 961 const $ = runner.trace! 947 962 if (c.type === 'test') { 948 - return limitMaxConcurrency(() => $( 963 + return $( 949 964 'run.test', 950 965 { 951 966 'vitest.test.id': c.id, ··· 957 972 'code.column.number': c.location?.column, 958 973 }, 959 974 () => runTest(c, runner), 960 - )) 975 + ) 961 976 } 962 977 else if (c.type === 'suite') { 963 978 return $(
+94 -2
test/cli/test/around-each.test.ts
··· 1064 1064 `) 1065 1065 }) 1066 1066 1067 + test('aroundEach teardown timeout works when runTest error is caught', async () => { 1068 + const { errorTree } = await runInlineTests({ 1069 + 'caught-inner-error-timeout.test.ts': ` 1070 + import { aroundEach, describe, expect, test } from 'vitest' 1071 + 1072 + describe('suite', () => { 1073 + aroundEach(async (runTest) => { 1074 + try { 1075 + await runTest() 1076 + } 1077 + catch { 1078 + // swallow inner hook failure, then run teardown work 1079 + } 1080 + await new Promise(resolve => setTimeout(resolve, 200)) 1081 + }, 50) 1082 + 1083 + aroundEach(async (runTest) => { 1084 + await runTest() 1085 + throw new Error('inner aroundEach teardown failure') 1086 + }) 1087 + 1088 + test('test', () => { 1089 + expect(1).toBe(1) 1090 + }) 1091 + }) 1092 + `, 1093 + }) 1094 + 1095 + expect(errorTree()).toMatchInlineSnapshot(` 1096 + { 1097 + "caught-inner-error-timeout.test.ts": { 1098 + "suite": { 1099 + "test": [ 1100 + "inner aroundEach teardown failure", 1101 + "The teardown phase of "aroundEach" hook timed out after 50ms.", 1102 + ], 1103 + }, 1104 + }, 1105 + } 1106 + `) 1107 + }) 1108 + 1067 1109 test('aroundEach with AsyncLocalStorage', async () => { 1068 1110 const { stdout, stderr, errorTree } = await runInlineTests({ 1069 1111 'async-local-storage.test.ts': ` ··· 1633 1675 `) 1634 1676 }) 1635 1677 1678 + test('aroundAll teardown timeout works when runSuite error is caught', async () => { 1679 + const { errorTree } = await runInlineTests({ 1680 + 'caught-inner-suite-error-timeout.test.ts': ` 1681 + import { aroundAll, describe, expect, test } from 'vitest' 1682 + 1683 + describe('suite', () => { 1684 + aroundAll(async (runSuite) => { 1685 + try { 1686 + await runSuite() 1687 + } 1688 + catch { 1689 + // swallow inner hook failure, then run teardown work 1690 + } 1691 + await new Promise(resolve => setTimeout(resolve, 200)) 1692 + }, 50) 1693 + 1694 + aroundAll(async (runSuite) => { 1695 + await runSuite() 1696 + throw new Error('inner aroundAll teardown failure') 1697 + }) 1698 + 1699 + test('test', () => { 1700 + expect(1).toBe(1) 1701 + }) 1702 + }) 1703 + `, 1704 + }) 1705 + 1706 + expect(errorTree()).toMatchInlineSnapshot(` 1707 + { 1708 + "caught-inner-suite-error-timeout.test.ts": { 1709 + "suite": { 1710 + "__suite_errors__": [ 1711 + "inner aroundAll teardown failure", 1712 + "The teardown phase of "aroundAll" hook timed out after 50ms.", 1713 + ], 1714 + "test": "passed", 1715 + }, 1716 + }, 1717 + } 1718 + `) 1719 + }) 1720 + 1636 1721 test('aroundAll with server start/stop pattern', async () => { 1637 1722 const { stdout, stderr, errorTree } = await runInlineTests({ 1638 1723 'server.test.ts': ` ··· 2043 2128 { 2044 2129 "caught-inner-error-timeout.test.ts": { 2045 2130 "suite": { 2131 + "__suite_errors__": [ 2132 + "inner aroundAll teardown failure", 2133 + "The teardown phase of "aroundAll" hook timed out after 50ms.", 2134 + ], 2046 2135 "test": "passed", 2047 2136 }, 2048 2137 }, ··· 2181 2270 { 2182 2271 "late-run-suite-after-timeout.test.ts": { 2183 2272 "timed out suite": { 2273 + "__suite_errors__": [ 2274 + "The setup phase of "aroundAll" hook timed out after 10ms.", 2275 + ], 2184 2276 "basic": "skipped", 2185 2277 }, 2186 2278 }, ··· 2228 2320 | ^ 2229 2321 18| }) 2230 2322 19| 2231 - ❯ nested-around-each-setup-error.test.ts:7:17 2323 + ❯ nested-around-each-setup-error.test.ts:7:11 2232 2324 2233 2325 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 2234 2326 ··· 2355 2447 | ^ 2356 2448 18| }) 2357 2449 19| 2358 - ❯ nested-around-all-setup-error.test.ts:7:17 2450 + ❯ nested-around-all-setup-error.test.ts:7:11 2359 2451 2360 2452 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/1]⎯ 2361 2453
+1076
test/cli/test/concurrent.test.ts
··· 1 + import { expect, test } from 'vitest' 2 + import { runInlineTests } from '../../test-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 + const deadlockSource = ` 14 + import { describe, expect, test } from 'vitest' 15 + import { createDefer } from '@vitest/utils/helpers' 16 + 17 + describe.concurrent('wrapper', () => { 18 + const defers = [ 19 + createDefer<void>(), 20 + createDefer<void>(), 21 + createDefer<void>(), 22 + ] 23 + 24 + test('a', async () => { 25 + expect(1).toBe(1) 26 + defers[0].resolve() 27 + await defers[2] 28 + }) 29 + 30 + test('b', async () => { 31 + expect(1).toBe(1) 32 + await defers[0] 33 + defers[1].resolve() 34 + await defers[2] 35 + }) 36 + 37 + test('c', async () => { 38 + expect(1).toBe(1) 39 + await defers[1] 40 + defers[2].resolve() 41 + }) 42 + }) 43 + ` 44 + 45 + test('deadlocks with insufficient maxConcurrency', async () => { 46 + const { errorTree } = await runInlineTests({ 47 + 'basic.test.ts': deadlockSource, 48 + }, { 49 + maxConcurrency: 2, 50 + testTimeout: 500, 51 + }) 52 + 53 + // "a" and "b" fill both concurrency slots and wait for `defers[2]`. 54 + // "c" is queued until one slot is released by timeout, then it starts, 55 + // observes `defers[1]` already resolved by "b", resolves `defers[2]`, and passes. 56 + expect(errorTree()).toMatchInlineSnapshot(` 57 + { 58 + "basic.test.ts": { 59 + "wrapper": { 60 + "a": [ 61 + "Test timed out in 500ms. 62 + If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".", 63 + ], 64 + "b": [ 65 + "Test timed out in 500ms. 66 + If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".", 67 + ], 68 + "c": "passed", 69 + }, 70 + }, 71 + } 72 + `) 73 + }) 74 + 75 + test('passes when maxConcurrency is high enough', async () => { 76 + const { stderr, errorTree } = await runInlineTests({ 77 + 'basic.test.ts': deadlockSource, 78 + }, { 79 + maxConcurrency: 3, 80 + }) 81 + 82 + expect(stderr).toBe('') 83 + expect(errorTree()).toMatchInlineSnapshot(` 84 + { 85 + "basic.test.ts": { 86 + "wrapper": { 87 + "a": "passed", 88 + "b": "passed", 89 + "c": "passed", 90 + }, 91 + }, 92 + } 93 + `) 94 + }) 95 + 96 + const suiteDeadlockSource = ` 97 + import { describe, expect, test } from 'vitest' 98 + import { createDefer } from '@vitest/utils/helpers' 99 + 100 + describe.concurrent('wrapper', () => { 101 + const defers = [ 102 + createDefer<void>(), 103 + createDefer<void>(), 104 + createDefer<void>(), 105 + ] 106 + 107 + describe('1st suite', () => { 108 + test('a', async () => { 109 + expect(1).toBe(1) 110 + defers[0].resolve() 111 + await defers[2] 112 + }) 113 + 114 + test('b', async () => { 115 + expect(1).toBe(1) 116 + await defers[0] 117 + defers[1].resolve() 118 + await defers[2] 119 + }) 120 + }) 121 + 122 + describe('2nd suite', () => { 123 + test('c', async () => { 124 + expect(1).toBe(1) 125 + await defers[1] 126 + defers[2].resolve() 127 + }) 128 + }) 129 + }) 130 + ` 131 + 132 + test('suite deadlocks with insufficient maxConcurrency', async () => { 133 + const { errorTree } = await runInlineTests({ 134 + 'basic.test.ts': suiteDeadlockSource, 135 + }, { 136 + maxConcurrency: 2, 137 + testTimeout: 500, 138 + }) 139 + 140 + expect(errorTree()).toMatchInlineSnapshot(` 141 + { 142 + "basic.test.ts": { 143 + "wrapper": { 144 + "1st suite": { 145 + "a": [ 146 + "Test timed out in 500ms. 147 + If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".", 148 + ], 149 + "b": [ 150 + "Test timed out in 500ms. 151 + If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".", 152 + ], 153 + }, 154 + "2nd suite": { 155 + "c": "passed", 156 + }, 157 + }, 158 + }, 159 + } 160 + `) 161 + }) 162 + 163 + test('suite passes when maxConcurrency is high enough', async () => { 164 + const { stderr, errorTree } = await runInlineTests({ 165 + 'basic.test.ts': suiteDeadlockSource, 166 + }, { 167 + maxConcurrency: 3, 168 + }) 169 + 170 + expect(stderr).toBe('') 171 + expect(errorTree()).toMatchInlineSnapshot(` 172 + { 173 + "basic.test.ts": { 174 + "wrapper": { 175 + "1st suite": { 176 + "a": "passed", 177 + "b": "passed", 178 + }, 179 + "2nd suite": { 180 + "c": "passed", 181 + }, 182 + }, 183 + }, 184 + } 185 + `) 186 + }) 187 + 188 + const beforeAllNeighboringSuitesSource = ` 189 + import { beforeAll, describe, expect, test } from 'vitest' 190 + import { createDefer } from '@vitest/utils/helpers' 191 + 192 + const defers = [ 193 + createDefer<void>(), 194 + createDefer<void>(), 195 + ] 196 + 197 + describe.concurrent('s1', () => { 198 + beforeAll(async () => { 199 + defers[0].resolve() 200 + await defers[1] 201 + }) 202 + 203 + test('a', () => { 204 + expect(1).toBe(1) 205 + }) 206 + }) 207 + 208 + describe.concurrent('s2', () => { 209 + beforeAll(async () => { 210 + await defers[0] 211 + defers[1].resolve() 212 + }) 213 + 214 + test('b', () => { 215 + expect(1).toBe(1) 216 + }) 217 + }) 218 + ` 219 + 220 + test('neighboring suite beforeAll deadlocks with insufficient maxConcurrency', async () => { 221 + const { errorTree } = await runInlineTests({ 222 + 'basic.test.ts': beforeAllNeighboringSuitesSource, 223 + }, { 224 + maxConcurrency: 1, 225 + hookTimeout: 500, 226 + }) 227 + 228 + expect(errorTree()).toMatchInlineSnapshot(` 229 + { 230 + "basic.test.ts": { 231 + "s1": { 232 + "__suite_errors__": [ 233 + "Hook timed out in 500ms. 234 + If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".", 235 + ], 236 + "a": "skipped", 237 + }, 238 + "s2": { 239 + "b": "passed", 240 + }, 241 + }, 242 + } 243 + `) 244 + }) 245 + 246 + test('neighboring suite beforeAll passes when maxConcurrency is high enough', async () => { 247 + const { stderr, errorTree } = await runInlineTests({ 248 + 'basic.test.ts': beforeAllNeighboringSuitesSource, 249 + }, { 250 + maxConcurrency: 2, 251 + }) 252 + 253 + expect(stderr).toBe('') 254 + expect(errorTree()).toMatchInlineSnapshot(` 255 + { 256 + "basic.test.ts": { 257 + "s1": { 258 + "a": "passed", 259 + }, 260 + "s2": { 261 + "b": "passed", 262 + }, 263 + }, 264 + } 265 + `) 266 + }) 267 + 268 + const afterAllNeighboringSuitesSource = ` 269 + import { afterAll, describe, expect, test } from 'vitest' 270 + import { createDefer } from '@vitest/utils/helpers' 271 + 272 + const defers = [ 273 + createDefer<void>(), 274 + createDefer<void>(), 275 + ] 276 + 277 + describe.concurrent('s1', () => { 278 + afterAll(async () => { 279 + defers[0].resolve() 280 + await defers[1] 281 + }) 282 + 283 + test('a', () => { 284 + expect(1).toBe(1) 285 + }) 286 + }) 287 + 288 + describe.concurrent('s2', () => { 289 + afterAll(async () => { 290 + await defers[0] 291 + defers[1].resolve() 292 + }) 293 + 294 + test('b', () => { 295 + expect(1).toBe(1) 296 + }) 297 + }) 298 + ` 299 + 300 + test('neighboring suite afterAll deadlocks with insufficient maxConcurrency', async () => { 301 + const { errorTree } = await runInlineTests({ 302 + 'basic.test.ts': afterAllNeighboringSuitesSource, 303 + }, { 304 + maxConcurrency: 1, 305 + hookTimeout: 500, 306 + }) 307 + 308 + expect(errorTree()).toMatchInlineSnapshot(` 309 + { 310 + "basic.test.ts": { 311 + "s1": { 312 + "__suite_errors__": [ 313 + "Hook timed out in 500ms. 314 + If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".", 315 + ], 316 + "a": "passed", 317 + }, 318 + "s2": { 319 + "b": "passed", 320 + }, 321 + }, 322 + } 323 + `) 324 + }) 325 + 326 + test('neighboring suite afterAll passes when maxConcurrency is high enough', async () => { 327 + const { stderr, errorTree } = await runInlineTests({ 328 + 'basic.test.ts': afterAllNeighboringSuitesSource, 329 + }, { 330 + maxConcurrency: 2, 331 + }) 332 + 333 + expect(stderr).toBe('') 334 + expect(errorTree()).toMatchInlineSnapshot(` 335 + { 336 + "basic.test.ts": { 337 + "s1": { 338 + "a": "passed", 339 + }, 340 + "s2": { 341 + "b": "passed", 342 + }, 343 + }, 344 + } 345 + `) 346 + }) 347 + 348 + const beforeEachDeadlockSource = ` 349 + import { beforeEach, describe, expect, test } from 'vitest' 350 + import { createDefer } from '@vitest/utils/helpers' 351 + 352 + describe.concurrent('wrapper', () => { 353 + const defers = [ 354 + createDefer<void>(), 355 + createDefer<void>(), 356 + createDefer<void>(), 357 + ] 358 + 359 + beforeEach(async () => { 360 + defers[0].resolve() 361 + await defers[2] 362 + }) 363 + 364 + beforeEach(async () => { 365 + await defers[0] 366 + defers[1].resolve() 367 + await defers[2] 368 + }) 369 + 370 + beforeEach(async () => { 371 + await defers[1] 372 + defers[2].resolve() 373 + }) 374 + 375 + test('t', () => { 376 + expect(1).toBe(1) 377 + }) 378 + }) 379 + ` 380 + 381 + test('beforeEach deadlocks with insufficient maxConcurrency', async () => { 382 + const { errorTree } = await runInlineTests({ 383 + 'basic.test.ts': beforeEachDeadlockSource, 384 + }, { 385 + maxConcurrency: 2, 386 + sequence: { hooks: 'parallel' }, 387 + hookTimeout: 500, 388 + }) 389 + 390 + expect(errorTree()).toMatchInlineSnapshot(` 391 + { 392 + "basic.test.ts": { 393 + "wrapper": { 394 + "t": [ 395 + "Hook timed out in 500ms. 396 + If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".", 397 + ], 398 + }, 399 + }, 400 + } 401 + `) 402 + }) 403 + 404 + test('beforeEach passes when maxConcurrency is high enough', async () => { 405 + const { stderr, errorTree } = await runInlineTests({ 406 + 'basic.test.ts': beforeEachDeadlockSource, 407 + }, { 408 + maxConcurrency: 3, 409 + sequence: { hooks: 'parallel' }, 410 + }) 411 + 412 + expect(stderr).toBe('') 413 + expect(errorTree()).toMatchInlineSnapshot(` 414 + { 415 + "basic.test.ts": { 416 + "wrapper": { 417 + "t": "passed", 418 + }, 419 + }, 420 + } 421 + `) 422 + }) 423 + 424 + const afterEachDeadlockSource = ` 425 + import { afterEach, describe, expect, test } from 'vitest' 426 + import { createDefer } from '@vitest/utils/helpers' 427 + 428 + describe.concurrent('wrapper', () => { 429 + const defers = [ 430 + createDefer<void>(), 431 + createDefer<void>(), 432 + createDefer<void>(), 433 + ] 434 + 435 + afterEach(async () => { 436 + defers[0].resolve() 437 + await defers[2] 438 + }) 439 + 440 + afterEach(async () => { 441 + await defers[0] 442 + defers[1].resolve() 443 + await defers[2] 444 + }) 445 + 446 + afterEach(async () => { 447 + await defers[1] 448 + defers[2].resolve() 449 + }) 450 + 451 + test('t', () => { 452 + expect(1).toBe(1) 453 + }) 454 + }) 455 + ` 456 + 457 + test('afterEach deadlocks with insufficient maxConcurrency', async () => { 458 + const { errorTree } = await runInlineTests({ 459 + 'basic.test.ts': afterEachDeadlockSource, 460 + }, { 461 + maxConcurrency: 2, 462 + sequence: { hooks: 'parallel' }, 463 + hookTimeout: 500, 464 + }) 465 + 466 + expect(errorTree()).toMatchInlineSnapshot(` 467 + { 468 + "basic.test.ts": { 469 + "wrapper": { 470 + "t": [ 471 + "Hook timed out in 500ms. 472 + If this is a long-running hook, pass a timeout value as the last argument or configure it globally with "hookTimeout".", 473 + ], 474 + }, 475 + }, 476 + } 477 + `) 478 + }) 479 + 480 + test('afterEach passes when maxConcurrency is high enough', async () => { 481 + const { stderr, errorTree } = await runInlineTests({ 482 + 'basic.test.ts': afterEachDeadlockSource, 483 + }, { 484 + maxConcurrency: 3, 485 + sequence: { hooks: 'parallel' }, 486 + }) 487 + 488 + expect(stderr).toBe('') 489 + expect(errorTree()).toMatchInlineSnapshot(` 490 + { 491 + "basic.test.ts": { 492 + "wrapper": { 493 + "t": "passed", 494 + }, 495 + }, 496 + } 497 + `) 498 + }) 499 + 500 + const aroundAllNeighboringSuitesSource = ` 501 + import { aroundAll, describe, expect, test } from 'vitest' 502 + import { createDefer } from '@vitest/utils/helpers' 503 + 504 + const defers = [ 505 + createDefer<void>(), 506 + createDefer<void>(), 507 + ] 508 + 509 + describe.concurrent('s1', () => { 510 + aroundAll(async (runSuite) => { 511 + defers[0].resolve() 512 + await defers[1] 513 + await runSuite() 514 + }) 515 + 516 + test('a', () => { 517 + expect(1).toBe(1) 518 + }) 519 + }) 520 + 521 + describe.concurrent('s2', () => { 522 + aroundAll(async (runSuite) => { 523 + await defers[0] 524 + defers[1].resolve() 525 + await runSuite() 526 + }) 527 + 528 + test('b', () => { 529 + expect(1).toBe(1) 530 + }) 531 + }) 532 + ` 533 + 534 + test('neighboring suite aroundAll deadlocks with insufficient maxConcurrency', async () => { 535 + const { errorTree } = await runInlineTests({ 536 + 'basic.test.ts': aroundAllNeighboringSuitesSource, 537 + }, { 538 + maxConcurrency: 1, 539 + hookTimeout: 500, 540 + }) 541 + 542 + expect(errorTree()).toMatchInlineSnapshot(` 543 + { 544 + "basic.test.ts": { 545 + "s1": { 546 + "__suite_errors__": [ 547 + "The setup phase of "aroundAll" hook timed out after 500ms.", 548 + ], 549 + "a": "skipped", 550 + }, 551 + "s2": { 552 + "b": "passed", 553 + }, 554 + }, 555 + } 556 + `) 557 + }) 558 + 559 + test('neighboring suite aroundAll passes when maxConcurrency is high enough', async () => { 560 + const { stderr, errorTree } = await runInlineTests({ 561 + 'basic.test.ts': aroundAllNeighboringSuitesSource, 562 + }, { 563 + maxConcurrency: 2, 564 + }) 565 + 566 + expect(stderr).toBe('') 567 + expect(errorTree()).toMatchInlineSnapshot(` 568 + { 569 + "basic.test.ts": { 570 + "s1": { 571 + "a": "passed", 572 + }, 573 + "s2": { 574 + "b": "passed", 575 + }, 576 + }, 577 + } 578 + `) 579 + }) 580 + 581 + const aroundAllNeighboringSuitesPostSource = ` 582 + import { aroundAll, describe, expect, test } from 'vitest' 583 + import { createDefer } from '@vitest/utils/helpers' 584 + 585 + const defers = [ 586 + createDefer<void>(), 587 + createDefer<void>(), 588 + ] 589 + 590 + describe.concurrent('s1', () => { 591 + aroundAll(async (runSuite) => { 592 + await runSuite() 593 + defers[0].resolve() 594 + await defers[1] 595 + }) 596 + 597 + test('a', () => { 598 + expect(1).toBe(1) 599 + }) 600 + }) 601 + 602 + describe.concurrent('s2', () => { 603 + aroundAll(async (runSuite) => { 604 + await runSuite() 605 + await defers[0] 606 + defers[1].resolve() 607 + }) 608 + 609 + test('b', () => { 610 + expect(1).toBe(1) 611 + }) 612 + }) 613 + ` 614 + 615 + test('neighboring suite aroundAll teardown deadlocks with insufficient maxConcurrency', async () => { 616 + const { errorTree } = await runInlineTests({ 617 + 'basic.test.ts': aroundAllNeighboringSuitesPostSource, 618 + }, { 619 + maxConcurrency: 1, 620 + hookTimeout: 500, 621 + }) 622 + 623 + expect(errorTree()).toMatchInlineSnapshot(` 624 + { 625 + "basic.test.ts": { 626 + "s1": { 627 + "__suite_errors__": [ 628 + "The teardown phase of \"aroundAll\" hook timed out after 500ms.", 629 + ], 630 + "a": "passed", 631 + }, 632 + "s2": { 633 + "b": "passed", 634 + }, 635 + }, 636 + } 637 + `) 638 + }) 639 + 640 + test('neighboring suite aroundAll teardown passes when maxConcurrency is high enough', async () => { 641 + const { stderr, errorTree } = await runInlineTests({ 642 + 'basic.test.ts': aroundAllNeighboringSuitesPostSource, 643 + }, { 644 + maxConcurrency: 2, 645 + }) 646 + 647 + expect(stderr).toBe('') 648 + expect(errorTree()).toMatchInlineSnapshot(` 649 + { 650 + "basic.test.ts": { 651 + "s1": { 652 + "a": "passed", 653 + }, 654 + "s2": { 655 + "b": "passed", 656 + }, 657 + }, 658 + } 659 + `) 660 + }) 661 + 662 + const aroundAllSetupTimeoutLateTeardownAcquireSource = ` 663 + import { aroundAll, describe, expect, test } from 'vitest' 664 + import { createDefer } from '@vitest/utils/helpers' 665 + 666 + const unblockS1Setup = createDefer<void>() 667 + const allowS2TestFinish = createDefer<void>() 668 + const blockForever = createDefer<void>() 669 + 670 + describe.concurrent('s1', () => { 671 + aroundAll(async (runSuite) => { 672 + await unblockS1Setup 673 + await runSuite() 674 + allowS2TestFinish.resolve() 675 + await blockForever 676 + }) 677 + 678 + test('a', () => { 679 + expect(1).toBe(1) 680 + }) 681 + }) 682 + 683 + describe.concurrent('s2', () => { 684 + aroundAll(async (runSuite) => { 685 + unblockS1Setup.resolve() 686 + await runSuite() 687 + }) 688 + 689 + test('b', async () => { 690 + await allowS2TestFinish 691 + expect(1).toBe(1) 692 + }) 693 + }) 694 + ` 695 + 696 + test('neighboring suite aroundAll does not hang when setup times out before late teardown acquire', async () => { 697 + const { errorTree } = await runInlineTests({ 698 + 'basic.test.ts': aroundAllSetupTimeoutLateTeardownAcquireSource, 699 + }, { 700 + maxConcurrency: 1, 701 + hookTimeout: 500, 702 + testTimeout: 500, 703 + }) 704 + 705 + expect(errorTree()).toMatchInlineSnapshot(` 706 + { 707 + "basic.test.ts": { 708 + "s1": { 709 + "__suite_errors__": [ 710 + "The setup phase of "aroundAll" hook timed out after 500ms.", 711 + ], 712 + "a": "skipped", 713 + }, 714 + "s2": { 715 + "b": [ 716 + "Test timed out in 500ms. 717 + If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".", 718 + ], 719 + }, 720 + }, 721 + } 722 + `) 723 + }) 724 + 725 + const aroundEachNeighboringTestsSource = ` 726 + import { aroundEach, describe, expect, test } from 'vitest' 727 + import { createDefer } from '@vitest/utils/helpers' 728 + 729 + const defers = [ 730 + createDefer<void>(), 731 + createDefer<void>(), 732 + ] 733 + 734 + describe.concurrent('wrapper', () => { 735 + aroundEach(async (runTest, context) => { 736 + if (context.task.name === 'a') { 737 + defers[0].resolve() 738 + await defers[1] 739 + await runTest() 740 + return 741 + } 742 + 743 + await defers[0] 744 + defers[1].resolve() 745 + await runTest() 746 + }) 747 + 748 + test('a', () => { 749 + expect(1).toBe(1) 750 + }) 751 + 752 + test('b', () => { 753 + expect(1).toBe(1) 754 + }) 755 + }) 756 + ` 757 + 758 + test('neighboring test aroundEach deadlocks with insufficient maxConcurrency', async () => { 759 + const { errorTree } = await runInlineTests({ 760 + 'basic.test.ts': aroundEachNeighboringTestsSource, 761 + }, { 762 + maxConcurrency: 1, 763 + hookTimeout: 500, 764 + }) 765 + 766 + expect(errorTree()).toMatchInlineSnapshot(` 767 + { 768 + "basic.test.ts": { 769 + "wrapper": { 770 + "a": [ 771 + "The setup phase of \"aroundEach\" hook timed out after 500ms.", 772 + ], 773 + "b": "passed", 774 + }, 775 + }, 776 + } 777 + `) 778 + }) 779 + 780 + test('neighboring test aroundEach passes when maxConcurrency is high enough', async () => { 781 + const { stderr, errorTree } = await runInlineTests({ 782 + 'basic.test.ts': aroundEachNeighboringTestsSource, 783 + }, { 784 + maxConcurrency: 2, 785 + }) 786 + 787 + expect(stderr).toBe('') 788 + expect(errorTree()).toMatchInlineSnapshot(` 789 + { 790 + "basic.test.ts": { 791 + "wrapper": { 792 + "a": "passed", 793 + "b": "passed", 794 + }, 795 + }, 796 + } 797 + `) 798 + }) 799 + 800 + const aroundEachNeighboringTestsPostSource = ` 801 + import { aroundEach, describe, expect, test } from 'vitest' 802 + import { createDefer } from '@vitest/utils/helpers' 803 + 804 + const defers = [ 805 + createDefer<void>(), 806 + createDefer<void>(), 807 + ] 808 + 809 + describe.concurrent('wrapper', () => { 810 + aroundEach(async (runTest, context) => { 811 + await runTest() 812 + 813 + if (context.task.name === 'a') { 814 + defers[0].resolve() 815 + await defers[1] 816 + return 817 + } 818 + 819 + await defers[0] 820 + defers[1].resolve() 821 + }) 822 + 823 + test('a', () => { 824 + expect(1).toBe(1) 825 + }) 826 + 827 + test('b', () => { 828 + expect(1).toBe(1) 829 + }) 830 + }) 831 + ` 832 + 833 + test('neighboring test aroundEach teardown deadlocks with insufficient maxConcurrency', async () => { 834 + const { errorTree } = await runInlineTests({ 835 + 'basic.test.ts': aroundEachNeighboringTestsPostSource, 836 + }, { 837 + maxConcurrency: 1, 838 + hookTimeout: 500, 839 + }) 840 + 841 + expect(errorTree()).toMatchInlineSnapshot(` 842 + { 843 + "basic.test.ts": { 844 + "wrapper": { 845 + "a": [ 846 + "The teardown phase of \"aroundEach\" hook timed out after 500ms.", 847 + ], 848 + "b": "passed", 849 + }, 850 + }, 851 + } 852 + `) 853 + }) 854 + 855 + test('neighboring test aroundEach teardown passes when maxConcurrency is high enough', async () => { 856 + const { stderr, errorTree } = await runInlineTests({ 857 + 'basic.test.ts': aroundEachNeighboringTestsPostSource, 858 + }, { 859 + maxConcurrency: 2, 860 + }) 861 + 862 + expect(stderr).toBe('') 863 + expect(errorTree()).toMatchInlineSnapshot(` 864 + { 865 + "basic.test.ts": { 866 + "wrapper": { 867 + "a": "passed", 868 + "b": "passed", 869 + }, 870 + }, 871 + } 872 + `) 873 + }) 874 + 875 + const aroundEachOuterCatchesInnerErrorSource = ` 876 + import { aroundEach, describe, expect, test } from 'vitest' 877 + 878 + describe.concurrent('wrapper', () => { 879 + aroundEach(async (runTest) => { 880 + let runTestError: unknown 881 + try { 882 + await runTest() 883 + } 884 + catch (error) { 885 + runTestError = error 886 + } 887 + 888 + await Promise.resolve() 889 + 890 + if (runTestError) { 891 + throw runTestError 892 + } 893 + }) 894 + 895 + aroundEach(async (runTest, context) => { 896 + await runTest() 897 + if (context.task.name === 'a') { 898 + throw new Error('inner aroundEach teardown failure') 899 + } 900 + }) 901 + 902 + test('a', () => { 903 + expect(1).toBe(1) 904 + }) 905 + 906 + test('b', () => { 907 + expect(1).toBe(1) 908 + }) 909 + }) 910 + ` 911 + 912 + test('aroundEach continues protocol when outer hook catches runTest error', async () => { 913 + const { errorTree } = await runInlineTests({ 914 + 'basic.test.ts': aroundEachOuterCatchesInnerErrorSource, 915 + }, { 916 + maxConcurrency: 1, 917 + }) 918 + 919 + expect(errorTree()).toMatchInlineSnapshot(` 920 + { 921 + "basic.test.ts": { 922 + "wrapper": { 923 + "a": [ 924 + "inner aroundEach teardown failure", 925 + ], 926 + "b": "passed", 927 + }, 928 + }, 929 + } 930 + `) 931 + }) 932 + 933 + const aroundAllOuterCatchesInnerErrorSource = ` 934 + import { aroundAll, describe, expect, test } from 'vitest' 935 + 936 + describe.concurrent('suite', () => { 937 + aroundAll(async (runSuite) => { 938 + let runSuiteError: unknown 939 + try { 940 + await runSuite() 941 + } 942 + catch (error) { 943 + runSuiteError = error 944 + } 945 + 946 + await Promise.resolve() 947 + 948 + if (runSuiteError) { 949 + throw runSuiteError 950 + } 951 + }) 952 + 953 + aroundAll(async (runSuite) => { 954 + await runSuite() 955 + throw new Error('inner aroundAll teardown failure') 956 + }) 957 + 958 + test('a', () => { 959 + expect(1).toBe(1) 960 + }) 961 + }) 962 + ` 963 + 964 + test('aroundAll continues protocol when outer hook catches runSuite error', async () => { 965 + const { errorTree } = await runInlineTests({ 966 + 'basic.test.ts': aroundAllOuterCatchesInnerErrorSource, 967 + }, { 968 + maxConcurrency: 1, 969 + }) 970 + 971 + expect(errorTree()).toMatchInlineSnapshot(` 972 + { 973 + "basic.test.ts": { 974 + "suite": { 975 + "__suite_errors__": [ 976 + "inner aroundAll teardown failure", 977 + ], 978 + "a": "passed", 979 + }, 980 + }, 981 + } 982 + `) 983 + }) 984 + 985 + const aroundEachCaughtInnerErrorTeardownTimeoutSource = ` 986 + import { aroundEach, describe, expect, test } from 'vitest' 987 + 988 + describe.concurrent('wrapper', () => { 989 + aroundEach(async (runTest) => { 990 + try { 991 + await runTest() 992 + } 993 + catch { 994 + // swallow inner failure, then run long teardown logic 995 + } 996 + await new Promise(resolve => setTimeout(resolve, 200)) 997 + }, 50) 998 + 999 + aroundEach(async (runTest) => { 1000 + await runTest() 1001 + throw new Error('inner aroundEach teardown failure') 1002 + }) 1003 + 1004 + test('a', () => { 1005 + expect(1).toBe(1) 1006 + }) 1007 + }) 1008 + ` 1009 + 1010 + test('aroundEach enforces teardown timeout when inner error is caught', async () => { 1011 + const { errorTree } = await runInlineTests({ 1012 + 'basic.test.ts': aroundEachCaughtInnerErrorTeardownTimeoutSource, 1013 + }, { 1014 + maxConcurrency: 1, 1015 + }) 1016 + 1017 + expect(errorTree()).toMatchInlineSnapshot(` 1018 + { 1019 + "basic.test.ts": { 1020 + "wrapper": { 1021 + "a": [ 1022 + "inner aroundEach teardown failure", 1023 + "The teardown phase of "aroundEach" hook timed out after 50ms.", 1024 + ], 1025 + }, 1026 + }, 1027 + } 1028 + `) 1029 + }) 1030 + 1031 + const aroundAllCaughtInnerErrorTeardownTimeoutSource = ` 1032 + import { aroundAll, describe, expect, test } from 'vitest' 1033 + 1034 + describe.concurrent('suite', () => { 1035 + aroundAll(async (runSuite) => { 1036 + try { 1037 + await runSuite() 1038 + } 1039 + catch { 1040 + // swallow inner failure, then run long teardown logic 1041 + } 1042 + await new Promise(resolve => setTimeout(resolve, 200)) 1043 + }, 50) 1044 + 1045 + aroundAll(async (runSuite) => { 1046 + await runSuite() 1047 + throw new Error('inner aroundAll teardown failure') 1048 + }) 1049 + 1050 + test('a', () => { 1051 + expect(1).toBe(1) 1052 + }) 1053 + }) 1054 + ` 1055 + 1056 + test('aroundAll enforces teardown timeout when inner error is caught', async () => { 1057 + const { errorTree } = await runInlineTests({ 1058 + 'basic.test.ts': aroundAllCaughtInnerErrorTeardownTimeoutSource, 1059 + }, { 1060 + maxConcurrency: 1, 1061 + }) 1062 + 1063 + expect(errorTree()).toMatchInlineSnapshot(` 1064 + { 1065 + "basic.test.ts": { 1066 + "suite": { 1067 + "__suite_errors__": [ 1068 + "inner aroundAll teardown failure", 1069 + "The teardown phase of "aroundAll" hook timed out after 50ms.", 1070 + ], 1071 + "a": "passed", 1072 + }, 1073 + }, 1074 + } 1075 + `) 1076 + })
+9 -3
test/cli/test/expect-task.test.ts
··· 172 172 name: 'test-bound extend & local extend', 173 173 test: testBoundLocalExtend, 174 174 }, 175 - ] as const)('works with $name', async ({ options, test }, { expect }) => { 175 + ] as const)('works with $name', async ({ options, test }, { task, expect }) => { 176 176 const { stdout, stderr } = await runInlineTests( 177 177 { 178 178 'basic.test.ts': test, 179 179 'to-match-test.ts': toMatchTest, 180 180 }, 181 181 { reporters: ['tap'], ...options }, 182 + undefined, 183 + task, 182 184 ) 183 185 184 186 expect(stderr).toBe('') ··· 210 212 name: 'global import', 211 213 test: withConcurrency(globalImport), 212 214 }, 213 - ] as const)('fails with $name', async ({ options, test }, { expect }) => { 215 + ] as const)('fails with $name', async ({ options, test }, { task, expect }) => { 214 216 const { stdout, ctx } = await runInlineTests( 215 217 { 216 218 'basic.test.ts': test, 217 219 'to-match-test.ts': toMatchTest, 218 220 }, 219 221 { reporters: ['tap'], ...options }, 222 + undefined, 223 + task, 220 224 ) 221 225 222 226 expect( ··· 263 267 name: 'test-bound extend & local extend', 264 268 test: withConcurrency(testBoundLocalExtend), 265 269 }, 266 - ])('works with $name', async ({ test }, { expect }) => { 270 + ])('works with $name', async ({ test }, { task, expect }) => { 267 271 const { stdout } = await runInlineTests( 268 272 { 269 273 'basic.test.ts': test, 270 274 'to-match-test.ts': toMatchTest, 271 275 }, 272 276 { reporters: ['tap'] }, 277 + undefined, 278 + task, 273 279 ) 274 280 275 281 expect(stdout.replace(/[\d.]+m?s/g, '<time>')).toMatchInlineSnapshot(`
+44 -1
test/core/test/concurrent-suite.test.ts
··· 1 1 import { createDefer } from '@vitest/utils/helpers' 2 - import { afterAll, describe, expect, test } from 'vitest' 2 + import { afterAll, beforeAll, describe, expect, test } from 'vitest' 3 3 4 4 describe('basic', () => { 5 5 const defers = [ ··· 181 181 }) 182 182 }) 183 183 } 184 + 185 + describe('suite hook concurrency', () => { 186 + const logs: string[] = [] 187 + 188 + afterAll(() => { 189 + expect(logs).toEqual(['s2-before', 's2-after', 's1-before', 's1-before-2', 's1-after-2', 's1-after']) 190 + }) 191 + 192 + describe.concurrent('s1', () => { 193 + // before/afterAll inside same suite runs sequentially in stack order 194 + beforeAll(async () => { 195 + await sleep(200) 196 + logs.push('s1-before') 197 + }) 198 + 199 + beforeAll(async () => { 200 + logs.push('s1-before-2') 201 + }) 202 + 203 + afterAll(async () => { 204 + logs.push('s1-after') 205 + }) 206 + 207 + afterAll(async () => { 208 + logs.push('s1-after-2') 209 + }) 210 + 211 + test('s1-t', () => {}) 212 + }) 213 + 214 + describe.concurrent('s2', () => { 215 + // before/afterAll in neighboring concurrent suite runs in parallel 216 + beforeAll(async () => { 217 + logs.push('s2-before') 218 + }) 219 + 220 + afterAll(async () => { 221 + logs.push('s2-after') 222 + }) 223 + 224 + test('s2-t', () => {}) 225 + }) 226 + })
+46 -18
packages/runner/src/utils/limit-concurrency.ts
··· 1 1 // A compact (code-wise, probably not memory-wise) singly linked list node. 2 2 type QueueNode<T> = [value: T, next?: QueueNode<T>] 3 3 4 + export interface ConcurrencyLimiter extends ConcurrencyLimiterFn { 5 + acquire: () => (() => void) | Promise<() => void> 6 + } 7 + 8 + type ConcurrencyLimiterFn = <Args extends unknown[], T>(func: (...args: Args) => PromiseLike<T> | T, ...args: Args) => Promise<T> 9 + 4 10 /** 5 11 * Return a function for running multiple async operations with limited concurrency. 6 12 */ 7 - export function limitConcurrency(concurrency: number = Infinity): <Args extends unknown[], T>(func: (...args: Args) => PromiseLike<T> | T, ...args: Args) => Promise<T> { 13 + export function limitConcurrency(concurrency: number = Infinity): ConcurrencyLimiter { 8 14 // The number of currently active + pending tasks. 9 15 let count = 0 10 16 ··· 30 36 } 31 37 } 32 38 33 - return (func, ...args) => { 34 - // Create a promise chain that: 35 - // 1. Waits for its turn in the task queue (if necessary). 36 - // 2. Runs the task. 37 - // 3. Allows the next pending task (if any) to run. 38 - return new Promise<void>((resolve) => { 39 - if (count++ < concurrency) { 40 - // No need to queue if fewer than maxConcurrency tasks are running. 41 - resolve() 39 + const acquire = () => { 40 + let released = false 41 + const release = () => { 42 + if (!released) { 43 + released = true 44 + finish() 42 45 } 43 - else if (tail) { 46 + } 47 + 48 + if (count++ < concurrency) { 49 + return release 50 + } 51 + 52 + return new Promise<() => void>((resolve) => { 53 + if (tail) { 44 54 // There are pending tasks, so append to the queue. 45 - tail = tail[1] = [resolve] 55 + tail = tail[1] = [() => resolve(release)] 46 56 } 47 57 else { 48 58 // No other pending tasks, initialize the queue with a new tail and head. 49 - head = tail = [resolve] 59 + head = tail = [() => resolve(release)] 50 60 } 51 - }).then(() => { 52 - // Running func here ensures that even a non-thenable result or an 53 - // immediately thrown error gets wrapped into a Promise. 54 - return func(...args) 55 - }).finally(finish) 61 + }) 56 62 } 63 + 64 + const limiterFn: ConcurrencyLimiterFn = (func, ...args) => { 65 + function run(release: () => void) { 66 + try { 67 + const result = func(...args) 68 + if (result instanceof Promise) { 69 + return result.finally(release) 70 + } 71 + release() 72 + return Promise.resolve(result) 73 + } 74 + catch (error) { 75 + release() 76 + return Promise.reject(error) 77 + } 78 + } 79 + 80 + const release = acquire() 81 + return release instanceof Promise ? release.then(run) : run(release) 82 + } 83 + 84 + return Object.assign(limiterFn, { acquire }) 57 85 }
-44
test/cli/fixtures/fails/concurrent-suite-deadlock.test.ts
··· 1 - import { createDefer } from '@vitest/utils/helpers' 2 - import { describe, test, vi, expect } 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 - expect(1).toBe(1) 25 - defers[0].resolve() 26 - await defers[2] 27 - }) 28 - 29 - test('b', async () => { 30 - expect(1).toBe(1) 31 - await defers[0] 32 - defers[1].resolve() 33 - await defers[2] 34 - }) 35 - }) 36 - 37 - describe('2nd suite', () => { 38 - test('c', async () => { 39 - expect(1).toBe(1) 40 - await defers[1] 41 - defers[2].resolve() 42 - }) 43 - }) 44 - })
-40
test/cli/fixtures/fails/concurrent-test-deadlock.test.ts
··· 1 - import { describe, expect, test, vi } from 'vitest' 2 - import { createDefer } from '@vitest/utils/helpers' 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 - expect(1).toBe(1) 24 - defers[0].resolve() 25 - await defers[2] 26 - }) 27 - 28 - test('b', async () => { 29 - expect(1).toBe(1) 30 - await defers[0] 31 - defers[1].resolve() 32 - await defers[2] 33 - }) 34 - 35 - test('c', async () => { 36 - expect(1).toBe(1) 37 - await defers[1] 38 - defers[2].resolve() 39 - }) 40 - })
-10
test/cli/test/__snapshots__/fails.test.ts.snap
··· 7 7 AssertionError: expected 'xx' to be 'yy' // Object.is equality" 8 8 `; 9 9 10 - exports[`should fail concurrent-suite-deadlock.test.ts 1`] = ` 11 - "Error: Test timed out in 500ms. 12 - Error: Test timed out in 500ms." 13 - `; 14 - 15 - exports[`should fail concurrent-test-deadlock.test.ts 1`] = ` 16 - "Error: Test timed out in 500ms. 17 - Error: Test timed out in 500ms." 18 - `; 19 - 20 10 exports[`should fail each-timeout.test.ts 1`] = `"Error: Test timed out in 10ms."`; 21 11 22 12 exports[`should fail empty.test.ts 1`] = `"Error: No test suite found in file <rootDir>/empty.test.ts"`;
+1 -1
packages/vitest/src/node/cli/cli-config.ts
··· 733 733 }, 734 734 }, 735 735 maxConcurrency: { 736 - description: 'Maximum number of concurrent tests in a suite (default: `5`)', 736 + description: 'Maximum number of concurrent tests and suites during test file execution (default: `5`)', 737 737 argument: '<number>', 738 738 }, 739 739 expect: {