[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(vitest): allow per-file and per-worker fixtures (#7704)

Co-authored-by: Ari Perkkiö <ari.perkkio@gmail.com>

authored by

Vladimir
Ari Perkkiö
and committed by
GitHub
(May 31, 2025, 2:07 PM +0200) 9cbfc231 407c0e4d

+1182 -197
+40
docs/guide/test-context.md
··· 373 373 }) 374 374 ``` 375 375 376 + #### Per-Scope Context <Version>3.2.0</Version> 377 + 378 + You can define context that will be initiated once per file or a worker. It is initiated the same way as a regular fixture with an objects parameter: 379 + 380 + ```ts 381 + import { test as baseTest } from 'vitest' 382 + 383 + export const test = baseTest.extend({ 384 + perFile: [ 385 + ({}, { use }) => use([]), 386 + { scope: 'file' }, 387 + ], 388 + perWorker: [ 389 + ({}, { use }) => use([]), 390 + { scope: 'worker' }, 391 + ], 392 + }) 393 + ``` 394 + 395 + The value is initialised the first time any test has accessed it, unless the fixture options have `auto: true` - in this case the value is initialised before any test has run. 396 + 397 + ```ts 398 + const test = baseTest.extend({ 399 + perFile: [ 400 + ({}, { use }) => use([]), 401 + { 402 + scope: 'file', 403 + // always run this hook before any test 404 + auto: true 405 + }, 406 + ], 407 + }) 408 + ``` 409 + 410 + The `worker` scope will run the fixture once per worker. The number of running workers depends on various factors. By default, every file runs in a separate worker, so `file` and `worker` scopes work the same way. 411 + 412 + However, if you disable [isolation](/config/#isolate), then the number of workers is limited by the [`maxWorkers`](/config/#maxworkers) or [`poolOptions`](/config/#pooloptions) configuration. 413 + 414 + Note that specifying `scope: 'worker'` when running tests in `vmThreads` or `vmForks` will work the same way as `scope: 'file'`. This limitation exists because every test file has its own VM context, so if Vitest were to initiate it once, one context could leak to another and create many reference inconsistencies (instances of the same class would reference different constructors, for example). 415 + 376 416 #### TypeScript 377 417 378 418 To provide fixture types for all your custom contexts, you can pass the fixtures type as a generic.
+1
test/cli/vitest.config.ts
··· 6 6 includeTaskLocation: true, 7 7 reporters: ['verbose'], 8 8 testTimeout: 60_000, 9 + globals: true, 9 10 poolOptions: { 10 11 forks: { 11 12 singleFork: true,
+34 -11
test/test-utils/index.ts
··· 77 77 const { reporters, ...rest } = cliOptions 78 78 79 79 ctx = await startVitest(mode, cliFilters, { 80 + // Test cases are already run with multiple forks/threads 81 + maxWorkers: 1, 82 + minWorkers: 1, 83 + 80 84 watch: false, 81 85 // "none" can be used to disable passing "reporter" option so that default value is used (it's not same as reporters: ["default"]) 82 86 ...(reporters === 'none' ? {} : reporters ? { reporters } : { reporters: ['verbose'] }), ··· 85 89 NO_COLOR: 'true', 86 90 ...rest.env, 87 91 }, 88 - 89 - // Test cases are already run with multiple forks/threads 90 - maxWorkers: 1, 91 - minWorkers: 1, 92 92 }, { 93 93 ...viteOverrides, 94 94 server: { ··· 273 273 return resolve(dirname(filename), path) 274 274 } 275 275 276 - export type TestFsStructure = Record<string, string | ViteUserConfig | WorkspaceProjectConfiguration[]> 276 + export type TestFsStructure = Record< 277 + string, 278 + | string 279 + | ViteUserConfig 280 + | WorkspaceProjectConfiguration[] 281 + | ((...args: any[]) => unknown) 282 + | [(...args: any[]) => unknown, { exports?: string[]; imports?: Record<string, string[]> }] 283 + > 277 284 278 - export function useFS(root: string, structure: TestFsStructure) { 285 + function getGeneratedFileContent(content: TestFsStructure[string]) { 286 + if (typeof content === 'string') { 287 + return content 288 + } 289 + if (typeof content === 'function') { 290 + return `await (${content})()` 291 + } 292 + if (Array.isArray(content) && typeof content[1] === 'object' && ('exports' in content[1] || 'imports' in content[1])) { 293 + const imports = Object.entries(content[1].imports || []) 294 + return ` 295 + ${imports.map(([path, is]) => `import { ${is.join(', ')} } from '${path}'`)} 296 + const results = await (${content[0]})({ ${imports.flatMap(([_, is]) => is).join(', ')} }) 297 + ${(content[1].exports || []).map(e => `export const ${e} = results["${e}"]`)} 298 + ` 299 + } 300 + return `export default ${JSON.stringify(content)}` 301 + } 302 + 303 + export function useFS<T extends TestFsStructure>(root: string, structure: T) { 279 304 const files = new Set<string>() 280 305 const hasConfig = Object.keys(structure).some(file => file.includes('.config.')) 281 306 if (!hasConfig) { 282 - structure['./vitest.config.js'] = {} 307 + ;(structure as any)['./vitest.config.js'] = {} 283 308 } 284 309 for (const file in structure) { 285 310 const filepath = resolve(root, file) 286 311 files.add(filepath) 287 - const content = typeof structure[file] === 'string' 288 - ? structure[file] 289 - : `export default ${JSON.stringify(structure[file])}` 312 + const content = getGeneratedFileContent(structure[file]) 290 313 fs.mkdirSync(dirname(filepath), { recursive: true }) 291 314 fs.writeFileSync(filepath, String(content), 'utf-8') 292 315 } ··· 318 341 } 319 342 320 343 export async function runInlineTests( 321 - structure: Record<string, string | ViteUserConfig | WorkspaceProjectConfiguration[]>, 344 + structure: TestFsStructure, 322 345 config?: UserConfig, 323 346 options?: VitestRunnerCLIOptions, 324 347 viteOverrides: ViteUserConfig = {},
+15
packages/runner/src/context.ts
··· 1 1 import type { Awaitable } from '@vitest/utils' 2 2 import type { VitestRunner } from './types/runner' 3 3 import type { 4 + File, 4 5 RuntimeContext, 5 6 SuiteCollector, 6 7 Test, ··· 269 270 error.stack = stackTraceError.stack.replace(error.message, stackTraceError.message) 270 271 } 271 272 return error 273 + } 274 + 275 + const fileContexts = new WeakMap<File, Record<string, unknown>>() 276 + 277 + export function getFileContext(file: File): Record<string, unknown> { 278 + const context = fileContexts.get(file) 279 + if (!context) { 280 + throw new Error(`Cannot find file context for ${file.name}`) 281 + } 282 + return context 283 + } 284 + 285 + export function setFileContext(file: File, context: Record<string, unknown>): void { 286 + fileContexts.set(file, context) 272 287 } 273 288 274 289 const table: string[] = []
+111 -12
packages/runner/src/fixture.ts
··· 1 + import type { VitestRunner } from './types' 1 2 import type { FixtureOptions, TestContext } from './types/tasks' 2 3 import { createDefer, isObject } from '@vitest/utils' 4 + import { getFileContext } from './context' 3 5 import { getTestFixture } from './map' 4 6 5 7 export interface FixtureItem extends FixtureOptions { 6 8 prop: string 7 9 value: any 10 + scope: 'test' | 'file' | 'worker' 8 11 /** 9 12 * Indicates whether the fixture is a function 10 13 */ ··· 43 46 export function mergeContextFixtures<T extends { fixtures?: FixtureItem[] }>( 44 47 fixtures: Record<string, any>, 45 48 context: T, 46 - inject: (key: string) => unknown, 49 + runner: VitestRunner, 47 50 ): T { 48 - const fixtureOptionKeys = ['auto', 'injected'] 51 + const fixtureOptionKeys = ['auto', 'injected', 'scope'] 49 52 const fixtureArray: FixtureItem[] = Object.entries(fixtures).map( 50 53 ([prop, value]) => { 51 54 const fixtureItem = { value } as FixtureItem ··· 60 63 Object.assign(fixtureItem, value[1]) 61 64 const userValue = value[0] 62 65 fixtureItem.value = fixtureItem.injected 63 - ? (inject(prop) ?? userValue) 66 + ? (runner.injectValue?.(prop) ?? userValue) 64 67 : userValue 65 68 } 66 69 70 + fixtureItem.scope = fixtureItem.scope || 'test' 71 + if (fixtureItem.scope === 'worker' && !runner.getWorkerContext) { 72 + fixtureItem.scope = 'file' 73 + } 67 74 fixtureItem.prop = prop 68 75 fixtureItem.isFn = typeof fixtureItem.value === 'function' 69 76 return fixtureItem ··· 86 93 ({ prop }) => prop !== fixture.prop && usedProps.includes(prop), 87 94 ) 88 95 } 96 + // test can access anything, so we ignore it 97 + if (fixture.scope !== 'test') { 98 + fixture.deps?.forEach((dep) => { 99 + if (!dep.isFn) { 100 + // non fn fixtures are always resolved and available to anyone 101 + return 102 + } 103 + // worker scope can only import from worker scope 104 + if (fixture.scope === 'worker' && dep.scope === 'worker') { 105 + return 106 + } 107 + // file scope an import from file and worker scopes 108 + if (fixture.scope === 'file' && dep.scope !== 'test') { 109 + return 110 + } 111 + 112 + throw new SyntaxError(`cannot use the ${dep.scope} fixture "${dep.prop}" inside the ${fixture.scope} fixture "${fixture.prop}"`) 113 + }) 114 + } 89 115 } 90 116 }) 91 117 ··· 94 120 95 121 const fixtureValueMaps = new Map<TestContext, Map<FixtureItem, any>>() 96 122 const cleanupFnArrayMap = new Map< 97 - TestContext, 123 + object, 98 124 Array<() => void | Promise<void>> 99 125 >() 100 126 101 - export async function callFixtureCleanup(context: TestContext): Promise<void> { 127 + export async function callFixtureCleanup(context: object): Promise<void> { 102 128 const cleanupFnArray = cleanupFnArrayMap.get(context) ?? [] 103 129 for (const cleanup of cleanupFnArray.reverse()) { 104 130 await cleanup() ··· 106 132 cleanupFnArrayMap.delete(context) 107 133 } 108 134 109 - export function withFixtures(fn: Function, testContext?: TestContext) { 135 + export function withFixtures(runner: VitestRunner, fn: Function, testContext?: TestContext) { 110 136 return (hookContext?: TestContext): any => { 111 137 const context: (TestContext & { [key: string]: any }) | undefined 112 138 = hookContext || testContext ··· 153 179 continue 154 180 } 155 181 156 - const resolvedValue = fixture.isFn 157 - ? await resolveFixtureFunction(fixture.value, context, cleanupFnArray) 158 - : fixture.value 182 + const resolvedValue = await resolveFixtureValue( 183 + runner, 184 + fixture, 185 + context!, 186 + cleanupFnArray, 187 + ) 159 188 context![fixture.prop] = resolvedValue 160 189 fixtureValueMap.set(fixture, resolvedValue) 161 - cleanupFnArray.unshift(() => { 162 - fixtureValueMap.delete(fixture) 163 - }) 190 + 191 + if (fixture.scope === 'test') { 192 + cleanupFnArray.unshift(() => { 193 + fixtureValueMap.delete(fixture) 194 + }) 195 + } 164 196 } 165 197 } 166 198 167 199 return resolveFixtures().then(() => fn(context)) 168 200 } 201 + } 202 + 203 + const globalFixturePromise = new WeakMap<FixtureItem, Promise<unknown>>() 204 + 205 + function resolveFixtureValue( 206 + runner: VitestRunner, 207 + fixture: FixtureItem, 208 + context: TestContext & { [key: string]: any }, 209 + cleanupFnArray: (() => void | Promise<void>)[], 210 + ) { 211 + const fileContext = getFileContext(context.task.file) 212 + const workerContext = runner.getWorkerContext?.() 213 + 214 + if (!fixture.isFn) { 215 + fileContext[fixture.prop] ??= fixture.value 216 + if (workerContext) { 217 + workerContext[fixture.prop] ??= fixture.value 218 + } 219 + return fixture.value 220 + } 221 + 222 + if (fixture.scope === 'test') { 223 + return resolveFixtureFunction( 224 + fixture.value, 225 + context, 226 + cleanupFnArray, 227 + ) 228 + } 229 + 230 + // in case the test runs in parallel 231 + if (globalFixturePromise.has(fixture)) { 232 + return globalFixturePromise.get(fixture)! 233 + } 234 + 235 + let fixtureContext: Record<string, unknown> 236 + 237 + if (fixture.scope === 'worker') { 238 + if (!workerContext) { 239 + throw new TypeError('[@vitest/runner] The worker context is not available in the current test runner. Please, provide the `getWorkerContext` method when initiating the runner.') 240 + } 241 + fixtureContext = workerContext 242 + } 243 + else { 244 + fixtureContext = fileContext 245 + } 246 + 247 + if (fixture.prop in fixtureContext) { 248 + return fixtureContext[fixture.prop] 249 + } 250 + 251 + if (!cleanupFnArrayMap.has(fixtureContext)) { 252 + cleanupFnArrayMap.set(fixtureContext, []) 253 + } 254 + const cleanupFnFileArray = cleanupFnArrayMap.get(fixtureContext)! 255 + 256 + const promise = resolveFixtureFunction( 257 + fixture.value, 258 + fixtureContext, 259 + cleanupFnFileArray, 260 + ).then((value) => { 261 + fixtureContext[fixture.prop] = value 262 + globalFixturePromise.delete(fixture) 263 + return value 264 + }) 265 + 266 + globalFixturePromise.set(fixture, promise) 267 + return promise 169 268 } 170 269 171 270 async function resolveFixtureFunction(
+4 -2
packages/runner/src/hooks.ts
··· 139 139 ): void { 140 140 assertTypes(fn, '"beforeEach" callback', ['function']) 141 141 const stackTraceError = new Error('STACK_TRACE_ERROR') 142 + const runner = getRunner() 142 143 return getCurrentSuite<ExtraContext>().on( 143 144 'beforeEach', 144 145 Object.assign( 145 146 withTimeout( 146 - withFixtures(fn), 147 + withFixtures(runner, fn), 147 148 timeout ?? getDefaultHookTimeout(), 148 149 true, 149 150 stackTraceError, ··· 179 180 timeout?: number, 180 181 ): void { 181 182 assertTypes(fn, '"afterEach" callback', ['function']) 183 + const runner = getRunner() 182 184 return getCurrentSuite<ExtraContext>().on( 183 185 'afterEach', 184 186 withTimeout( 185 - withFixtures(fn), 187 + withFixtures(runner, fn), 186 188 timeout ?? getDefaultHookTimeout(), 187 189 true, 188 190 new Error('STACK_TRACE_ERROR'),
+17 -1
packages/runner/src/run.ts
··· 20 20 import { shuffle } from '@vitest/utils' 21 21 import { processError } from '@vitest/utils/error' 22 22 import { collectTests } from './collect' 23 - import { abortContextSignal } from './context' 23 + import { abortContextSignal, getFileContext } from './context' 24 24 import { PendingError, TestRunAbortError } from './errors' 25 25 import { callFixtureCleanup } from './fixture' 26 26 import { getBeforeHookCleanupCallback } from './hooks' ··· 537 537 try { 538 538 await callSuiteHook(suite, suite, 'afterAll', runner, [suite]) 539 539 await callCleanupHooks(runner, beforeAllCleanups) 540 + if (suite.file === suite) { 541 + const context = getFileContext(suite as File) 542 + await callFixtureCleanup(context) 543 + } 540 544 } 541 545 catch (e) { 542 546 failTask(suite.result, e, runner.config.diffOptions) ··· 598 602 } 599 603 } 600 604 605 + const workerRunners = new WeakSet<VitestRunner>() 606 + 601 607 export async function startTests(specs: string[] | FileSpecification[], runner: VitestRunner): Promise<File[]> { 602 608 const cancel = runner.cancel?.bind(runner) 603 609 // Ideally, we need to have an event listener for this, but only have a runner here. ··· 609 615 abortContextSignal(test.context, error), 610 616 ) 611 617 return cancel?.(reason) 618 + } 619 + 620 + if (!workerRunners.has(runner)) { 621 + runner.onCleanupWorkerContext?.(async () => { 622 + const context = runner.getWorkerContext?.() 623 + if (context) { 624 + await callFixtureCleanup(context) 625 + } 626 + }) 627 + workerRunners.add(runner) 612 628 } 613 629 614 630 try {
+3 -3
packages/runner/src/suite.ts
··· 355 355 setFn( 356 356 task, 357 357 withTimeout( 358 - withAwaitAsyncAssertions(withFixtures(handler, context), task), 358 + withAwaitAsyncAssertions(withFixtures(runner, handler, context), task), 359 359 timeout, 360 360 false, 361 361 stackTraceError, ··· 424 424 const parsed = mergeContextFixtures( 425 425 fixtures, 426 426 { fixtures: collectorFixtures }, 427 - (key: string) => getRunner().injectValue?.(key), 427 + runner, 428 428 ) 429 429 if (parsed.fixtures) { 430 430 collectorFixtures = parsed.fixtures ··· 767 767 const _context = mergeContextFixtures( 768 768 fixtures, 769 769 context || {}, 770 - (key: string) => getRunner().injectValue?.(key), 770 + runner, 771 771 ) 772 772 773 773 return createTest(function fn(
+757
test/cli/test/scoped-fixtures.test.ts
··· 1 + /// <reference types="vitest/globals" /> 2 + 3 + import type { TestAPI } from 'vitest' 4 + import type { ViteUserConfig } from 'vitest/config' 5 + import type { TestSpecification, UserConfig } from 'vitest/node' 6 + import type { TestFsStructure } from '../../test-utils' 7 + import { runInlineTests } from '../../test-utils' 8 + 9 + interface TestContext { 10 + file: string 11 + worker: string 12 + } 13 + 14 + test('test fixture cannot import from file fixture', async () => { 15 + const { stderr } = await runInlineTests({ 16 + 'basic.test.ts': () => { 17 + const extendedTest = it.extend<{ 18 + file: string 19 + local: string 20 + }>({ 21 + local: ({}, use) => use('local'), 22 + file: [ 23 + ({ local }, use) => use(local), 24 + { scope: 'file' }, 25 + ], 26 + }) 27 + 28 + extendedTest('not working', ({ file: _file }) => {}) 29 + }, 30 + }, { globals: true }) 31 + expect(stderr).toContain('cannot use the test fixture "local" inside the file fixture "file"') 32 + }) 33 + 34 + test('can import file fixture inside the local fixture', async () => { 35 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ 36 + file: string 37 + local: string 38 + }>({ 39 + local: async ({ file }, use) => { 40 + log('init local') 41 + await use(file) 42 + log('teardown local') 43 + }, 44 + file: [ 45 + async ({}, use) => { 46 + log('init file') 47 + await use('file') 48 + log('teardown file') 49 + }, 50 + { scope: 'file' }, 51 + ], 52 + }), { 53 + 'basic.test.ts': ({ extendedTest }) => { 54 + extendedTest('test1', ({ local: _local }) => {}) 55 + }, 56 + }) 57 + 58 + expect(stderr).toBe('') 59 + expect(fixtures).toMatchInlineSnapshot(` 60 + ">> fixture | init file | test1 61 + >> fixture | init local | test1 62 + >> fixture | teardown local | test1 63 + >> fixture | teardown file | test1" 64 + `) 65 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > test1 <time>"`) 66 + }) 67 + 68 + test('can import worker fixture inside the local fixture', async () => { 69 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ 70 + worker: string 71 + local: string 72 + }>({ 73 + local: async ({ worker }, use) => { 74 + log('init local') 75 + await use(worker) 76 + log('teardown local') 77 + }, 78 + worker: [ 79 + async ({}, use) => { 80 + log('init worker') 81 + await use('worker') 82 + log('teardown worker') 83 + }, 84 + { scope: 'worker' }, 85 + ], 86 + }), { 87 + 'basic.test.ts': ({ extendedTest }) => { 88 + extendedTest('test1', ({ local }) => { 89 + expect(local).toBe('worker') 90 + }) 91 + }, 92 + }) 93 + 94 + expect(stderr).toBe('') 95 + expect(fixtures).toMatchInlineSnapshot(` 96 + ">> fixture | init worker | test1 97 + >> fixture | init local | test1 98 + >> fixture | teardown local | test1 99 + >> fixture | teardown worker | test1" 100 + `) 101 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > test1 <time>"`) 102 + }) 103 + 104 + test('test fixture cannot import from worker fixture', async () => { 105 + const { stderr } = await runInlineTests({ 106 + 'basic.test.ts': () => { 107 + const extendedTest = it.extend<{ 108 + worker: string 109 + local: string 110 + }>({ 111 + local: ({}, use) => use('local'), 112 + worker: [ 113 + ({ local }, use) => use(local), 114 + { scope: 'worker' }, 115 + ], 116 + }) 117 + 118 + extendedTest('not working', ({ worker: _worker }) => {}) 119 + }, 120 + }, { globals: true }) 121 + expect(stderr).toContain('cannot use the test fixture "local" inside the worker fixture "worker"') 122 + }) 123 + 124 + test('auto worker fixture is initialised always before the first test', async () => { 125 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ worker: string }>({ 126 + worker: [ 127 + async ({}, use) => { 128 + log('init file') 129 + await use('worker') 130 + log('teardown file') 131 + }, 132 + { scope: 'worker', auto: true }, 133 + ], 134 + }), { 135 + 'basic.test.ts': ({ extendedTest }) => { 136 + extendedTest('test1', ({}) => {}) 137 + extendedTest('test2', ({}) => {}) 138 + extendedTest('test3', ({ worker: _worker }) => {}) 139 + extendedTest('test4', ({}) => {}) 140 + }, 141 + }) 142 + 143 + expect(stderr).toBe('') 144 + expect(fixtures).toMatchInlineSnapshot(` 145 + ">> fixture | init file | test1 146 + >> fixture | teardown file | test4" 147 + `) 148 + expect(tests).toMatchInlineSnapshot(` 149 + " ✓ basic.test.ts > test1 <time> 150 + ✓ basic.test.ts > test2 <time> 151 + ✓ basic.test.ts > test3 <time> 152 + ✓ basic.test.ts > test4 <time>" 153 + `) 154 + }) 155 + 156 + test('worker fixture can import a static value from test fixture', async () => { 157 + const { stderr, stdout } = await runInlineTests({ 158 + 'basic.test.ts': () => { 159 + const extendedTest = it.extend<{ 160 + worker: string 161 + local: string 162 + }>({ 163 + local: 'local', 164 + worker: [ 165 + ({ local }, use) => use(local), 166 + { scope: 'worker' }, 167 + ], 168 + }) 169 + 170 + extendedTest('working', ({ worker, local }) => { 171 + expect(worker).toBe(local) 172 + expect(worker).toBe('local') 173 + }) 174 + }, 175 + }, { globals: true }) 176 + expect(stdout).toContain('basic.test.ts') 177 + expect(stderr).toBe('') 178 + }) 179 + 180 + test('file fixture can import a static value from test fixture', async () => { 181 + const { stderr, stdout } = await runInlineTests({ 182 + 'basic.test.ts': () => { 183 + const extendedTest = it.extend<{ 184 + file: string 185 + local: string 186 + }>({ 187 + local: 'local', 188 + file: [ 189 + ({ local }, use) => use(local), 190 + { scope: 'file' }, 191 + ], 192 + }) 193 + 194 + extendedTest('working', ({ file, local }) => { 195 + expect(file).toBe(local) 196 + expect(file).toBe('local') 197 + }) 198 + }, 199 + }, { globals: true }) 200 + expect(stdout).toContain('basic.test.ts') 201 + expect(stderr).toBe('') 202 + }) 203 + 204 + test('worker fixture works in vmThreads and runs for every file', async () => { 205 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ worker: string }>({ 206 + worker: [ 207 + async ({}, use) => { 208 + log('init worker') 209 + await use('worker') 210 + log('teardown worker') 211 + }, 212 + { scope: 'worker' }, 213 + ], 214 + }), { 215 + '1-basic.test.ts': ({ extendedTest }) => { 216 + extendedTest('test1', ({ worker: _worker }) => {}) 217 + }, 218 + '2-basic.test.ts': ({ extendedTest }) => { 219 + extendedTest('test1', ({ worker: _worker }) => {}) 220 + }, 221 + }) 222 + 223 + expect(stderr).toBe('') 224 + expect(fixtures).toMatchInlineSnapshot(` 225 + ">> fixture | init worker | test1 226 + >> fixture | teardown worker | test1 227 + >> fixture | init worker | test1 228 + >> fixture | teardown worker | test1" 229 + `) 230 + expect(tests).toMatchInlineSnapshot(` 231 + " ✓ 1-basic.test.ts > test1 <time> 232 + ✓ 2-basic.test.ts > test1 <time>" 233 + `) 234 + }) 235 + 236 + test('worker fixtures in isolated tests init and teardown twice', async () => { 237 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ worker: string }>({ 238 + worker: [ 239 + async ({}, use) => { 240 + log('init worker') 241 + await use('worker') 242 + log('teardown worker') 243 + }, 244 + { scope: 'worker' }, 245 + ], 246 + }), { 247 + '1-basic.test.ts': ({ extendedTest }) => { 248 + extendedTest('test1', ({ worker: _worker }) => {}) 249 + }, 250 + '2-basic.test.ts': ({ extendedTest }) => { 251 + extendedTest('test1', ({ worker: _worker }) => {}) 252 + }, 253 + }, { 254 + globals: true, 255 + maxWorkers: 1, 256 + minWorkers: 1, 257 + pool: 'vmThreads', 258 + }) 259 + 260 + expect(stderr).toBe('') 261 + expect(fixtures).toMatchInlineSnapshot(` 262 + ">> fixture | init worker | test1 263 + >> fixture | teardown worker | test1 264 + >> fixture | init worker | test1 265 + >> fixture | teardown worker | test1" 266 + `) 267 + expect(tests).toMatchInlineSnapshot(` 268 + " ✓ 1-basic.test.ts > test1 <time> 269 + ✓ 2-basic.test.ts > test1 <time>" 270 + `) 271 + }) 272 + 273 + test('worker fixture initiates and torn down in different workers', async () => { 274 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ worker: string }>({ 275 + worker: [ 276 + async ({}, use) => { 277 + log('init worker') 278 + await use('worker') 279 + log('teardown worker') 280 + }, 281 + { scope: 'worker' }, 282 + ], 283 + }), { 284 + '1-basic.test.ts': ({ extendedTest }) => { 285 + extendedTest('test1', ({ worker: _worker }) => {}) 286 + }, 287 + '2-basic.test.ts': ({ extendedTest }) => { 288 + extendedTest('test2', ({ worker: _worker }) => {}) 289 + }, 290 + }, { 291 + globals: true, 292 + isolate: false, 293 + maxWorkers: 2, 294 + minWorkers: 2, 295 + pool: 'threads', 296 + }) 297 + 298 + expect(stderr).toBe('') 299 + 300 + // tests run in parallel so we can't guarantee the order 301 + expect(fixtures).toContain(`>> fixture | init worker | test1`) 302 + expect(fixtures).toContain(`>> fixture | teardown worker | test1`) 303 + 304 + expect(fixtures).toContain(`>> fixture | init worker | test2`) 305 + expect(fixtures).toContain(`>> fixture | teardown worker | test2`) 306 + 307 + expect(tests).toContain(' ✓ 1-basic.test.ts > test1 <time>') 308 + expect(tests).toContain(' ✓ 2-basic.test.ts > test2 <time>') 309 + }) 310 + 311 + test('worker fixture initiates and torn down in one non-isolated worker', async () => { 312 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ worker: string }>({ 313 + worker: [ 314 + async ({}, use) => { 315 + log('init worker') 316 + await use('worker') 317 + log('teardown worker') 318 + }, 319 + { scope: 'worker' }, 320 + ], 321 + }), { 322 + '1-basic.test.ts': ({ extendedTest }) => { 323 + extendedTest('test1', ({ worker: _worker }) => {}) 324 + }, 325 + '2-basic.test.ts': ({ extendedTest }) => { 326 + extendedTest('test1', ({ worker: _worker }) => {}) 327 + }, 328 + }, { 329 + globals: true, 330 + isolate: false, 331 + maxWorkers: 1, 332 + minWorkers: 1, 333 + pool: 'threads', 334 + }) 335 + 336 + expect(stderr).toBe('') 337 + expect(fixtures).toMatchInlineSnapshot(` 338 + ">> fixture | init worker | test1 339 + >> fixture | teardown worker | test1" 340 + `) 341 + expect(tests).toMatchInlineSnapshot(` 342 + " ✓ 1-basic.test.ts > test1 <time> 343 + ✓ 2-basic.test.ts > test1 <time>" 344 + `) 345 + }) 346 + 347 + test('worker fixtures are available in beforeEach and afterEach', async () => { 348 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ worker: string }>({ 349 + worker: [ 350 + async ({}, use) => { 351 + log('init worker') 352 + await use('worker') 353 + log('teardown worker') 354 + }, 355 + { scope: 'worker' }, 356 + ], 357 + }), { 358 + 'basic.test.ts': ({ extendedTest }) => { 359 + beforeEach<TestContext>(({ worker }) => { 360 + console.log('>> fixture | beforeEach |', worker) 361 + }) 362 + afterEach<TestContext>(({ worker }) => { 363 + console.log('>> fixture | afterEach |', worker) 364 + }) 365 + extendedTest('test1', ({}) => {}) 366 + extendedTest('test2', ({}) => {}) 367 + }, 368 + }) 369 + 370 + expect(stderr).toBe('') 371 + expect(fixtures).toMatchInlineSnapshot(` 372 + ">> fixture | init worker | test1 373 + >> fixture | beforeEach | worker 374 + >> fixture | afterEach | worker 375 + >> fixture | beforeEach | worker 376 + >> fixture | afterEach | worker 377 + >> fixture | teardown worker | test2" 378 + `) 379 + expect(tests).toMatchInlineSnapshot(` 380 + " ✓ basic.test.ts > test1 <time> 381 + ✓ basic.test.ts > test2 <time>" 382 + `) 383 + }) 384 + 385 + test('file fixtures are available in beforeEach and afterEach', async () => { 386 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ 387 + file: string 388 + }>({ 389 + file: [ 390 + async ({}, use) => { 391 + log('init file') 392 + await use('file') 393 + log('teardown file') 394 + }, 395 + { scope: 'file' }, 396 + ], 397 + }), { 398 + 'basic.test.ts': ({ extendedTest }) => { 399 + beforeEach<TestContext>(({ file }) => { 400 + console.log('>> fixture | beforeEach |', file) 401 + }) 402 + afterEach<TestContext>(({ file }) => { 403 + console.log('>> fixture | afterEach |', file) 404 + }) 405 + extendedTest('test1', ({}) => {}) 406 + extendedTest('test2', ({}) => {}) 407 + }, 408 + }) 409 + 410 + expect(stderr).toBe('') 411 + expect(fixtures).toMatchInlineSnapshot(` 412 + ">> fixture | init file | test1 413 + >> fixture | beforeEach | file 414 + >> fixture | afterEach | file 415 + >> fixture | beforeEach | file 416 + >> fixture | afterEach | file 417 + >> fixture | teardown file | test2" 418 + `) 419 + expect(tests).toMatchInlineSnapshot(` 420 + " ✓ basic.test.ts > test1 <time> 421 + ✓ basic.test.ts > test2 <time>" 422 + `) 423 + }) 424 + 425 + test('auto file fixture is initialised always before the first test', async () => { 426 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ 427 + file: string 428 + }>({ 429 + file: [ 430 + async ({}, use) => { 431 + log('init file') 432 + await use('file') 433 + log('teardown file') 434 + }, 435 + { scope: 'file', auto: true }, 436 + ], 437 + }), { 438 + 'basic.test.ts': ({ extendedTest }) => { 439 + extendedTest('test1', ({}) => {}) 440 + extendedTest('test2', ({}) => {}) 441 + extendedTest('test3', ({ file: _file }) => {}) 442 + extendedTest('test4', ({}) => {}) 443 + }, 444 + }) 445 + 446 + expect(stderr).toBe('') 447 + expect(fixtures).toMatchInlineSnapshot(` 448 + ">> fixture | init file | test1 449 + >> fixture | teardown file | test4" 450 + `) 451 + expect(tests).toMatchInlineSnapshot(` 452 + " ✓ basic.test.ts > test1 <time> 453 + ✓ basic.test.ts > test2 <time> 454 + ✓ basic.test.ts > test3 <time> 455 + ✓ basic.test.ts > test4 <time>" 456 + `) 457 + }) 458 + 459 + test.for([ 460 + true, 461 + false, 462 + ])('file fixture is provided as a factory and is initialised once in all suites, teardown is called once per file (isolate %s)', async (isolate) => { 463 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ file: string }>({ 464 + file: [ 465 + async ({}, use) => { 466 + log('init file') 467 + await use('file') 468 + log('teardown file') 469 + }, 470 + { scope: 'file' }, 471 + ], 472 + }), { 473 + 'basic.test.js': ({ extendedTest }) => { 474 + extendedTest('[first] test 1', ({ file }) => { 475 + expect(file).toBe('file') 476 + }) 477 + 478 + extendedTest('test 2', ({ file }) => { 479 + expect(file).toBe('file') 480 + }) 481 + 482 + extendedTest('test 3', ({ file }) => { 483 + expect(file).toBe('file') 484 + }) 485 + 486 + describe('suite 1', () => { 487 + extendedTest('test 1 1', ({ file }) => { 488 + expect(file).toBe('file') 489 + }) 490 + 491 + extendedTest('test 1 2', ({ file }) => { 492 + expect(file).toBe('file') 493 + }) 494 + 495 + describe('suite 2', () => { 496 + extendedTest('[first] test 1 2 1', ({ file }) => { 497 + expect(file).toBe('file') 498 + }) 499 + }) 500 + }) 501 + }, 502 + 503 + 'second.test.js': ({ extendedTest }) => { 504 + // doesn't access "file", not initialised 505 + extendedTest('[second] test 0', ({}) => {}) 506 + // accesses "file" for the first time, initialised 507 + extendedTest('[second] test 1', ({ file: _file }) => {}) 508 + extendedTest('[second] test 2', ({ file: _file }) => {}) 509 + 510 + describe('suite 1', () => { 511 + extendedTest('[second] test 1', ({ file: _file }) => {}) 512 + }) 513 + }, 514 + 515 + 'third.test.js': ({ extendedTest }) => { 516 + // doesn't access "file" at all 517 + extendedTest('[third] test 0', ({}) => {}) 518 + }, 519 + }, { 520 + globals: true, 521 + isolate, 522 + maxWorkers: 1, 523 + }) 524 + 525 + expect(stderr).toBe('') 526 + 527 + expect(fixtures).toMatchInlineSnapshot(` 528 + ">> fixture | init file | [first] test 1 529 + >> fixture | teardown file | suite 1 > suite 2 > [first] test 1 2 1 530 + >> fixture | init file | [second] test 1 531 + >> fixture | teardown file | suite 1 > [second] test 1" 532 + `) 533 + 534 + expect(tests).toMatchInlineSnapshot(` 535 + " ✓ basic.test.js > [first] test 1 <time> 536 + ✓ basic.test.js > test 2 <time> 537 + ✓ basic.test.js > test 3 <time> 538 + ✓ basic.test.js > suite 1 > test 1 1 <time> 539 + ✓ basic.test.js > suite 1 > test 1 2 <time> 540 + ✓ basic.test.js > suite 1 > suite 2 > [first] test 1 2 1 <time> 541 + ✓ second.test.js > [second] test 0 <time> 542 + ✓ second.test.js > [second] test 1 <time> 543 + ✓ second.test.js > [second] test 2 <time> 544 + ✓ second.test.js > suite 1 > [second] test 1 <time> 545 + ✓ third.test.js > [third] test 0 <time>" 546 + `) 547 + }) 548 + 549 + describe.for([ 550 + { pool: 'forks' }, 551 + { pool: 'threads' }, 552 + { 553 + pool: 'browser', 554 + name: 'core', 555 + browser: { 556 + enabled: true, 557 + provider: 'playwright', 558 + headless: true, 559 + instances: [ 560 + { browser: 'chromium', name: '' }, 561 + ], 562 + }, 563 + }, 564 + ])('works properly in $pool', (options) => { 565 + test('file and worker fixtures are initiated', async () => { 566 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ 567 + file: string 568 + worker: string 569 + }>({ 570 + worker: [ 571 + async ({}, use) => { 572 + log('init worker') 573 + await use('worker') 574 + log('teardown worker') 575 + }, 576 + { scope: 'worker' }, 577 + ], 578 + file: [ 579 + async ({}, use) => { 580 + log('init file') 581 + await use('file') 582 + log('teardown file') 583 + }, 584 + { scope: 'file' }, 585 + ], 586 + }), { 587 + 'basic.test.ts': ({ extendedTest }) => { 588 + extendedTest('test1', ({ file: _file, worker: _worker }) => {}) 589 + }, 590 + 'vitest.config.js': { test: options }, 591 + }) 592 + 593 + expect(stderr).toBe('') 594 + expect(fixtures).toMatchInlineSnapshot(` 595 + ">> fixture | init worker | test1 596 + >> fixture | init file | test1 597 + >> fixture | teardown file | test1 598 + >> fixture | teardown worker | test1" 599 + `) 600 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > test1 <time>"`) 601 + }) 602 + 603 + test('file and worker fixtures are initiated with auto', async () => { 604 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ 605 + file: string 606 + worker: string 607 + }>({ 608 + worker: [ 609 + async ({}, use) => { 610 + log('init worker') 611 + await use('worker') 612 + log('teardown worker') 613 + }, 614 + { scope: 'worker', auto: true }, 615 + ], 616 + file: [ 617 + async ({}, use) => { 618 + log('init file') 619 + await use('file') 620 + log('teardown file') 621 + }, 622 + { scope: 'file', auto: true }, 623 + ], 624 + }), { 625 + 'basic.test.ts': ({ extendedTest }) => { 626 + extendedTest('test1', () => {}) 627 + }, 628 + 'vitest.config.js': { test: options }, 629 + }) 630 + 631 + expect(stderr).toBe('') 632 + expect(fixtures).toMatchInlineSnapshot(` 633 + ">> fixture | init worker | test1 634 + >> fixture | init file | test1 635 + >> fixture | teardown file | test1 636 + >> fixture | teardown worker | test1" 637 + `) 638 + expect(tests).toMatchInlineSnapshot(`" ✓ basic.test.ts > test1 <time>"`) 639 + }) 640 + }) 641 + 642 + describe('browser tests', () => { 643 + test('initiates worker scope once for non-isolated tests', async () => { 644 + const { stderr, fixtures, tests } = await runFixtureTests(({ log }) => it.extend<{ 645 + file: string 646 + worker: string 647 + }>({ 648 + worker: [ 649 + async ({}, use) => { 650 + log('init worker') 651 + await use('worker') 652 + log('teardown worker') 653 + }, 654 + { scope: 'worker' }, 655 + ], 656 + file: [ 657 + async ({}, use) => { 658 + log('init file') 659 + await use('file') 660 + log('teardown file') 661 + }, 662 + { scope: 'file' }, 663 + ], 664 + }), { 665 + '1-basic.test.ts': ({ extendedTest }) => { 666 + extendedTest('test1', ({ file: _file, worker: _worker }) => {}) 667 + }, 668 + '2-basic.test.ts': ({ extendedTest }) => { 669 + extendedTest('test2', ({ file: _file, worker: _worker }) => {}) 670 + }, 671 + 'vitest.config.js': { 672 + test: { 673 + browser: { 674 + enabled: true, 675 + provider: 'playwright', 676 + headless: true, 677 + isolate: false, 678 + instances: [ 679 + { browser: 'chromium' }, 680 + ], 681 + }, 682 + }, 683 + }, 684 + }) 685 + 686 + expect(stderr).toBe('') 687 + expect(fixtures).toMatchInlineSnapshot(` 688 + ">> fixture | init worker | test1 689 + >> fixture | init file | test1 690 + >> fixture | teardown file | test1 691 + >> fixture | init file | test2 692 + >> fixture | teardown file | test2 693 + >> fixture | teardown worker | test2" 694 + `) 695 + expect(tests).toMatchInlineSnapshot(` 696 + " ✓ |chromium| 1-basic.test.ts > test1 <time> 697 + ✓ |chromium| 2-basic.test.ts > test2 <time>" 698 + `) 699 + }) 700 + }) 701 + 702 + async function runFixtureTests<T>( 703 + extendedTest: ({ log }: { log: typeof console.log }) => TestAPI<T>, 704 + fs: Record<string, ((context: { extendedTest: TestAPI<T> }) => unknown) | ViteUserConfig>, 705 + config?: UserConfig, 706 + ) { 707 + if (typeof fs['vitest.config.js'] === 'object') { 708 + fs['vitest.config.js'].test!.globals = true 709 + } 710 + const { stderr, stdout } = await runInlineTests({ 711 + 'test.js': ` 712 + export const extendedTest = (${extendedTest.toString()})({ log: (...args) => console.log('>> fixture |', ...args, '| ' + expect.getState().currentTestName) }) 713 + `, 714 + 'vitest.config.js': { test: { globals: true } }, 715 + ...Object.entries(fs).reduce((acc, [key, value]) => { 716 + if (typeof value === 'object' && !Array.isArray(value)) { 717 + acc[key] = value 718 + } 719 + if (typeof value === 'function') { 720 + acc[key] = [value, { imports: { './test.js': ['extendedTest'] } }] 721 + } 722 + return acc 723 + }, {} as TestFsStructure), 724 + }, { ...config, sequence: { sequencer: StableTestFileOrderSorter } }) 725 + 726 + return { 727 + stderr, 728 + stdout, 729 + fixtures: getFixtureLogs(stdout), 730 + tests: getSuccessTests(stdout), 731 + } 732 + } 733 + 734 + function getSuccessTests(stdout: string) { 735 + return stdout 736 + .split('\n') 737 + .filter(f => f.startsWith(' ✓ ')) 738 + .map(f => f.replace(/\dms/, '<time>')) 739 + .join('\n') 740 + } 741 + 742 + function getFixtureLogs(stdout: string) { 743 + return stdout 744 + .split('\n') 745 + .filter(f => f.startsWith('>> fixture |')) 746 + .join('\n') 747 + } 748 + 749 + class StableTestFileOrderSorter { 750 + sort(files: TestSpecification[]) { 751 + return files.sort((a, b) => a.moduleId.localeCompare(b.moduleId)) 752 + } 753 + 754 + shard(files: TestSpecification[]) { 755 + return files 756 + } 757 + }
+29 -39
test/reporters/src/data.ts
··· 1 - import type { ErrorWithDiff, File, Suite, Task } from 'vitest' 1 + import type { ErrorWithDiff, Suite, Task } from 'vitest' 2 + import { createFileTask } from '@vitest/runner/utils' 2 3 3 - const file: File = { 4 - id: '1223128da3', 5 - name: 'test/core/test/basic.test.ts', 6 - type: 'suite', 7 - meta: {}, 8 - mode: 'run', 9 - filepath: '/vitest/test/core/test/basic.test.ts', 10 - result: { state: 'fail', duration: 145.99284195899963 }, 11 - tasks: [], 12 - projectName: '', 13 - file: null!, 4 + const file = createFileTask( 5 + '/vitest/test/core/test/basic.test.ts', 6 + '/vitest/test/core/test', 7 + '', 8 + ) 9 + file.mode = 'run' 10 + file.result = { 11 + state: 'fail', 12 + duration: 145.99284195899963, 14 13 } 15 - file.file = file 16 14 17 15 const suite: Suite = { 18 - id: '1223128da3_0', 16 + id: `${file.id}_0`, 19 17 type: 'suite', 20 18 name: 'suite', 21 19 mode: 'run', ··· 25 23 tasks: [], 26 24 } 27 25 28 - const passedFile: File = { 29 - id: '1223128da3', 30 - name: 'basic.test.ts', 31 - type: 'suite', 32 - suite, 33 - meta: {}, 34 - mode: 'run', 35 - filepath: '/vitest/test/core/test/basic.test.ts', 36 - result: { state: 'pass', duration: 145.99284195899963 }, 37 - tasks: [ 38 - ], 39 - projectName: '', 40 - file: null!, 41 - } 42 - passedFile.file = passedFile 26 + const passedFile = createFileTask( 27 + '/vitest/test/core/test/basic.test.ts', 28 + '/vitest/test/core/test', 29 + '', 30 + ) 31 + passedFile.mode = 'run' 32 + passedFile.result = { state: 'pass', duration: 145.99284195899963 } 43 33 passedFile.tasks.push({ 44 - id: '1223128da3_0_0', 34 + id: `${file.id}_1`, 45 35 type: 'test', 46 36 name: 'Math.sqrt()', 47 37 mode: 'run', ··· 80 70 81 71 const tasks: Task[] = [ 82 72 { 83 - id: '1223128da3_0_0', 73 + id: `${suite.id}_0`, 84 74 type: 'test', 85 75 name: 'Math.sqrt()', 86 76 mode: 'run', ··· 102 92 context: null as any, 103 93 }, 104 94 { 105 - id: '1223128da3_0_1', 95 + id: `${suite.id}_1`, 106 96 type: 'test', 107 97 name: 'JSON', 108 98 mode: 'run', ··· 116 106 context: null as any, 117 107 }, 118 108 { 119 - id: '1223128da3_0_3', 109 + id: `${suite.id}_3`, 120 110 type: 'test', 121 111 name: 'async with timeout', 122 112 mode: 'skip', ··· 130 120 context: null as any, 131 121 }, 132 122 { 133 - id: '1223128da3_0_4', 123 + id: `${suite.id}_4`, 134 124 type: 'test', 135 125 name: 'timeout', 136 126 annotations: [], ··· 144 134 context: null as any, 145 135 }, 146 136 { 147 - id: '1223128da3_0_5', 137 + id: `${suite.id}_5`, 148 138 type: 'test', 149 139 name: 'callback setup success ', 150 140 mode: 'run', ··· 158 148 context: null as any, 159 149 }, 160 150 { 161 - id: '1223128da3_0_6', 151 + id: `${suite.id}_6`, 162 152 type: 'test', 163 153 name: 'callback test success ', 164 154 mode: 'run', ··· 172 162 context: null as any, 173 163 }, 174 164 { 175 - id: '1223128da3_0_7', 165 + id: `${suite.id}_7`, 176 166 type: 'test', 177 167 name: 'callback setup success done(false)', 178 168 mode: 'run', ··· 186 176 context: null as any, 187 177 }, 188 178 { 189 - id: '1223128da3_0_8', 179 + id: `${suite.id}_8`, 190 180 type: 'test', 191 181 name: 'callback test success done(false)', 192 182 mode: 'run', ··· 208 198 ], 209 199 }, 210 200 { 211 - id: '1223128da3_0_9', 201 + id: `${suite.id}_9`, 212 202 type: 'test', 213 203 name: 'todo test', 214 204 mode: 'todo',
+2 -12
test/reporters/tests/junit.test.ts
··· 1 1 import type { File, Suite, Task, TaskResult } from 'vitest' 2 + import { createFileTask } from '@vitest/runner/utils' 2 3 import { resolve } from 'pathe' 3 4 import { expect, test } from 'vitest' 4 5 import { getDuration } from '../../../packages/vitest/src/node/reporters/junit' ··· 8 9 9 10 test('calc the duration used by junit', () => { 10 11 const result: TaskResult = { state: 'pass', duration: 0 } 11 - const file: File = { 12 - id: '1', 13 - filepath: 'test.ts', 14 - file: null!, 15 - projectName: '', 16 - type: 'suite', 17 - tasks: [], 18 - name: 'test.ts', 19 - mode: 'run', 20 - meta: {}, 21 - } 22 - file.file = file 12 + const file: File = createFileTask('/test.ts', '/', 'test') 23 13 const suite: Suite = { 24 14 id: '1_0', 25 15 type: 'suite',
+1
packages/browser/src/client/utils.ts
··· 81 81 method: 'run' | 'collect' 82 82 orchestrator?: IframeOrchestrator 83 83 commands: CommandsManager 84 + cleanups: Array<() => unknown> 84 85 cdp?: { 85 86 on: (event: string, listener: (payload: any) => void) => void 86 87 once: (event: string, listener: (payload: any) => void) => void
+11 -6
packages/runner/src/types/runner.ts
··· 5 5 SequenceHooks, 6 6 SequenceSetupFiles, 7 7 Suite, 8 - Task, 9 8 TaskEventPack, 10 9 TaskResultPack, 11 10 Test, ··· 84 83 /** 85 84 * Called before running a single test. Doesn't have "result" yet. 86 85 */ 87 - onBeforeRunTask?: (test: Task) => unknown 86 + onBeforeRunTask?: (test: Test) => unknown 88 87 /** 89 88 * Called before actually running the test function. Already has "result" with "state" and "startTime". 90 89 */ 91 90 onBeforeTryTask?: ( 92 - test: Task, 91 + test: Test, 93 92 options: { retry: number; repeats: number } 94 93 ) => unknown 95 94 /** ··· 99 98 /** 100 99 * Called after result and state are set. 101 100 */ 102 - onAfterRunTask?: (test: Task) => unknown 101 + onAfterRunTask?: (test: Test) => unknown 103 102 /** 104 103 * Called right after running the test function. Doesn't have new state yet. Will not be called, if the test function throws. 105 104 */ 106 105 onAfterTryTask?: ( 107 - test: Task, 106 + test: Test, 108 107 options: { retry: number; repeats: number } 109 108 ) => unknown 110 109 ··· 126 125 * If defined, will be called instead of usual Vitest handling. Useful, if you have your custom test function. 127 126 * "before" and "after" hooks will not be ignored. 128 127 */ 129 - runTask?: (test: Task) => Promise<void> 128 + runTask?: (test: Test) => Promise<void> 130 129 131 130 /** 132 131 * Called, when a task is updated. The same as "onTaskUpdate" in a reporter, but this is running in the same thread as tests. ··· 173 172 * The name of the current pool. Can affect how stack trace is inferred on the server side. 174 173 */ 175 174 pool?: string 175 + 176 + /** 177 + * Return the worker context for fixtures specified with `scope: 'worker'` 178 + */ 179 + getWorkerContext?: () => Record<string, unknown> 180 + onCleanupWorkerContext?: (cleanup: () => unknown) => void 176 181 177 182 /** @private */ 178 183 _currentTaskStartTime?: number
+11
packages/runner/src/types/tasks.ts
··· 523 523 export interface FixtureOptions { 524 524 /** 525 525 * Whether to automatically set up current fixture, even though it's not being used in tests. 526 + * @default false 526 527 */ 527 528 auto?: boolean 528 529 /** 529 530 * Indicated if the injected value from the config should be preferred over the fixture value 530 531 */ 531 532 injected?: boolean 533 + /** 534 + * When should the fixture be set up. 535 + * - **test**: fixture will be set up before every test 536 + * - **worker**: fixture will be set up once per worker 537 + * - **file**: fixture will be set up once per file 538 + * 539 + * **Warning:** The `vmThreads` and `vmForks` pools initiate worker fixtures once per test file. 540 + * @default 'test' 541 + */ 542 + scope?: 'test' | 'worker' | 'file' 532 543 } 533 544 534 545 export type Use<T> = (value: T) => Promise<void>
+2
packages/runner/src/utils/collect.ts
··· 1 1 import type { File, Suite, TaskBase } from '../types/tasks' 2 2 import { processError } from '@vitest/utils/error' 3 3 import { relative } from 'pathe' 4 + import { setFileContext } from '../context' 4 5 5 6 /** 6 7 * If any tasks been marked as `only`, mark all other tasks as `skip`. ··· 194 195 pool, 195 196 } 196 197 file.file = file 198 + setFileContext(file, Object.create(null)) 197 199 return file 198 200 } 199 201
+14
packages/vitest/src/runtime/cleanup.ts
··· 1 + const listeners = new Set<() => unknown>() 2 + 3 + export function addCleanupListener(listener: () => unknown): void { 4 + listeners.add(listener) 5 + } 6 + 7 + export function removeCleanupListener(listener: () => unknown): void { 8 + listeners.delete(listener) 9 + } 10 + 11 + export async function cleanup(): Promise<void> { 12 + const promises = [...listeners].map(l => l()) 13 + await Promise.all(promises) 14 + }
+2
packages/vitest/src/runtime/runVmTests.ts
··· 77 77 78 78 config.snapshotOptions.snapshotEnvironment = snapshotEnvironment 79 79 80 + runner.getWorkerContext = undefined 81 + 80 82 workerState.onCancel.then((reason) => { 81 83 closeInspector(config) 82 84 runner.cancel?.(reason)
+6
packages/vitest/src/runtime/worker.ts
··· 5 5 import { workerId as poolId } from 'tinypool' 6 6 import { ModuleCacheMap } from 'vite-node/client' 7 7 import { loadEnvironment } from '../integrations/env/loader' 8 + import { addCleanupListener, cleanup as cleanupWorker } from './cleanup' 8 9 import { setupInspect } from './inspector' 9 10 import { createRuntimeRpc, rpcDone } from './rpc' 10 11 import { isChildProcess, setProcessTitle } from './utils' ··· 91 92 prepare: prepareStart, 92 93 }, 93 94 rpc, 95 + onCleanup: listener => addCleanupListener(listener), 94 96 providedContext: ctx.providedContext, 95 97 onFilterStackTrace(stack) { 96 98 return createStackString(parseStacktrace(stack)) ··· 119 121 120 122 export function collect(ctx: ContextRPC): Promise<void> { 121 123 return execute('collect', ctx) 124 + } 125 + 126 + export async function teardown(): Promise<void> { 127 + return cleanupWorker() 122 128 }
+2
packages/vitest/src/types/global.ts
··· 112 112 * This API is useful for running snapshot tests concurrently because global expect cannot track them. 113 113 */ 114 114 readonly expect: ExpectStatic 115 + /** @internal */ 116 + _local: boolean 115 117 } 116 118 117 119 interface TaskMeta {
+1
packages/vitest/src/types/worker.ts
··· 45 45 onCancel: Promise<CancelReason> 46 46 moduleCache: ModuleCacheMap 47 47 moduleExecutionInfo?: ModuleExecutionInfo 48 + onCleanup: (listener: () => unknown) => void 48 49 providedContext: Record<string, any> 49 50 durations: { 50 51 environment: number
+10 -10
test/reporters/tests/__snapshots__/reporters.spec.ts.snap
··· 993 993 exports[`tap reporter 1`] = ` 994 994 "TAP version 13 995 995 1..1 996 - not ok 1 - test/core/test/basic.test.ts # time=145.99ms { 996 + not ok 1 - basic.test.ts # time=145.99ms { 997 997 1..1 998 998 ok 1 - suite # time=1.90ms { 999 999 1..9 ··· 1022 1022 exports[`tap-flat reporter 1`] = ` 1023 1023 "TAP version 13 1024 1024 1..9 1025 - not ok 1 - test/core/test/basic.test.ts > suite > Math.sqrt() # time=1.44ms 1025 + not ok 1 - basic.test.ts > suite > Math.sqrt() # time=1.44ms 1026 1026 --- 1027 1027 error: 1028 1028 name: "AssertionError" ··· 1031 1031 actual: "2.23606797749979" 1032 1032 expected: "2" 1033 1033 ... 1034 - ok 2 - test/core/test/basic.test.ts > suite > JSON # time=1.02ms 1035 - ok 3 - test/core/test/basic.test.ts > suite > async with timeout # SKIP 1036 - ok 4 - test/core/test/basic.test.ts > suite > timeout # time=100.51ms 1037 - ok 5 - test/core/test/basic.test.ts > suite > callback setup success # time=20.18ms 1038 - ok 6 - test/core/test/basic.test.ts > suite > callback test success # time=0.33ms 1039 - ok 7 - test/core/test/basic.test.ts > suite > callback setup success done(false) # time=19.74ms 1040 - ok 8 - test/core/test/basic.test.ts > suite > callback test success done(false) # time=0.19ms 1041 - ok 9 - test/core/test/basic.test.ts > suite > todo test # TODO 1034 + ok 2 - basic.test.ts > suite > JSON # time=1.02ms 1035 + ok 3 - basic.test.ts > suite > async with timeout # SKIP 1036 + ok 4 - basic.test.ts > suite > timeout # time=100.51ms 1037 + ok 5 - basic.test.ts > suite > callback setup success # time=20.18ms 1038 + ok 6 - basic.test.ts > suite > callback test success # time=0.33ms 1039 + ok 7 - basic.test.ts > suite > callback setup success done(false) # time=19.74ms 1040 + ok 8 - basic.test.ts > suite > callback test success done(false) # time=0.19ms 1041 + ok 9 - basic.test.ts > suite > todo test # TODO 1042 1042 " 1043 1043 `;
+1
packages/browser/src/client/public/esm-client-injector.js
··· 20 20 wrapModule, 21 21 wrapDynamicImport: wrapModule, 22 22 moduleCache, 23 + cleanups: [], 23 24 config: { __VITEST_CONFIG__ }, 24 25 viteConfig: { __VITEST_VITE_CONFIG__ }, 25 26 type: { __VITEST_TYPE__ },
+4 -4
packages/browser/src/client/tester/runner.ts
··· 1 - import type { CancelReason, File, Suite, Task, TaskEventPack, TaskResultPack, TestAnnotation, VitestRunner } from '@vitest/runner' 2 - import type { RunnerTestCase, SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest' 1 + import type { CancelReason, File, Suite, Task, TaskEventPack, TaskResultPack, Test, TestAnnotation, VitestRunner } from '@vitest/runner' 2 + import type { SerializedConfig, TestExecutionMethod, WorkerGlobalState } from 'vitest' 3 3 import type { VitestExecutor } from 'vitest/execute' 4 4 import type { VitestBrowserClientMocker } from './mocker' 5 5 import { globalChannel, onCancel } from '@vitest/browser/client' ··· 59 59 await super.onBeforeTryTask?.(...args) 60 60 } 61 61 62 - onAfterRunTask = async (task: Task) => { 62 + onAfterRunTask = async (task: Test) => { 63 63 await super.onAfterRunTask?.(task) 64 64 65 65 if (this.config.bail && task.result?.state === 'fail') { ··· 146 146 return rpc().onCollected(this.method, files) 147 147 } 148 148 149 - onTestAnnotate = (test: RunnerTestCase, annotation: TestAnnotation): Promise<TestAnnotation> => { 149 + onTestAnnotate = (test: Test, annotation: TestAnnotation): Promise<TestAnnotation> => { 150 150 if (annotation.location) { 151 151 // the file should be the test file 152 152 // tests from other files are not supported
+1
packages/browser/src/client/tester/state.ts
··· 30 30 throw new Error('Not called in the browser') 31 31 }, 32 32 }, 33 + onCleanup: fn => getBrowserState().cleanups.push(fn), 33 34 moduleCache: getBrowserState().moduleCache, 34 35 rpc: null as any, 35 36 durations: {
+4
packages/browser/src/client/tester/tester.ts
··· 234 234 await userEvent.cleanup() 235 235 .catch(error => unhandledError(error, 'Cleanup Error')) 236 236 237 + await Promise.all( 238 + getBrowserState().cleanups.map(fn => fn()), 239 + ).catch(error => unhandledError(error, 'Cleanup Error')) 240 + 237 241 // if isolation is disabled, Vitest reuses the same iframe and we 238 242 // don't need to switch the context back at all 239 243 if (contextSwitched) {
+19 -21
packages/vitest/src/node/pools/forks.ts
··· 19 19 20 20 function createChildProcessChannel(project: TestProject, collect = false) { 21 21 const emitter = new EventEmitter() 22 - const cleanup = () => emitter.removeAllListeners() 23 22 24 23 const events = { message: 'message', response: 'response' } 25 24 const channel: TinypoolChannel = { 26 25 onMessage: callback => emitter.on(events.message, callback), 27 26 postMessage: message => emitter.emit(events.response, message), 27 + onClose: () => emitter.removeAllListeners(), 28 28 } 29 29 30 30 const rpc = createBirpc<RunnerRPC, RuntimeRPC>(createMethodsRPC(project, { cacheFs: true, collect }), { ··· 42 42 }, 43 43 }) 44 44 45 - project.ctx.onCancel(reason => rpc.onCancel(reason)) 45 + project.vitest.onCancel(reason => rpc.onCancel(reason)) 46 46 47 - return { channel, cleanup } 47 + return channel 48 48 } 49 49 50 50 export function createForksPool( 51 - ctx: Vitest, 51 + vitest: Vitest, 52 52 { execArgv, env }: PoolProcessOptions, 53 53 ): ProcessPool { 54 54 const numCpus ··· 56 56 ? nodeos.availableParallelism() 57 57 : nodeos.cpus().length 58 58 59 - const threadsCount = ctx.config.watch 59 + const threadsCount = vitest.config.watch 60 60 ? Math.max(Math.floor(numCpus / 2), 1) 61 61 : Math.max(numCpus - 1, 1) 62 62 63 - const poolOptions = ctx.config.poolOptions?.forks ?? {} 63 + const poolOptions = vitest.config.poolOptions?.forks ?? {} 64 64 65 65 const maxThreads 66 - = poolOptions.maxForks ?? ctx.config.maxWorkers ?? threadsCount 66 + = poolOptions.maxForks ?? vitest.config.maxWorkers ?? threadsCount 67 67 const minThreads 68 - = poolOptions.minForks ?? ctx.config.minWorkers ?? threadsCount 68 + = poolOptions.minForks ?? vitest.config.minWorkers ?? threadsCount 69 69 70 - const worker = resolve(ctx.distPath, 'workers/forks.js') 70 + const worker = resolve(vitest.distPath, 'workers/forks.js') 71 71 72 72 const options: TinypoolOptions = { 73 73 runtime: 'child_process', 74 - filename: resolve(ctx.distPath, 'worker.js'), 74 + filename: resolve(vitest.distPath, 'worker.js'), 75 + teardown: 'teardown', 75 76 76 77 maxThreads, 77 78 minThreads, ··· 79 80 env, 80 81 execArgv: [...(poolOptions.execArgv ?? []), ...execArgv], 81 82 82 - terminateTimeout: ctx.config.teardownTimeout, 83 + terminateTimeout: vitest.config.teardownTimeout, 83 84 concurrentTasksPerWorker: 1, 84 85 } 85 86 ··· 89 90 options.isolateWorkers = true 90 91 } 91 92 92 - if (poolOptions.singleFork || !ctx.config.fileParallelism) { 93 + if (poolOptions.singleFork || !vitest.config.fileParallelism) { 93 94 options.maxThreads = 1 94 95 options.minThreads = 1 95 96 } ··· 107 108 invalidates: string[] = [], 108 109 ) { 109 110 const paths = files.map(f => f.filepath) 110 - ctx.state.clearFiles(project, paths) 111 + vitest.state.clearFiles(project, paths) 111 112 112 - const { channel, cleanup } = createChildProcessChannel(project, name === 'collect') 113 + const channel = createChildProcessChannel(project, name === 'collect') 113 114 const workerId = ++id 114 115 const data: ContextRPC = { 115 116 pool: 'forks', ··· 131 132 error instanceof Error 132 133 && /Failed to terminate worker/.test(error.message) 133 134 ) { 134 - ctx.state.addProcessTimeoutCause( 135 + vitest.state.addProcessTimeoutCause( 135 136 `Failed to terminate worker while running ${paths.join(', ')}.`, 136 137 ) 137 138 } 138 139 // Intentionally cancelled 139 140 else if ( 140 - ctx.isCancelling 141 + vitest.isCancelling 141 142 && error instanceof Error 142 143 && /The task has been cancelled/.test(error.message) 143 144 ) { 144 - ctx.state.cancelFiles(paths, project) 145 + vitest.state.cancelFiles(paths, project) 145 146 } 146 147 else { 147 148 throw error 148 149 } 149 150 } 150 - finally { 151 - cleanup() 152 - } 153 151 } 154 152 155 153 return async (specs, invalidates) => { 156 154 // Cancel pending tasks from pool when possible 157 - ctx.onCancel(() => pool.cancelPendingTasks()) 155 + vitest.onCancel(() => pool.cancelPendingTasks()) 158 156 159 157 const configs = new WeakMap<TestProject, SerializedConfig>() 160 158 const getConfig = (project: TestProject): SerializedConfig => {
+23 -21
packages/vitest/src/node/pools/threads.ts
··· 34 34 }, 35 35 }) 36 36 37 - project.ctx.onCancel(reason => rpc.onCancel(reason)) 37 + project.vitest.onCancel(reason => rpc.onCancel(reason)) 38 38 39 39 return { workerPort, port } 40 40 } 41 41 42 42 export function createThreadsPool( 43 - ctx: Vitest, 43 + vitest: Vitest, 44 44 { execArgv, env }: PoolProcessOptions, 45 45 ): ProcessPool { 46 46 const numCpus ··· 48 48 ? nodeos.availableParallelism() 49 49 : nodeos.cpus().length 50 50 51 - const threadsCount = ctx.config.watch 51 + const threadsCount = vitest.config.watch 52 52 ? Math.max(Math.floor(numCpus / 2), 1) 53 53 : Math.max(numCpus - 1, 1) 54 54 55 - const poolOptions = ctx.config.poolOptions?.threads ?? {} 55 + const poolOptions = vitest.config.poolOptions?.threads ?? {} 56 56 57 57 const maxThreads 58 - = poolOptions.maxThreads ?? ctx.config.maxWorkers ?? threadsCount 58 + = poolOptions.maxThreads ?? vitest.config.maxWorkers ?? threadsCount 59 59 const minThreads 60 - = poolOptions.minThreads ?? ctx.config.minWorkers ?? threadsCount 60 + = poolOptions.minThreads ?? vitest.config.minWorkers ?? threadsCount 61 61 62 - const worker = resolve(ctx.distPath, 'workers/threads.js') 62 + const worker = resolve(vitest.distPath, 'workers/threads.js') 63 63 64 64 const options: TinypoolOptions = { 65 - filename: resolve(ctx.distPath, 'worker.js'), 65 + filename: resolve(vitest.distPath, 'worker.js'), 66 + teardown: 'teardown', 66 67 // TODO: investigate further 67 68 // It seems atomics introduced V8 Fatal Error https://github.com/vitest-dev/vitest/issues/1191 68 69 useAtomics: poolOptions.useAtomics ?? false, ··· 73 74 env, 74 75 execArgv: [...(poolOptions.execArgv ?? []), ...execArgv], 75 76 76 - terminateTimeout: ctx.config.teardownTimeout, 77 + terminateTimeout: vitest.config.teardownTimeout, 77 78 concurrentTasksPerWorker: 1, 78 79 } 79 80 ··· 83 84 options.isolateWorkers = true 84 85 } 85 86 86 - if (poolOptions.singleThread || !ctx.config.fileParallelism) { 87 + if (poolOptions.singleThread || !vitest.config.fileParallelism) { 87 88 options.maxThreads = 1 88 89 options.minThreads = 1 89 90 } ··· 101 102 invalidates: string[] = [], 102 103 ) { 103 104 const paths = files.map(f => f.filepath) 104 - ctx.state.clearFiles(project, paths) 105 + vitest.state.clearFiles(project, paths) 105 106 106 107 const { workerPort, port } = createWorkerChannel(project, name === 'collect') 108 + const onClose = () => { 109 + port.close() 110 + workerPort.close() 111 + } 112 + 107 113 const workerId = ++id 108 114 const data: WorkerContext = { 109 115 pool: 'threads', ··· 118 124 providedContext: project.getProvidedContext(), 119 125 } 120 126 try { 121 - await pool.run(data, { transferList: [workerPort], name }) 127 + await pool.run(data, { transferList: [workerPort], name, channel: { onClose } }) 122 128 } 123 129 catch (error) { 124 130 // Worker got stuck and won't terminate - this may cause process to hang ··· 126 132 error instanceof Error 127 133 && /Failed to terminate worker/.test(error.message) 128 134 ) { 129 - ctx.state.addProcessTimeoutCause( 135 + vitest.state.addProcessTimeoutCause( 130 136 `Failed to terminate worker while running ${paths.join( 131 137 ', ', 132 138 )}. \nSee https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker for troubleshooting.`, ··· 134 140 } 135 141 // Intentionally cancelled 136 142 else if ( 137 - ctx.isCancelling 143 + vitest.isCancelling 138 144 && error instanceof Error 139 145 && /The task has been cancelled/.test(error.message) 140 146 ) { 141 - ctx.state.cancelFiles(paths, project) 147 + vitest.state.cancelFiles(paths, project) 142 148 } 143 149 else { 144 150 throw error 145 151 } 146 152 } 147 - finally { 148 - port.close() 149 - workerPort.close() 150 - } 151 153 } 152 154 153 155 return async (specs, invalidates) => { 154 156 // Cancel pending tasks from pool when possible 155 - ctx.onCancel(() => pool.cancelPendingTasks()) 157 + vitest.onCancel(() => pool.cancelPendingTasks()) 156 158 157 159 const configs = new WeakMap<TestProject, SerializedConfig>() 158 160 const getConfig = (project: TestProject): SerializedConfig => { ··· 160 162 return configs.get(project)! 161 163 } 162 164 163 - const config = project.getSerializableConfig() 165 + const config = project.serializedConfig 164 166 configs.set(project, config) 165 167 return config 166 168 }
+17 -17
packages/vitest/src/node/pools/vmForks.ts
··· 48 48 }, 49 49 ) 50 50 51 - project.ctx.onCancel(reason => rpc.onCancel(reason)) 51 + project.vitest.onCancel(reason => rpc.onCancel(reason)) 52 52 53 53 return { channel, cleanup } 54 54 } 55 55 56 56 export function createVmForksPool( 57 - ctx: Vitest, 57 + vitest: Vitest, 58 58 { execArgv, env }: PoolProcessOptions, 59 59 ): ProcessPool { 60 60 const numCpus ··· 62 62 ? nodeos.availableParallelism() 63 63 : nodeos.cpus().length 64 64 65 - const threadsCount = ctx.config.watch 65 + const threadsCount = vitest.config.watch 66 66 ? Math.max(Math.floor(numCpus / 2), 1) 67 67 : Math.max(numCpus - 1, 1) 68 68 69 - const poolOptions = ctx.config.poolOptions?.vmForks ?? {} 69 + const poolOptions = vitest.config.poolOptions?.vmForks ?? {} 70 70 71 71 const maxThreads 72 - = poolOptions.maxForks ?? ctx.config.maxWorkers ?? threadsCount 72 + = poolOptions.maxForks ?? vitest.config.maxWorkers ?? threadsCount 73 73 const minThreads 74 - = poolOptions.maxForks ?? ctx.config.minWorkers ?? threadsCount 74 + = poolOptions.maxForks ?? vitest.config.minWorkers ?? threadsCount 75 75 76 - const worker = resolve(ctx.distPath, 'workers/vmForks.js') 76 + const worker = resolve(vitest.distPath, 'workers/vmForks.js') 77 77 78 78 const options: TinypoolOptions = { 79 79 runtime: 'child_process', 80 - filename: resolve(ctx.distPath, 'worker.js'), 80 + filename: resolve(vitest.distPath, 'worker.js'), 81 81 82 82 maxThreads, 83 83 minThreads, ··· 92 92 ...execArgv, 93 93 ], 94 94 95 - terminateTimeout: ctx.config.teardownTimeout, 95 + terminateTimeout: vitest.config.teardownTimeout, 96 96 concurrentTasksPerWorker: 1, 97 - maxMemoryLimitBeforeRecycle: getMemoryLimit(ctx.config) || undefined, 97 + maxMemoryLimitBeforeRecycle: getMemoryLimit(vitest.config) || undefined, 98 98 } 99 99 100 - if (poolOptions.singleFork || !ctx.config.fileParallelism) { 100 + if (poolOptions.singleFork || !vitest.config.fileParallelism) { 101 101 options.maxThreads = 1 102 102 options.minThreads = 1 103 103 } ··· 115 115 invalidates: string[] = [], 116 116 ) { 117 117 const paths = files.map(f => f.filepath) 118 - ctx.state.clearFiles(project, paths) 118 + vitest.state.clearFiles(project, paths) 119 119 120 120 const { channel, cleanup } = createChildProcessChannel(project, name === 'collect') 121 121 const workerId = ++id ··· 139 139 error instanceof Error 140 140 && /Failed to terminate worker/.test(error.message) 141 141 ) { 142 - ctx.state.addProcessTimeoutCause( 142 + vitest.state.addProcessTimeoutCause( 143 143 `Failed to terminate worker while running ${paths.join(', ')}.`, 144 144 ) 145 145 } 146 146 // Intentionally cancelled 147 147 else if ( 148 - ctx.isCancelling 148 + vitest.isCancelling 149 149 && error instanceof Error 150 150 && /The task has been cancelled/.test(error.message) 151 151 ) { 152 - ctx.state.cancelFiles(paths, project) 152 + vitest.state.cancelFiles(paths, project) 153 153 } 154 154 else { 155 155 throw error ··· 162 162 163 163 return async (specs, invalidates) => { 164 164 // Cancel pending tasks from pool when possible 165 - ctx.onCancel(() => pool.cancelPendingTasks()) 165 + vitest.onCancel(() => pool.cancelPendingTasks()) 166 166 167 167 const configs = new Map<TestProject, SerializedConfig>() 168 168 const getConfig = (project: TestProject): SerializedConfig => { ··· 170 170 return configs.get(project)! 171 171 } 172 172 173 - const _config = project.getSerializableConfig() 173 + const _config = project.serializedConfig 174 174 const config = wrapSerializableConfig(_config) 175 175 176 176 configs.set(project, config)
+16 -16
packages/vitest/src/node/pools/vmThreads.ts
··· 37 37 }, 38 38 }) 39 39 40 - project.ctx.onCancel(reason => rpc.onCancel(reason)) 40 + project.vitest.onCancel(reason => rpc.onCancel(reason)) 41 41 42 42 return { workerPort, port } 43 43 } 44 44 45 45 export function createVmThreadsPool( 46 - ctx: Vitest, 46 + vitest: Vitest, 47 47 { execArgv, env }: PoolProcessOptions, 48 48 ): ProcessPool { 49 49 const numCpus ··· 51 51 ? nodeos.availableParallelism() 52 52 : nodeos.cpus().length 53 53 54 - const threadsCount = ctx.config.watch 54 + const threadsCount = vitest.config.watch 55 55 ? Math.max(Math.floor(numCpus / 2), 1) 56 56 : Math.max(numCpus - 1, 1) 57 57 58 - const poolOptions = ctx.config.poolOptions?.vmThreads ?? {} 58 + const poolOptions = vitest.config.poolOptions?.vmThreads ?? {} 59 59 60 60 const maxThreads 61 - = poolOptions.maxThreads ?? ctx.config.maxWorkers ?? threadsCount 61 + = poolOptions.maxThreads ?? vitest.config.maxWorkers ?? threadsCount 62 62 const minThreads 63 - = poolOptions.minThreads ?? ctx.config.minWorkers ?? threadsCount 63 + = poolOptions.minThreads ?? vitest.config.minWorkers ?? threadsCount 64 64 65 - const worker = resolve(ctx.distPath, 'workers/vmThreads.js') 65 + const worker = resolve(vitest.distPath, 'workers/vmThreads.js') 66 66 67 67 const options: TinypoolOptions = { 68 - filename: resolve(ctx.distPath, 'worker.js'), 68 + filename: resolve(vitest.distPath, 'worker.js'), 69 69 // TODO: investigate further 70 70 // It seems atomics introduced V8 Fatal Error https://github.com/vitest-dev/vitest/issues/1191 71 71 useAtomics: poolOptions.useAtomics ?? false, ··· 83 83 ...execArgv, 84 84 ], 85 85 86 - terminateTimeout: ctx.config.teardownTimeout, 86 + terminateTimeout: vitest.config.teardownTimeout, 87 87 concurrentTasksPerWorker: 1, 88 - maxMemoryLimitBeforeRecycle: getMemoryLimit(ctx.config) || undefined, 88 + maxMemoryLimitBeforeRecycle: getMemoryLimit(vitest.config) || undefined, 89 89 } 90 90 91 - if (poolOptions.singleThread || !ctx.config.fileParallelism) { 91 + if (poolOptions.singleThread || !vitest.config.fileParallelism) { 92 92 options.maxThreads = 1 93 93 options.minThreads = 1 94 94 } ··· 106 106 invalidates: string[] = [], 107 107 ) { 108 108 const paths = files.map(f => f.filepath) 109 - ctx.state.clearFiles(project, paths) 109 + vitest.state.clearFiles(project, paths) 110 110 111 111 const { workerPort, port } = createWorkerChannel(project, name === 'collect') 112 112 const workerId = ++id ··· 131 131 error instanceof Error 132 132 && /Failed to terminate worker/.test(error.message) 133 133 ) { 134 - ctx.state.addProcessTimeoutCause( 134 + vitest.state.addProcessTimeoutCause( 135 135 `Failed to terminate worker while running ${paths.join( 136 136 ', ', 137 137 )}. \nSee https://vitest.dev/guide/common-errors.html#failed-to-terminate-worker for troubleshooting.`, ··· 139 139 } 140 140 // Intentionally cancelled 141 141 else if ( 142 - ctx.isCancelling 142 + vitest.isCancelling 143 143 && error instanceof Error 144 144 && /The task has been cancelled/.test(error.message) 145 145 ) { 146 - ctx.state.cancelFiles(paths, project) 146 + vitest.state.cancelFiles(paths, project) 147 147 } 148 148 else { 149 149 throw error ··· 157 157 158 158 return async (specs, invalidates) => { 159 159 // Cancel pending tasks from pool when possible 160 - ctx.onCancel(() => pool.cancelPendingTasks()) 160 + vitest.onCancel(() => pool.cancelPendingTasks()) 161 161 162 162 const configs = new Map<TestProject, SerializedConfig>() 163 163 const getConfig = (project: TestProject): SerializedConfig => {
+14 -3
packages/vitest/src/runtime/runners/test.ts
··· 5 5 ImportDuration, 6 6 Suite, 7 7 Task, 8 + Test, 8 9 TestContext, 9 10 VitestRunner, 10 11 VitestRunnerImportSource, ··· 20 21 import { vi } from '../../integrations/vi' 21 22 import { rpc } from '../rpc' 22 23 import { getWorkerState } from '../utils' 24 + 25 + // worker context is shared between all tests 26 + const workerContext = Object.create(null) 23 27 24 28 export class VitestTestRunner implements VitestRunner { 25 29 private snapshotClient = getSnapshotClient() ··· 44 48 this.workerState.current = file 45 49 } 46 50 51 + onCleanupWorkerContext(listener: () => unknown): void { 52 + this.workerState.onCleanup(listener) 53 + } 54 + 47 55 onAfterRunFiles(): void { 48 56 this.snapshotClient.clear() 49 57 this.workerState.current = undefined 58 + } 59 + 60 + getWorkerContext(): Record<string, unknown> { 61 + return workerContext 50 62 } 51 63 52 64 async onAfterRunSuite(suite: Suite): Promise<void> { ··· 133 145 ) 134 146 } 135 147 136 - onAfterTryTask(test: Task): void { 148 + onAfterTryTask(test: Test): void { 137 149 const { 138 150 assertionCalls, 139 151 expectedAssertionsNumber, ··· 141 153 isExpectingAssertions, 142 154 isExpectingAssertionsError, 143 155 } 144 - // @ts-expect-error _local is untyped 145 - = 'context' in test && test.context._local 156 + = test.context._local 146 157 ? test.context.expect.getState() 147 158 : getState((globalThis as any)[GLOBAL_EXPECT]) 148 159 if (
+10 -19
test/cli/fixtures/custom-pool/pool/custom-pool.ts
··· 7 7 RunnerTask 8 8 } from 'vitest' 9 9 import type { ProcessPool, Vitest } from 'vitest/node' 10 - import { createMethodsRPC } from 'vitest/node' 11 - import { generateFileHash } from '@vitest/runner/utils' 10 + import { createFileTask, generateFileHash } from '@vitest/runner/utils' 12 11 import { normalize, relative } from 'pathe' 13 12 14 13 export default (vitest: Vitest): ProcessPool => { ··· 24 23 for (const [project, file] of specs) { 25 24 vitest.state.clearFiles(project) 26 25 vitest.logger.console.warn('[pool] running tests for', project.name, 'in', normalize(file).toLowerCase().replace(normalize(process.cwd()).toLowerCase(), '')) 27 - const path = relative(project.config.root, file) 28 - const taskFile: RunnerTestFile = { 29 - id: generateFileHash(path, project.config.name), 30 - name: path, 31 - mode: 'run', 32 - meta: {}, 33 - projectName: project.name, 34 - filepath: file, 35 - type: 'suite', 36 - tasks: [], 37 - result: { 38 - state: 'pass', 39 - }, 40 - file: null!, 41 - } 42 - taskFile.file = taskFile 26 + const taskFile = createFileTask( 27 + file, 28 + project.config.root, 29 + project.name, 30 + 'custom' 31 + ) 32 + taskFile.mode = 'run' 33 + taskFile.result = { state: 'pass' } 43 34 const taskTest: RunnerTestCase = { 44 35 type: 'test', 45 36 name: 'custom test', 46 - id: 'custom-test', 37 + id: `${taskFile.id}_0`, 47 38 context: {} as any, 48 39 suite: taskFile, 49 40 mode: 'run',