diff --git a/packages/vitest/src/runtime/runner/fixture.ts b/packages/vitest/src/runtime/runner/fixture.ts index bc82d7537..98a0b488b 100644 --- a/packages/vitest/src/runtime/runner/fixture.ts +++ b/packages/vitest/src/runtime/runner/fixture.ts @@ -426,8 +426,12 @@ export function withFixtures(fn: Function, options?: WithFixturesOptions) { } cachedFixtures.add(fixture) - const resolvedValue = await resolveTestFixtureValue(fixture, context, cleanupFnArray) - context[fixture.name] = resolvedValue + try { + context[fixture.name] = await resolveTestFixtureValue(fixture, context, cleanupFnArray) + } catch (error) { + cachedFixtures.delete(fixture) + throw error + } cleanupFnArray.push(() => { cachedFixtures.delete(fixture) @@ -503,11 +507,14 @@ async function resolveScopeFixtureValue( fixture.name, fixture.scope === 'file' ? { ...workerContext, ...fileContext } : fixtureContext, cleanupFnFileArray, - ).then((value) => { - fixtureContext[fixture.name] = value - scopedFixturePromiseCache.delete(fixture) - return value - }) + ) + .then((value) => { + fixtureContext[fixture.name] = value + return value + }) + .finally(() => { + scopedFixturePromiseCache.delete(fixture) + }) scopedFixturePromiseCache.set(fixture, promise) return promise } diff --git a/test/e2e/test/fixture-failed-setup.test.ts b/test/e2e/test/fixture-failed-setup.test.ts new file mode 100644 index 000000000..546fc1dd7 --- /dev/null +++ b/test/e2e/test/fixture-failed-setup.test.ts @@ -0,0 +1,116 @@ +import { expect, test } from 'vitest' +import { runInlineTests } from '../../test-utils' + +test('file fixture whose setup failed is set up again on retry', async () => { + const { stdout, errorTree } = await runInlineTests({ + 'basic.test.ts': ` + import { expect, test } from 'vitest' + + let attempts = 0 + + const extended = test.extend<{ file: string }>({ + file: [ + async ({}, use) => { + attempts++ + console.log('>> init file', attempts) + if (attempts === 1) { + throw new Error('setup failed once') + } + await use('file') + }, + { scope: 'file' }, + ], + }) + + extended('test1', { retry: 1 }, ({ file }) => { + expect(file).toBe('file') + }) + `, + }) + + expect(getFixtureLogs(stdout)).toMatchInlineSnapshot(` + ">> init file 1 + >> init file 2" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "basic.test.ts": { + "test1": "passed", + }, + } + `) +}) + +test('worker fixture whose setup failed does not poison the next file', async () => { + const { stdout, errorTree } = await runInlineTests( + { + 'fixture.ts': ` + import { test } from 'vitest' + + let attempts = 0 + + export const extended = test.extend<{ worker: string }>({ + worker: [ + async ({}, use) => { + attempts++ + console.log('>> init worker', attempts) + if (attempts === 1) { + throw new Error('setup failed once') + } + await use('worker') + }, + { scope: 'worker' }, + ], + }) + `, + '1-basic.test.ts': ` + import { extended } from './fixture' + extended('test1', ({ worker: _worker }) => {}) + `, + '2-basic.test.ts': ` + import { extended } from './fixture' + extended('test1', ({ worker: _worker }) => {}) + `, + }, + { + isolate: false, + maxWorkers: 1, + pool: 'threads', + sequence: { sequencer: StableTestFileOrderSorter }, + }, + ) + + expect(getFixtureLogs(stdout)).toMatchInlineSnapshot(` + ">> init worker 1 + >> init worker 2" + `) + expect(errorTree()).toMatchInlineSnapshot(` + { + "1-basic.test.ts": { + "test1": [ + "setup failed once", + ], + }, + "2-basic.test.ts": { + "test1": "passed", + }, + } + `) +}) + +function getFixtureLogs(stdout: string) { + return stdout + .split('\n') + .filter((line) => line.startsWith('>> init')) + .join('\n') +} + +class StableTestFileOrderSorter { + sort(files: { moduleId: string }[]) { + return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId)) + } + + shard(files: { moduleId: string }[]) { + return files + } +} diff --git a/test/unit/test/fixture-retry.test.ts b/test/unit/test/fixture-retry.test.ts new file mode 100644 index 000000000..6c341b3c1 --- /dev/null +++ b/test/unit/test/fixture-retry.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from 'vitest' + +let attempts = 0 + +const flaky = test.extend<{ value: string }>({ + value: async ({}, use) => { + attempts++ + if (attempts === 1) { + throw new Error('setup failed once') + } + await use('ready') + }, +}) + +flaky('fixture setup runs again on retry', { retry: 1 }, ({ value }) => { + expect(attempts).toBe(2) + expect(value).toBe('ready') +})