diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index f8d5ecd4689c..5d1489bea274 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -188,7 +188,6 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => { 'vitest > pretty-format > ansi-regex', 'vitest > chai', 'vitest > chai > loupe', - 'vitest > @vitest/runner > p-limit', 'vitest > @vitest/utils > diff-sequences', 'vitest > @vitest/utils > loupe', '@vitest/browser > @testing-library/user-event', diff --git a/packages/runner/package.json b/packages/runner/package.json index fff6124243da..eb1cb35ef174 100644 --- a/packages/runner/package.json +++ b/packages/runner/package.json @@ -43,7 +43,6 @@ }, "dependencies": { "@vitest/utils": "workspace:*", - "p-limit": "^5.0.0", "pathe": "^1.1.2" } } diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 44291f143fa5..308ef41a7224 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -1,4 +1,3 @@ -import limit from 'p-limit' import type { Awaitable } from '@vitest/utils' import { getSafeTimers, shuffle } from '@vitest/utils' import { processError } from '@vitest/utils/error' @@ -20,6 +19,7 @@ import type { Test, } from './types' import { partitionSuiteChildren } from './utils/suite' +import { limitConcurrency } from './utils/limit-concurrency' import { getFn, getHooks } from './map' import { collectTests } from './collect' import { setCurrentTest } from './test-state' @@ -466,7 +466,7 @@ export async function runSuite(suite: Suite, runner: VitestRunner) { } } -let limitMaxConcurrency: ReturnType +let limitMaxConcurrency: ReturnType async function runSuiteChild(c: Task, runner: VitestRunner) { if (c.type === 'test' || c.type === 'custom') { @@ -478,7 +478,7 @@ async function runSuiteChild(c: Task, runner: VitestRunner) { } export async function runFiles(files: File[], runner: VitestRunner) { - limitMaxConcurrency ??= limit(runner.config.maxConcurrency) + limitMaxConcurrency ??= limitConcurrency(runner.config.maxConcurrency) for (const file of files) { if (!file.tasks.length && !runner.config.passWithNoTests) { diff --git a/packages/runner/src/utils/index.ts b/packages/runner/src/utils/index.ts index 46b2d49e5a38..83c5b1633f57 100644 --- a/packages/runner/src/utils/index.ts +++ b/packages/runner/src/utils/index.ts @@ -2,3 +2,4 @@ export * from './collect' export * from './suite' export * from './tasks' export * from './chain' +export * from './limit-concurrency' diff --git a/packages/runner/src/utils/limit-concurrency.ts b/packages/runner/src/utils/limit-concurrency.ts new file mode 100644 index 000000000000..3ba509da2fa2 --- /dev/null +++ b/packages/runner/src/utils/limit-concurrency.ts @@ -0,0 +1,57 @@ +// A compact (code-wise, probably not memory-wise) singly linked list node. +type QueueNode = [value: T, next?: QueueNode] + +/** + * Return a function for running multiple async operations with limited concurrency. + */ +export function limitConcurrency(concurrency = Infinity): (func: (...args: Args) => PromiseLike | T, ...args: Args) => Promise { + // The number of currently active + pending tasks. + let count = 0 + + // The head and tail of the pending task queue, built using a singly linked list. + // Both head and tail are initially undefined, signifying an empty queue. + // They both become undefined again whenever there are no pending tasks. + let head: undefined | QueueNode<() => void> + let tail: undefined | QueueNode<() => void> + + // A bookkeeping function executed whenever a task has been run to completion. + const finish = () => { + count-- + + // Check if there are further pending tasks in the queue. + if (head) { + // Allow the next pending task to run and pop it from the queue. + head[0]() + head = head[1] + + // The head may now be undefined if there are no further pending tasks. + // In that case, set tail to undefined as well. + tail = head && tail + } + } + + return (func, ...args) => { + // Create a promise chain that: + // 1. Waits for its turn in the task queue (if necessary). + // 2. Runs the task. + // 3. Allows the next pending task (if any) to run. + return new Promise((resolve) => { + if (count++ < concurrency) { + // No need to queue if fewer than maxConcurrency tasks are running. + resolve() + } + else if (tail) { + // There are pending tasks, so append to the queue. + tail = tail[1] = [resolve] + } + else { + // No other pending tasks, initialize the queue with a new tail and head. + head = tail = [resolve] + } + }).then(() => { + // Running func here ensures that even a non-thenable result or an + // immediately thrown error gets wrapped into a Promise. + return func(...args) + }).finally(finish) + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 648ebf9da975..15c75149e1f3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -652,9 +652,6 @@ importers: '@vitest/utils': specifier: workspace:* version: link:../utils - p-limit: - specifier: ^5.0.0 - version: 5.0.0 pathe: specifier: ^1.1.2 version: 1.1.2 @@ -12863,13 +12860,6 @@ packages: yocto-queue: 1.0.0 dev: true - /p-limit@5.0.0: - resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} - engines: {node: '>=18'} - dependencies: - yocto-queue: 1.0.0 - dev: false - /p-locate@4.1.0: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} @@ -16765,6 +16755,7 @@ packages: /yocto-queue@1.0.0: resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} engines: {node: '>=12.20'} + dev: true /zimmerframe@1.1.0: resolution: {integrity: sha512-+AmV37r9NPUy7KcuG0Fde9AAFSD88kN5pnqvD7Pkp5WLLK0jct7hAtIDXXFDCRk3l5Mc1r2Sth3gfP2ZLE+/Qw==} diff --git a/test/cli/test/console.test.ts b/test/cli/test/console.test.ts index ad0c4080d225..a1ead6cd8289 100644 --- a/test/cli/test/console.test.ts +++ b/test/cli/test/console.test.ts @@ -41,8 +41,8 @@ test('can run custom pools with Vitest', async () => { `) const stderrArray = vitest.stderr.split('\n') // remove stack trace - const stderr = stderrArray.slice(0, -9).join('\n') - const stackStderr = stderrArray.slice(-9).join('\n') + const stderr = stderrArray.slice(0, -14).join('\n') + const stackStderr = stderrArray.slice(-14).join('\n') expect(stderr).toMatchInlineSnapshot(` "stderr | trace.test.ts > logging to stdout warn with trace @@ -64,7 +64,7 @@ test('can run custom pools with Vitest', async () => { expect(stackStderr).not.toMatch('❯ ') if (process.platform !== 'win32') { const root = resolve(process.cwd(), '../..') - expect(stackStderr.replace(new RegExp(root, 'g'), '').replace(/\d+:\d+/g, 'ln:cl')).toMatchInlineSnapshot(` + expect(stackStderr.replace(new RegExp(root, 'g'), '').replace(/\d+:\d+/g, 'ln:cl').replace(/\.\w+\.js:/g, '..js:')).toMatchInlineSnapshot(` "stderr | trace.test.ts > logging to stdout Trace: trace with trace at /test/cli/fixtures/console/trace.test.ts:ln:cl @@ -72,6 +72,11 @@ test('can run custom pools with Vitest', async () => { at file:///packages/runner/dist/index.js:ln:cl at runTest (file:///packages/runner/dist/index.js:ln:cl) at processTicksAndRejections (node:internal/process/task_queues:ln:cl) + at runSuite (file:///packages/runner/dist/index.js:ln:cl) + at runFiles (file:///packages/runner/dist/index.js:ln:cl) + at startTests (file:///packages/runner/dist/index.js:ln:cl) + at file:///packages/vitest/dist/chunks/runtime-runBaseTests..js:ln:cl + at withEnv (file:///packages/vitest/dist/chunks/runtime-runBaseTests..js:ln:cl) " `)