/
collect.ts
161 lines (154 loc) · 4.54 KB
/
collect.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import { relative } from 'pathe'
import { parse as parseAst } from 'acorn'
import { ancestor as walkAst } from 'acorn-walk'
import type { RawSourceMap } from 'vite-node'
import { calculateSuiteHash, generateHash, interpretTaskModes, someTasksAreOnly } from '@vitest/runner/utils'
import type { File, Suite, Test } from '../types'
import type { WorkspaceProject } from '../node/workspace'
interface ParsedFile extends File {
start: number
end: number
}
interface ParsedTest extends Test {
start: number
end: number
}
interface ParsedSuite extends Suite {
start: number
end: number
}
interface LocalCallDefinition {
start: number
end: number
name: string
type: 'suite' | 'test'
mode: 'run' | 'skip' | 'only' | 'todo'
task: ParsedSuite | ParsedFile | ParsedTest
}
export interface FileInformation {
file: File
filepath: string
parsed: string
map: RawSourceMap | null
definitions: LocalCallDefinition[]
}
export async function collectTests(ctx: WorkspaceProject, filepath: string): Promise<null | FileInformation> {
const request = await ctx.vitenode.transformRequest(filepath, filepath)
if (!request)
return null
const ast = parseAst(request.code, {
ecmaVersion: 'latest',
allowAwaitOutsideFunction: true,
})
const testFilepath = relative(ctx.config.root, filepath)
const file: ParsedFile = {
filepath,
type: 'suite',
id: generateHash(`${testFilepath}${ctx.config.name || ''}`),
name: testFilepath,
mode: 'run',
tasks: [],
start: ast.start,
end: ast.end,
meta: { typecheck: true },
}
const definitions: LocalCallDefinition[] = []
const getName = (callee: any): string | null => {
if (!callee)
return null
if (callee.type === 'Identifier')
return callee.name
if (callee.type === 'MemberExpression') {
// direct call as `__vite_ssr_exports_0__.test()`
if (callee.object?.name?.startsWith('__vite_ssr_'))
return getName(callee.property)
// call as `__vite_ssr__.test.skip()`
return getName(callee.object?.property)
}
return null
}
walkAst(ast, {
CallExpression(node) {
const { callee } = node as any
const name = getName(callee)
if (!name)
return
if (!['it', 'test', 'describe', 'suite'].includes(name))
return
const { arguments: [{ value: message }] } = node as any
const property = callee?.property?.name
let mode = (!property || property === name) ? 'run' : property
if (!['run', 'skip', 'todo', 'only', 'skipIf', 'runIf'].includes(mode))
throw new Error(`${name}.${mode} syntax is not supported when testing types`)
// cannot statically analyze, so we always skip it
if (mode === 'skipIf' || mode === 'runIf')
mode = 'skip'
definitions.push({
start: node.start,
end: node.end,
name: message,
type: (name === 'it' || name === 'test') ? 'test' : 'suite',
mode,
} as LocalCallDefinition)
},
})
let lastSuite: ParsedSuite = file
const updateLatestSuite = (index: number) => {
const suite = lastSuite
while (lastSuite !== file && lastSuite.end < index)
lastSuite = suite.suite as ParsedSuite
return lastSuite
}
definitions.sort((a, b) => a.start - b.start).forEach((definition) => {
const latestSuite = updateLatestSuite(definition.start)
let mode = definition.mode
if (latestSuite.mode !== 'run') // inherit suite mode, if it's set
mode = latestSuite.mode
if (definition.type === 'suite') {
const task: ParsedSuite = {
type: definition.type,
id: '',
suite: latestSuite,
file,
tasks: [],
mode,
name: definition.name,
end: definition.end,
start: definition.start,
meta: {
typecheck: true,
},
}
definition.task = task
latestSuite.tasks.push(task)
lastSuite = task
return
}
const task: ParsedTest = {
type: definition.type,
id: '',
suite: latestSuite,
file,
mode,
context: {} as any, // not used in typecheck
name: definition.name,
end: definition.end,
start: definition.start,
meta: {
typecheck: true,
},
}
definition.task = task
latestSuite.tasks.push(task)
})
calculateSuiteHash(file)
const hasOnly = someTasksAreOnly(file)
interpretTaskModes(file, ctx.config.testNamePattern, hasOnly, false, ctx.config.allowOnly)
return {
file,
parsed: request.code,
filepath,
map: request.map as RawSourceMap | null,
definitions,
}
}