diff --git a/packages/runner/src/run.ts b/packages/runner/src/run.ts index 09825491dc3e..a5c59759b2c6 100644 --- a/packages/runner/src/run.ts +++ b/packages/runner/src/run.ts @@ -898,7 +898,8 @@ export async function runSuite(suite: Suite, runner: VitestRunner): Promise runSuiteChild(c, runner))) + const groupLimiter = limitConcurrency(runner.config.maxConcurrency) + await Promise.all(tasksGroup.map(c => groupLimiter(() => runSuiteChild(c, runner)))) } else { const { sequence } = runner.config diff --git a/test/cli/test/concurrent.test.ts b/test/cli/test/concurrent.test.ts index 80601067a554..dbbb19cfac7c 100644 --- a/test/cli/test/concurrent.test.ts +++ b/test/cli/test/concurrent.test.ts @@ -1074,3 +1074,296 @@ test('aroundAll enforces teardown timeout when inner error is caught', async () } `) }) + +function extractLogs(log: string) { + const result = log.split('\n').filter(line => line.match(/^![<>]/)).join('\n') + return `\n${result.trim()}\n` +} + +test('sibling task sequential lifecycle guarantee', async () => { + const result = await runInlineTests({ + 'basic.test.ts': ` +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +beforeEach(async ({ task }) => { + console.log("!> beforeEach", task.name) + await sleep(10) + console.log("!< beforeEach", task.name) +}) + +afterEach(async ({ task }) => { + console.log("!> afterEach", task.name) + await sleep(10) + console.log("!< afterEach", task.name) +}) + +test.concurrent.for(["a", "b"])("%s", async (_, { task }) => { + console.log("!> test", task.name) + await sleep(10) + console.log("!< test", task.name) +}) +`, + }, { + maxConcurrency: 1, + globals: true, + }) + + expect(extractLogs(result.stdout)).toMatchInlineSnapshot(` + " + !> beforeEach a + !< beforeEach a + !> test a + !< test a + !> afterEach a + !< afterEach a + !> beforeEach b + !< beforeEach b + !> test b + !< test b + !> afterEach b + !< afterEach b + " + `) + expect(result.errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "a": "passed", + "b": "passed", + }, + } + `) +}) + +test('sibling suite sequential lifecycle guarantee', async () => { + const result = await runInlineTests({ + 'basic.test.ts': ` +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +describe.for(["a", "b"])("%s", { concurrent: true }, () => { + beforeAll(async ({}, suite) => { + console.log("!> beforeAll", suite.name) + await sleep(10) + console.log("!< beforeAll", suite.name) + }) + + afterAll(async ({}, suite) => { + console.log("!> afterAll", suite.name) + await sleep(10) + console.log("!< afterAll", suite.name) + }) + + test.concurrent("test", async ({ task }) => { + console.log("!> test", task.suite.name) + await sleep(10) + console.log("!< test", task.suite.name) + }) +}) +`, + }, { + maxConcurrency: 1, + globals: true, + }) + + expect(extractLogs(result.stdout)).toMatchInlineSnapshot(` + " + !> beforeAll a + !< beforeAll a + !> test a + !< test a + !> afterAll a + !< afterAll a + !> beforeAll b + !< beforeAll b + !> test b + !< test b + !> afterAll b + !< afterAll b + " + `) + expect(result.errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "a": { + "test": "passed", + }, + "b": { + "test": "passed", + }, + }, + } + `) +}) + +// we could enforce this by adding yet another limit globally at `runTest` +// (like we originally had before https://github.com/vitest-dev/vitest/pull/9653) +// but there's no way to achieve the same for deep suite-level hooks anyways, +// so we don't do that (yet). +test('non-sibling test sequential lifecycle non-guarantee', async () => { + const result = await runInlineTests({ + 'basic.test.ts': ` +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +describe.for(["a0", "a1"])("%s", { concurrent: true }, () => { + describe.for(["b0", "b1"])("%s", { concurrent: true }, () => { + beforeEach(async ({ task }) => { + console.log("!> beforeEach", task.suite.suite.name, task.suite.name, task.name) + await sleep(10) + console.log("!< beforeEach", task.suite.suite.name, task.suite.name, task.name) + }) + + afterEach(async ({ task }) => { + console.log("!> afterEach", task.suite.suite.name, task.suite.name, task.name) + await sleep(10) + console.log("!< afterEach", task.suite.suite.name, task.suite.name, task.name) + }) + + test("test", async ({ task }) => { + console.log("!> test", task.suite.suite.name,task.suite.name, task.name) + await sleep(10) + console.log("!< test", task.suite.suite.name,task.suite.name, task.name) + }) + }) +}) +`, + }, { + maxConcurrency: 2, + globals: true, + }) + + expect(extractLogs(result.stdout)).toMatchInlineSnapshot(` + " + !> beforeEach a0 b0 test + !> beforeEach a0 b1 test + !< beforeEach a0 b0 test + !> beforeEach a1 b0 test + !< beforeEach a0 b1 test + !> beforeEach a1 b1 test + !< beforeEach a1 b0 test + !> test a0 b0 test + !< beforeEach a1 b1 test + !> test a0 b1 test + !< test a0 b0 test + !> test a1 b0 test + !< test a0 b1 test + !> test a1 b1 test + !< test a1 b0 test + !> afterEach a0 b0 test + !< test a1 b1 test + !> afterEach a0 b1 test + !< afterEach a0 b0 test + !> afterEach a1 b0 test + !< afterEach a0 b1 test + !> afterEach a1 b1 test + !< afterEach a1 b0 test + !< afterEach a1 b1 test + " + `) + + expect(result.errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "a0": { + "b0": { + "test": "passed", + }, + "b1": { + "test": "passed", + }, + }, + "a1": { + "b0": { + "test": "passed", + }, + "b1": { + "test": "passed", + }, + }, + }, + } + `) +}) + +test('non-sibling suite sequential lifecycle non-guarantee', async () => { + const result = await runInlineTests({ + 'basic.test.ts': ` +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + +describe.for(["a0", "a1"])("%s", { concurrent: true }, () => { + describe.for(["b0", "b1"])("%s", { concurrent: true }, () => { + beforeAll(async ({}, suite) => { + console.log("!> beforeAll", suite.suite.name, suite.name) + await sleep(10) + console.log("!< beforeAll", suite.suite.name, suite.name) + }) + + afterAll(async ({}, suite) => { + console.log("!> afterAll", suite.suite.name, suite.name) + await sleep(10) + console.log("!< afterAll", suite.suite.name, suite.name) + }) + + test("test", async ({ task }) => { + console.log("!> test", task.suite.suite.name, task.suite.name, task.name) + await sleep(10) + console.log("!< test", task.suite.suite.name, task.suite.name, task.name) + }) + }) +}) +`, + }, { + maxConcurrency: 2, + globals: true, + }) + + expect(extractLogs(result.stdout)).toMatchInlineSnapshot(` + " + !> beforeAll a0 b0 + !> beforeAll a0 b1 + !< beforeAll a0 b0 + !> beforeAll a1 b0 + !< beforeAll a0 b1 + !> beforeAll a1 b1 + !< beforeAll a1 b0 + !> test a0 b0 test + !< beforeAll a1 b1 + !> test a0 b1 test + !< test a0 b0 test + !> test a1 b0 test + !< test a0 b1 test + !> test a1 b1 test + !< test a1 b0 test + !> afterAll a0 b0 + !< test a1 b1 test + !> afterAll a0 b1 + !< afterAll a0 b0 + !> afterAll a1 b0 + !< afterAll a0 b1 + !> afterAll a1 b1 + !< afterAll a1 b0 + !< afterAll a1 b1 + " + `) + + expect(result.errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "a0": { + "b0": { + "test": "passed", + }, + "b1": { + "test": "passed", + }, + }, + "a1": { + "b0": { + "test": "passed", + }, + "b1": { + "test": "passed", + }, + }, + }, + } + `) +})