Skip to content

Commit bdd9854

Browse files
authored
feat!: expose concurrencyId/workerId on TestModule's diagnostics, make id 1-based (#10516)
1 parent 206e8cf commit bdd9854

23 files changed

Lines changed: 244 additions & 110 deletions

File tree

docs/api/advanced/test-module.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,24 @@ interface ModuleDiagnostic {
117117
* The time spent importing every non-externalized dependency that Vitest has processed.
118118
*/
119119
readonly importDurations: Record<string, ImportDuration>
120+
/**
121+
* The id of the worker that ran this file. This value cannot be higher than `maxWorkers`.
122+
* If file did not run yet, this will be 0.
123+
*
124+
* **Warning**: Node.js tests and browser tests run in different pools and do not share `concurrencyId`.
125+
* It is possible to have multiple modules with the same `concurrencyId` because of that.
126+
* Use `project.isBrowserEnabled()` to distinguish the concurrency.
127+
*/
128+
readonly concurrencyId: number
129+
/**
130+
* Incremental number of the worker that ran this file. This number increases with each worker.
131+
* If file did not run yet, this will be 0.
132+
*
133+
* **Warning**: Node.js tests and browser tests run in different pools and do not share `workerId`.
134+
* It is possible to have multiple modules with the same `workerId` because of that.
135+
* Use `project.isBrowserEnabled()` to distinguish the concurrency.
136+
*/
137+
readonly workerId: number
120138
}
121139

122140
/** The time spent importing & executing a non-externalized file. */

packages/browser/src/client/channel.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ export interface IframeExecuteEvent {
3232
files: FileSpecification[]
3333
iframeId: string
3434
context: string
35+
concurrencyId: number
36+
workerId: number
3537
}
3638

