Skip to content

Commit 1de0aa2

Browse files
authored
fix: correctly identify concurrent test during static analysis (#9846)
1 parent f48a601 commit 1de0aa2

File tree

2 files changed

+360
-25
lines changed

2 files changed

+360
-25
lines changed

packages/vitest/src/node/ast-collect.ts

Lines changed: 76 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { File, Suite, Task, Test } from '@vitest/runner'
2-
import type { Property } from 'estree'
32
import type { SerializedConfig } from '../runtime/config'
43
import type { TestError } from '../types/general'
54
import type { TestProject } from './project'
@@ -46,6 +45,8 @@ interface LocalCallDefinition {
4645
mode: 'run' | 'skip' | 'only' | 'todo' | 'queued'
4746
task: ParsedSuite | ParsedFile | ParsedTest
4847
dynamic: boolean
48+
concurrent: boolean
49+
sequential: boolean
4950
tags: string[]
5051
}
5152

@@ -103,8 +104,8 @@ function astParseFile(filepath: string, code: string) {
103104
) {
104105
return getName(callee.property)
105106
}
106-
// call as `__vite_ssr__.test.skip()`
107-
return getName(callee.object?.property)
107+
// call as `__vite_ssr__.test.skip()` or `describe.concurrent.each()`
108+
return getName(callee.object)
108109
}
109110
// unwrap (0, ...)
110111
if (callee.type === 'SequenceExpression' && callee.expressions.length === 2) {
@@ -116,6 +117,29 @@ function astParseFile(filepath: string, code: string) {
116117
return null
117118
}
118119

120+
const getProperties = (callee: any): string[] => {
121+
if (!callee) {
122+
return []
123+
}
124+
if (callee.type === 'Identifier') {
125+
return []
126+
}
127+
if (callee.type === 'CallExpression') {
128+
return getProperties(callee.callee)
129+
}
130+
if (callee.type === 'TaggedTemplateExpression') {
131+
return getProperties(callee.tag)
132+
}
133+
if (callee.type === 'MemberExpression') {
134+
const props = getProperties(callee.object)
135+
if (callee.property?.name) {
136+
props.push(callee.property.name)
137+
}
138+
return props
139+
}
140+
return []
141+
}
142+
119143
walkAst(ast as any, {
120144
CallExpression(node) {
121145
const { callee } = node as any
@@ -127,12 +151,24 @@ function astParseFile(filepath: string, code: string) {
127151
verbose?.(`Skipping ${name} (unknown call)`)
128152
return
129153
}
154+
const properties = getProperties(callee)
130155
const property = callee?.property?.name
131-
let mode = !property || property === name ? 'run' : property
132-
// they will be picked up in the next iteration
133-
if (['each', 'for', 'skipIf', 'runIf', 'extend', 'scoped', 'override'].includes(mode)) {
156+
// intermediate calls like .each(), .for() will be picked up in the next iteration
157+
if (property && ['each', 'for', 'skipIf', 'runIf', 'extend', 'scoped', 'override'].includes(property)) {
134158
return
135159
}
160+
// derive mode from the full chain (handles any order like .skip.concurrent or .concurrent.skip)
161+
let mode: 'run' | 'skip' | 'only' | 'todo' = 'run'
162+
for (const prop of properties) {
163+
if (prop === 'skip' || prop === 'only' || prop === 'todo') {
164+
mode = prop
165+
}
166+
else if (prop === 'skipIf' || prop === 'runIf') {
167+
mode = 'skip'
168+
}
169+
}
170+
let isConcurrent = properties.includes('concurrent')
171+
let isSequential = properties.includes('sequential')
136172

137173
let start: number
138174
const end = node.end
@@ -179,39 +215,46 @@ function astParseFile(filepath: string, code: string) {
179215
// Vitest module mocker injects these
180216
.replace(/__vi_import_\d+__\./g, '')
181217

182-
// cannot statically analyze, so we always skip it
183-
if (mode === 'skipIf' || mode === 'runIf') {
184-
mode = 'skip'
185-
}
186-
187218
const parentCalleeName = typeof callee?.callee === 'object' && callee?.callee.type === 'MemberExpression' && callee?.callee.property?.name
188219
let isDynamicEach = parentCalleeName === 'each' || parentCalleeName === 'for'
189220
if (!isDynamicEach && callee.type === 'TaggedTemplateExpression') {
190221
const property = callee.tag?.property?.name
191222
isDynamicEach = property === 'each' || property === 'for'
192223
}
193224

194-
// Extract tags from the second argument if it's an options object
225+
// Extract options from the second argument if it's an options object
195226
const tags: string[] = []
196227
const secondArg = node.arguments?.[1]
197228
if (secondArg?.type === 'ObjectExpression') {
198-
const tagsProperty = secondArg.properties?.find(
199-
(p: any) => p.type === 'Property' && p.key?.type === 'Identifier' && p.key.name === 'tags',
200-
) as Property | undefined
201-
if (tagsProperty) {
202-
const tagsValue = tagsProperty.value
203-
if (tagsValue?.type === 'Literal' && typeof tagsValue.value === 'string') {
204-
// tags: 'single-tag'
205-
tags.push(tagsValue.value)
229+
for (const prop of (secondArg.properties || []) as any[]) {
230+
if (prop.type !== 'Property' || prop.key?.type !== 'Identifier') {
231+
continue
206232
}
207-
else if (tagsValue?.type === 'ArrayExpression') {
208-
// tags: ['tag1', 'tag2']
209-
for (const element of tagsValue.elements || []) {
210-
if (element?.type === 'Literal' && typeof element.value === 'string') {
211-
tags.push(element.value)
233+
const keyName = prop.key.name
234+
if (keyName === 'tags') {
235+
const tagsValue = prop.value
236+
if (tagsValue?.type === 'Literal' && typeof tagsValue.value === 'string') {
237+
tags.push(tagsValue.value)
238+
}
239+
else if (tagsValue?.type === 'ArrayExpression') {
240+
for (const element of tagsValue.elements || []) {
241+
if (element?.type === 'Literal' && typeof element.value === 'string') {
242+
tags.push(element.value)
243+
}
212244
}
213245
}
214246
}
247+
else if (prop.value?.type === 'Literal' && prop.value.value === true) {
248+
if (keyName === 'skip' || keyName === 'only' || keyName === 'todo') {
249+
mode = keyName
250+
}
251+
else if (keyName === 'concurrent') {
252+
isConcurrent = true
253+
}
254+
else if (keyName === 'sequential') {
255+
isSequential = true
256+
}
257+
}
215258
}
216259
}
217260

@@ -224,6 +267,8 @@ function astParseFile(filepath: string, code: string) {
224267
mode,
225268
task: null as any,
226269
dynamic: isDynamicEach,
270+
concurrent: isConcurrent,
271+
sequential: isSequential,
227272
tags,
228273
} satisfies LocalCallDefinition)
229274
},
@@ -366,6 +411,10 @@ function createFileTask(
366411
// Inherit tags from parent suite and merge with own tags
367412
const parentTags = latestSuite.tags || []
368413
const taskTags = unique([...parentTags, ...definition.tags])
414+
// resolve concurrent/sequential: sequential cancels inherited concurrent
415+
const concurrent = definition.sequential
416+
? undefined
417+
: (definition.concurrent || latestSuite.concurrent || undefined)
369418

370419
if (definition.type === 'suite') {
371420
const task: ParsedSuite = {
@@ -376,6 +425,7 @@ function createFileTask(
376425
tasks: [],
377426
mode,
378427
each: definition.dynamic,
428+
concurrent,
379429
name: definition.name,
380430
fullName: createTaskName([latestSuite.fullName, definition.name]),
381431
fullTestName: createTaskName([latestSuite.fullTestName, definition.name]),
@@ -398,6 +448,7 @@ function createFileTask(
398448
suite: latestSuite,
399449
file,
400450
each: definition.dynamic,
451+
concurrent,
401452
mode,
402453
context: {} as any, // not used on the server
403454
name: definition.name,

0 commit comments

Comments
 (0)