3739
export interface IframeCleanupEvent {

packages/browser/src/client/orchestrator.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ export class IframeOrchestrator {
170170
files: options.files,
171171
method: options.method,
172172
context: options.providedContext,
173+
concurrencyId: options.concurrencyId,
174+
workerId: options.workerId,
173175
})
174176
debug('finished running tests', options.files.join(', '))
175177
// we don't cleanup here because in non-isolated mode
@@ -207,6 +209,8 @@ export class IframeOrchestrator {
207209
method: options.method,
208210
iframeId: file,
209211
context: options.providedContext,
212+
concurrencyId: options.concurrencyId,
213+
workerId: options.workerId,
210214
})
211215
// perform "cleanup" to cleanup resources and calculate the coverage
212216
await this.sendEventToIframe({

packages/browser/src/client/tester/state.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const state: WorkerGlobalState = {
1111
rpc: null as any,
1212
pool: 'browser',
1313
workerId: 1,
14+
concurrencyId: 1,
1415
config,
1516
projectName: config.name || '',
1617
files: [],

packages/browser/src/client/tester/tester.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,16 @@ channel.addEventListener('message', async (e) => {
5353

5454
switch (data.event) {
5555
case 'execute': {
56-
const { method, files, context } = data
56+
const { method, files, context, concurrencyId, workerId } = data
5757
const state = getWorkerState()
5858
const parsedContext = parse(context)
5959

60+
state.ctx.concurrencyId = concurrencyId
61+
state.ctx.workerId = workerId
6062
state.ctx.providedContext = parsedContext
6163
state.providedContext = parsedContext
64+
state.metaEnv.VITEST_POOL_ID = String(concurrencyId)
65+
state.metaEnv.VITEST_WORKER_ID = String(workerId)
6266

6367
if (method === 'collect') {
6468
await executeTests('collect', files).catch(err => unhandledError(err, 'Collect Error'))

packages/ui/client/components/views/ViewReport.spec.ts

Lines changed: 36 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import type { RunnerTestFile } from 'vitest'
21
import { faker } from '@faker-js/faker'
3-
import { beforeEach, describe, expect, it } from 'vitest'
2+
import { beforeEach, describe, expect, it, TestRunner } from 'vitest'
43
import { config } from '~/composables/client'
54
import { page, render } from '~/test'
65
import ViewReport from './ViewReport.vue'
@@ -42,23 +41,16 @@ const error = {
4241
diff,
4342
}
4443

45-
const fileWithTextStacks: RunnerTestFile = {
46-
id: 'f-1',
47-
name: 'test/plain-stack-trace.ts',
48-
type: 'suite',
49-
mode: 'run',
50-
filepath: 'test/plain-stack-trace.ts',
51-
fullName: 'test/plain-stack-trace.ts',
52-
meta: {},
53-
result: {
54-
state: 'fail',
55-
errors: [error],
56-
},
57-
tasks: [],
58-
projectName: '',
59-
file: null!,
44+
const fileWithTextStacks = TestRunner.createFileTask(
45+
'test/plain-stack-trace.ts',
46+
'',
47+
'',
48+
)
49+
fileWithTextStacks.mode = 'run'
50+
fileWithTextStacks.result = {
51+
state: 'fail',
52+
errors: [error],
6053
}
61-
fileWithTextStacks.file = fileWithTextStacks
6254

6355
describe.todo('ViewReport', () => {
6456
describe('RunnerTestFile where stacks are in text', () => {
@@ -93,31 +85,20 @@ describe.todo('ViewReport', () => {
9385
})
9486

9587
it('test html stack trace without html message', async () => {
96-
const file: RunnerTestFile = {
97-
id: 'f-1',
98-
name: 'test/plain-stack-trace.ts',
99-
type: 'suite',
100-
mode: 'run',
101-
filepath: 'test/plain-stack-trace.ts',
102-
fullName: 'test/plain-stack-trace.ts',
103-
meta: {},
104-
result: {
105-
state: 'fail',
106-
errors: [
107-
{
108-
name: 'Do some test',
109-
stacks: [],
110-
stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m',
111-
message: 'Error: Transform failed with 1 error:',
112-
diff,
113-
},
114-
],
115-
},
116-
tasks: [],
117-
projectName: '',
118-
file: null!,
88+
const file = TestRunner.createFileTask('test/plain-stack-trace.ts', '', '')
89+
file.mode = 'run'
90+
file.result = {
91+
state: 'fail',
92+
errors: [
93+
{
94+
name: 'Do some test',
95+
stacks: [],
96+
stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m',
97+
message: 'Error: Transform failed with 1 error:',
98+
diff,
99+
},
100+
],
119101
}
120-
file.file = file
121102
const container = await render(ViewReport, {
122103
props: { file },
123104
})
@@ -153,31 +134,20 @@ describe.todo('ViewReport', () => {
153134
})
154135

155136
it('test html stack trace and message', async () => {
156-
const file: RunnerTestFile = {
157-
id: 'f-1',
158-
name: 'test/plain-stack-trace.ts',
159-
type: 'suite',
160-
mode: 'run',
161-
filepath: 'test/plain-stack-trace.ts',
162-
fullName: 'test/plain-stack-trace.ts',
163-
meta: {},
164-
result: {
165-
state: 'fail',
166-
errors: [
167-
{
168-
name: 'Do some test',
169-
stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m',
170-
stacks: [],
171-
message: '\x1B[44mError: Transform failed with 1 error:\x1B[0m',
172-
diff,
173-
},
174-
],
175-
},
176-
tasks: [],
177-
projectName: '',
178-
file: null!,
137+
const file = TestRunner.createFileTask('test/plain-stack-trace.ts', '', '')
138+
file.mode = 'run'
139+
file.result = {
140+
state: 'fail',
141+
errors: [
142+
{
143+
name: 'Do some test',
144+
stack: '\x1B[33mtest/plain-stack-trace.ts\x1B[0m',
145+
stacks: [],
146+
message: '\x1B[44mError: Transform failed with 1 error:\x1B[0m',
147+
diff,
148+
},
149+
],
179150
}
180-
file.file = file
181151
const container = await render(ViewReport, {
182152
props: { file },
183153
})

packages/vitest/src/integrations/vi.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ import type { VitestMocker } from '../runtime/moduleRunner/moduleMocker'
1212
import type { MockFactoryWithHelper, MockOptions } from '../types/mocker'
1313
import { clearAllMocks, fn, isMockFunction, resetAllMocks, restoreAllMocks, spyOn } from '@vitest/spy'
1414
import { assertTypes, createSimpleStackTrace } from '@vitest/utils/helpers'
15-
import { getWorkerState, isChildProcess, resetModules, waitForImportsToResolve } from '../runtime/utils'
15+
import { getSafeTimers } from '@vitest/utils/timers'
16+
import { getWorkerState, isChildProcess, resetModules } from '../runtime/utils'
1617
import { parseSingleStack } from '../utils/source-map'
1718
import { FakeTimers } from './mock/timers'
1819
import { waitFor, waitUntil } from './wait'
@@ -875,3 +876,25 @@ function copyStackTrace(target: Error, source: Error) {
875876
}
876877
return target
877878
}
879+
880+
function waitNextTick() {
881+
const { setTimeout } = getSafeTimers()
882+
return new Promise(resolve => setTimeout(resolve, 0))
883+
}
884+
885+
async function waitForImportsToResolve(): Promise<void> {
886+
await waitNextTick()
887+
const state = getWorkerState()
888+
const promises: Promise<unknown>[] = []
889+
const resolvingCount = state.resolvingModules.size
890+
for (const [_, mod] of state.evaluatedModules.idToModuleMap) {
891+
if (mod.promise && !mod.evaluated) {
892+
promises.push(mod.promise)
893+
}
894+
}
895+
if (!promises.length && !resolvingCount) {
896+
return
897+
}
898+
await Promise.allSettled(promises)
899+
await waitForImportsToResolve()
900+
}

packages/vitest/src/node/browser/sessions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export class BrowserSessions {
4141
this.sessions.set(sessionId, {
4242
project,
4343
otelCarrier: options?.otelCarrier,
44+
// assigned by the pool on the session's first run, freed when it disconnects
45+
concurrencyId: 0,
4446
connected: () => {
4547
isConnected = true
4648
resolveIfReady()

packages/vitest/src/node/pool.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ export function createPool(ctx: Vitest): ProcessPool {
8484
// browser pool has a more complex logic, so we keep it separately for now
8585
browserSpecs: TestSpecification[]
8686
}[] = []
87-
let workerId = 0
87+
let workerId = 1
8888

8989
const sorted = await sequencer.sort(specs)
9090
const { environments, tags } = await getSpecificationsOptions(specs)

packages/vitest/src/node/pools/browser.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ class BrowserPool {
264264
this.project.vitest._browserSessions.sessionIds.add(sessionId)
265265
const project = this.project.name
266266
debug?.('[%s] creating session for %s', sessionId, project)
267-
let page = this._traces.$(
267+
const page = this._traces.$(
268268
`vitest.browser.open`,
269269
{
270270
context: this._otel.context,
@@ -273,8 +273,7 @@ class BrowserPool {
273273
},
274274
},
275275
() => this.openPage(sessionId, { parallel: workerCount > 1 }),
276-
)
277-
page = page.then(() => {
276+
).then(() => {
278277
// start running tests on the page when it's ready
279278
this.runNextTest(method, sessionId)
280279
})
@@ -292,6 +291,33 @@ class BrowserPool {
292291
})
293292
}
294293

294+
// stable slot id (1..maxWorkers) assigned to each session/orchestrator on its
295+
// first run, exposed to the test runner as both `concurrencyId` and `workerId`.
296+
// the id lives on the session, so it is freed when the session disconnects, and
297+
// the used set is derived from the live orchestrators, so it stays within maxWorkers
298+
private getConcurrencyId(sessionId: string): number {
299+
const sessions = this.project.vitest._browserSessions
300+
const session = sessions.getSession(sessionId)
301+
if (session?.concurrencyId) {
302+
return session.concurrencyId
303+
}
304+
const used = new Set<number>()
305+
for (const id of this.orchestrators.keys()) {
306+
const concurrencyId = sessions.getSession(id)?.concurrencyId
307+
if (concurrencyId) {
308+
used.add(concurrencyId)
309+
}
310+
}
311+
let concurrencyId = 1
312+
while (used.has(concurrencyId)) {
313+
concurrencyId++
314+
}
315+
if (session) {
316+
session.concurrencyId = concurrencyId
317+
}
318+
return concurrencyId
319+
}
320+
295321
private getOrchestrator(sessionId: string) {
296322
const orchestrator = this.orchestrators.get(sessionId)
297323
if (!orchestrator) {
@@ -359,6 +385,7 @@ class BrowserPool {
359385
},
360386
},
361387
async () => {
388+
const concurrencyId = this.getConcurrencyId(sessionId)
362389
return orchestrator.createTesters(
363390
{
364391
method,
@@ -367,6 +394,10 @@ class BrowserPool {
367394
// so we need to stringify it first to avoid double serialization
368395
providedContext: this._providedContext || '[{}]',
369396
otelCarrier: this._traces.getContextCarrier(),
397+
concurrencyId,
398+
// in the browser there is a single tab per orchestrator,
399+
// so the worker id matches the concurrency slot
400+
workerId: concurrencyId,
370401
},
371402
)
372403
},

0 commit comments

Comments
 (0)